maestro/docs/tools/ssh-tools.md
clade 7049a874f3 feat: initial public release (MAESTRO v0.1.0)
Open-source release of MAESTRO, an agent orchestration platform that runs
LLM-driven tasks through sandboxed tools, with a web UI. Apache-2.0.
See README.md and docs/ (getting-started, configuration, architecture).
2026-06-03 04:01:14 +00:00

14 KiB
Raw Blame History

SSH ツール詳細ガイド (SshExec / SshUpload / SshDownload / SshListConnections)

リモートサーバーで shell コマンドを実行したり、ワークスペースとリモートファイルシステムの間でファイルを転送するためのツール群。同じ前提・同じエラーモデル・同じ監査経路を共有するので、本ドキュメントに統合してある。運用者向けの設計・設定詳細は docs/ssh.md を参照。

4 ツールの位置づけ

ツール 用途 入力
SshListConnections この movement で使える接続の UUID + label + host 一覧を取得 (引数なし)
SshExec リモートで shell 単一行を実行 connection_id, command, (任意) timeout_ms
SshUpload workspace → リモートへファイル転送 (SFTP) connection_id, local_path, remote_path, (任意) timeout_ms
SshDownload リモート → workspace へファイル取得 (SFTP) connection_id, remote_path, local_path, (任意) timeout_ms

転送系の 3 ツールは、接続側の remote_path_prefix 配下の絶対パスのみを受け付け、workspace 外への local パスは reject される。connection_id は piece 側の allowed_ssh_connections に明示されている UUID のみ使用可能。

タスク本文に connection_id が記されていないときは、まず SshListConnections を呼んで該当の host / label の UUID を取得すること。

共通: 4 つの前提条件

ツール呼び出し前に以下が全て揃っている必要がある。どれか一つでも欠けると即エラー応答 (audit には denied で記録される)。

  1. ssh.enabled: trueconfig.yaml で設定されている
  2. MCP_ENCRYPTION_KEY 環境変数が 64 hex 文字 (= 32 バイト) で設定されている
  3. 対象 connection の host key が verify 済。新規作成直後は host_key_verified_at IS NULL 状態で SshExec/Upload/Download は host_key_not_verified で失敗する。SSH Connections パネル (Settings → User Folder → SSH Connections) で /test を実行 → 鍵 fingerprint を確認 → "Verify" ボタンで verify する
  4. piece の現在 movement で allowed_ssh_connections に当該 UUID が明示されている (またはワイルドカード *)。空配列 [] は「SSH 使用するが許可なし」の deny 宣言とみなされ全 UUID が reject される

不足時のエラーメッセージ例: SshExec error: piece "ops" movement "exec" does not list connection abcd1234... in allowed_ssh_connections.

SshListConnections

SshListConnections({})

引数なし。現在の movement の allowed_ssh_connections + ジョブ owner の access grant を満たす接続だけを返す (admin 無効化 / piece 除外 / grant 無しは filter out)。

戻り値 (JSON 文字列):

{
  "connections": [
    {
      "id": "abcd1234-5678-90ab-cdef-1234567890ab",
      "label": "prod-aao",
      "host": "10.0.0.10",
      "port": 22,
      "username": "deploy",
      "host_key_verified": true,
      "host_key_pending": false
    }
  ]
}
  • host_key_verified: false の接続は SshExec/Upload/Download/Console* で使う前に UI から TOFU 検証する必要がある (host_key_pending: true ならまだ未検証で取り消し可能な状態)
  • connections が空配列の場合は admin に接続登録 / grant 発行を依頼する
  • 通常は 最初に呼ぶ ことで AI が "どの host か" を発見できる。1 ターンで複数回呼ぶ必要はない (結果は安定)
  • 監査 action: ssh.list_connections (detail に countwildcard フラグ)

SshExec

SshExec({
  connection_id: "abcd1234-...",
  command: "ls -la /srv/agent",
  timeout_ms: 30000  // 任意
})

戻り値 (JSON 文字列):

{
  "exit_code": 0,
  "stdout": "total 12\ndrwxr-xr-x 3 agent agent ...",
  "stderr": "",
  "truncated_stdout": false,
  "truncated_stderr": false
}
  • exit_code は remote プロセスの終了コード。0 でない場合も isError=false で返り、LLM が判断する
  • 標準出力は config.yamlssh.max_output_bytes (デフォルト 32 KiB) で truncate される。truncated_stdout: true の場合はコマンドを head / tail / grep で絞り込んで再試行する
  • 同等以上のサイズが見込まれる出力は SshDownload でファイル取得した上で Read で扱うこと

