14 KiB
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 で記録される)。
ssh.enabled: trueがconfig.yamlで設定されているMCP_ENCRYPTION_KEY環境変数が 64 hex 文字 (= 32 バイト) で設定されている- 対象 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 する - 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 にcountとwildcardフラグ)
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.yamlのssh.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.yaml の ssh.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.yaml の ssh.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.yaml の ssh.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.exec の detail には 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 フロー・運用ガイド・セキュリティモデル