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

272 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SSH ツール詳細ガイド (SshExec / SshUpload / SshDownload / SshListConnections)
リモートサーバーで shell コマンドを実行したり、ワークスペースとリモートファイルシステムの間でファイルを転送するためのツール群。同じ前提・同じエラーモデル・同じ監査経路を共有するので、本ドキュメントに統合してある。運用者向けの設計・設定詳細は **[docs/ssh.md](../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: true`** が `config.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
```js
SshListConnections({})
```
引数なし。現在の movement の `allowed_ssh_connections` + ジョブ owner の access grant を満たす接続だけを返す (admin 無効化 / piece 除外 / grant 無しは filter out)。
戻り値 (JSON 文字列):
```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
```js
SshExec({
connection_id: "abcd1234-...",
command: "ls -la /srv/agent",
timeout_ms: 30000 // 任意
})
```
戻り値 (JSON 文字列):
```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
```js
SshUpload({
connection_id: "abcd1234-...",
local_path: "output/report.csv", // workspace 相対
remote_path: "/srv/agent/2026-05/report.csv" // 絶対パス、prefix 配下
})
```
戻り値:
```json
{
"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
```js
SshDownload({
connection_id: "abcd1234-...",
remote_path: "/srv/agent/2026-05/log.txt",
local_path: "input/log.txt" // workspace 相対
})
```
戻り値:
```json
{
"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 に取り込む
```js
// 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 で加工した設定ファイルを反映
```js
// 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. 大量出力を直接受け取らずファイル経由で扱う
```js
// 直接 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](../ssh.md) — 設定・UI フロー・運用ガイド・セキュリティモデル