command フィルタリング (2 段)

  • 組み込み deny-list: rm -rf /, mkfs, dd, :(){:|:&};: 系のシステム破壊 / fork bomb 系を unconditional で reject
  • 接続側カスタム正規表現: 接続作成時に deny_patterns / allow_patterns (改行区切りの正規表現リスト) を設定可能。デフォルトは空 (= 制限なし)。allow_patterns を設定した場合、deny を通過した後さらに全 allow パターンに合致しないと reject

エラー: SshExec error: command rejected by built-in deny-list (matched pattern: rm\s+-rf). / command rejected by connection deny-list.

timeout

timeout_ms 未指定時は config.yamlssh.call_timeout_seconds (デフォルト 30 秒)。これは TCP 接続 + handshake + 認証 + コマンド実行を全て含む wall-clock。タイムアウトすると exec_timeout エラーで終了し、audit row は failed outcome + detail.error = 'exec_timeout' で記録される (途中で生成された stdout は破棄される)。

SshUpload

SshUpload({
  connection_id: "abcd1234-...",
  local_path: "output/report.csv",       // workspace 相対
  remote_path: "/srv/agent/2026-05/report.csv"  // 絶対パス、prefix 配下
})

戻り値:

{
  "ok": true,
  "bytes": 4096,
  "remote": "/srv/agent/2026-05/report.csv"
}
  • local_path: workspace ルートからの相対パス。シンボリックリンク経由で workspace 外を指すパスは O_NOFOLLOW + parent lstat で reject される
  • remote_path: 接続の remote_path_prefix (例: /srv/agent) 配下の絶対パスのみ。/srv/agent/../etc/passwd のような traversal は POSIX 正規化後に prefix 外と判定されて reject
  • アップロード先のディレクトリは事前に存在している必要がある (mkdir -p 相当を行いたければ先に SshExec({command: "mkdir -p /srv/agent/2026-05"}) を呼ぶ)
  • 既存ファイルへの上書きは現状 reject せず upload する。冪等性が必要な場合は呼び出し側で確認すること

サイズ上限

config.yamlssh.max_upload_size_mb (デフォルト 100 MB) を超える local ファイルは remote_too_large 相当で reject。

SshDownload

SshDownload({
  connection_id: "abcd1234-...",
  remote_path: "/srv/agent/2026-05/log.txt",
  local_path: "input/log.txt"            // workspace 相対
})

戻り値:

{
  "ok": true,
  "bytes": 8192,
  "local": "input/log.txt"
}
  • local_path既に存在するファイルへの上書きは reject される (local_target_exists エラー)。新規パスを指定するか、既存ファイルを別ツールで削除してから再試行
  • 親ディレクトリは呼び出し側で作成済にしておくこと。Write 相当の mkdir-p は行わない (e.g. output/foo/bar.txt を指定するなら、事前に Bash({command: "mkdir -p output/foo"}) 等で作成)
  • remote_path の prefix 配下チェック、サイズ上限 (ssh.max_download_size_mb)、SSRF チェックは Upload と同じ

Host key TOFU フロー (LLM 側で完結しない)

接続を新規作成した直後は host key が観測されていない (host_key_b64 IS NULL)。最初の /test 呼び出し (または最初の Exec/Upload/Download) で鍵を観測すると、host_key_first_observe エラーが返り、host_key_b64 / host_key_fingerprint / host_key_pending_token が DB に書き込まれる。

Host key first-observe on connection <id> (fingerprint SHA256:...).
Verify via UI (SshConnections panel) before retrying. Pending token: <uuid>

LLM ではここで止め、ユーザーに UI で fingerprint を確認 → Verify を依頼する。Verify を完了するまで全 SSH ツールは host_key_not_verified で失敗する。

サーバー再構築や鍵 rotation で fingerprint が変わると host_key_mismatch が返る。これは 既存鍵の上書きにあたるので reason 付きで UI から明示的に replace する必要がある (/replace-host-key エンドポイント)。LLM は自分で replace してはいけない。

WARN: Host key MISMATCH on connection <id> (now SHA256:...).
Likely possibilities: server rebuild, key rotation, or MITM.
Verify carefully via UI and supply a reason. Pending token: <uuid>

共通エラーコード一覧

isError: true で返るエラーメッセージは以下のいずれか。LLM は基本的に retry せず、メッセージに従って人に判断を仰ぐか、別の手段に切り替えること。

code 意味 対応
host_key_first_observe 初回鍵観測 UI で verify するようユーザーに依頼
host_key_mismatch 鍵 fingerprint が変化 UI で replace するようユーザーに依頼 (MITM 可能性)
host_key_not_verified 鍵記録済だが未 verify 同上、UI で verify
host_key_alg_not_allowed サーバーが禁止アルゴリズムを提示 接続不能、運用者に報告
auth_failed 秘密鍵が認証拒否された 接続設定 (key/username) を確認
connect_timeout ハンドシェイク前に timeout network 経路 / SSRF policy 確認
exec_timeout コマンド実行が timeout timeout_ms を増やす、コマンドを軽量化
transfer_timeout SFTP 転送が timeout ファイルサイズ確認、回線確認
output_too_large stdout が max_output_bytes 超過 フィルタリング、SshDownload に切替
remote_too_large ファイルが max_(up|down)load_size_mb 超過 サイズ確認、設定変更
local_target_exists download 先が既存 別パス選択
forbidden_address SSRF policy で reject private 接続なら allow_private_addresses 設定
invalid_host / dns_failed / connect_failed 接続 / DNS 失敗 host 設定、ネットワーク確認

abuse_locked / disabled_by_admin 等の運用上の reject は SshExec: access denied (...) for connection X. 形式のエラー (isError=true) で返る。

abuse counter による自動 lock

連続失敗を 3 つのスコープで集計する:

  • user: 同一ユーザー × 任意接続
  • host:user: 同一 (host, username) ペア
  • host (global): 同一 host (global connection のみ対象)

config.yamlssh.abuse_window_minutes (10) 以内に ssh.abuse_failure_threshold (5) 回失敗すると、当該スコープが ssh.abuse_lock_minutes (30) ロック。ロック解除は時間経過待ち、または admin が UI から force-unlock (理由 + 8 字以上必須、レート制限 10 回/時)。

成功すると user scope のカウンターだけクリアされる (他のスコープは時間経過で window から外れる)。

監査ログ

3 ツールはすべて以下のライフサイクルを踏む:

audit.begin (outcome=pending)  → commit (DB)
   ↓
  remote 呼び出し
   ↓
audit.complete (outcome=success | failed | denied | aborted)

途中でプロセスがクラッシュした場合、pending 行は次回起動時の recovery sweep で aborted に倒される (forensics 用「実行されたが結果不明」)。

action 名:

  • ssh.exec (SshExec)
  • ssh.upload (SshUpload)
  • ssh.download (SshDownload)
  • ssh.connection.host_key.first_observe / mismatch (TOFU 発火時)

ssh.execdetail には command そのものではなく SHA-256 truncated hex (16 char)command_hash として記録される。command 全文は記録されない (PII / secrets 漏洩防止)。retry 検知やパターン分析は hash 比較で行う。

監査ログの参照経路:

  • ユーザー本人の接続: SshConnections パネルの "Audit" タブ
  • admin (全接続): Settings → SSH → Audit Log (フィルタ: action / outcome / connection / time range)

Workflow Recipes

A. リモートで生成したレポートを workspace に取り込む

// 1. リモートでレポート生成
SshExec({ connection_id: CONN, command: "/srv/agent/build-report.sh > /tmp/report-$(date +%Y%m%d).csv" })
// 2. 生成パスを確認
const ls = SshExec({ connection_id: CONN, command: "ls -1 /tmp/report-*.csv | tail -1" })
const remote = JSON.parse(ls.output).stdout.trim()
// 3. workspace に取り込み
SshDownload({ connection_id: CONN, remote_path: remote, local_path: `input/${remote.split('/').pop()}` })

B. workspace で加工した設定ファイルを反映

// 1. ワークスペースで設定を生成
Write({ file_path: "output/nginx.conf", content: "..." })
// 2. リモートにアップロード
SshUpload({ connection_id: CONN, local_path: "output/nginx.conf", remote_path: "/srv/agent/nginx.conf" })
// 3. validate + reload
SshExec({ connection_id: CONN, command: "nginx -t -c /srv/agent/nginx.conf && systemctl reload nginx" })

C. 大量出力を直接受け取らずファイル経由で扱う

// 直接 SshExec すると max_output_bytes で truncate される
// → 一度ファイルに書いてから Download する
SshExec({ connection_id: CONN, command: "journalctl -u app --since '1 hour ago' > /tmp/app.log" })
SshDownload({ connection_id: CONN, remote_path: "/tmp/app.log", local_path: "input/app.log" })
Read({ file_path: "input/app.log", offset: 0, limit: 200 })  // 必要に応じて

関連ツール

  • Read / Write / Edit: workspace 内のファイルを扱う前後で組み合わせる
  • Bash: workspace 内でのローカル処理 (mkdir, jq 加工等)

参考

  • docs/ssh.md — 設定・UI フロー・運用ガイド・セキュリティモデル