# SSH Console Tools (SshConsoleEnsure / SshConsoleSend / SshConsoleSnapshot) AI と人間が共有する SSH PTY セッションを操作する 3 ツール。1 タスクに 1 PTY セッションが対応し、`cd` / 環境変数 / foreground プロセスは job をまたいで維持される。長時間の対話作業 / TUI (vim, top, less, tmux) / 複数ラウンドの調査向け。 単発コマンドだけなら **`SshExec`** (ssh-ops piece) のほうが軽い。本ツール群は対話的シェル + AI が画面を見続ける用途に最適化されている。 ## 典型的な flow (まずこれを真似る) ```js // 1. どの接続が使えるか発見 (タスク本文に UUID が無いとき) SshListConnections({}) // → {"connections":[{"id":"abcd1234-...","label":"prod-aao","host":"...","host_key_verified":true}]} // 2. セッション確保 (冪等。何度呼んでも同じセッションを返す) SshConsoleEnsure({ connection_id: "abcd1234-..." }) // → {"ok":true,"reused":false,"connection_id":"abcd1234-...","cols":120,"rows":32} // 3. コマンドを送信。改行で実行される SshConsoleSend({ connection_id: "abcd1234-...", input: "uptime\n", wait_ms: 800, // 出力が落ち着くまで待つ ms (default 500, max 5000) }) // → {"ok":true,"bytes_sent":7,"screen_after":"... load average: 0.05 ...","new_output_bytes":120} // 4. screen_after で見切れた場合は scrollback を取得 SshConsoleSnapshot({ connection_id: "abcd1234-...", kind: "scrollback", max_bytes: 32768, }) // → {"kind":"scrollback","byte_count":12345,"truncated":false,"text":"..."} ``` ## SshConsoleEnsure セッションを確保する (無ければ open、有れば再利用)。**冪等**。`SshConsoleSend` を呼ぶ前に必須ではない (auto-ensure される) が、最初に明示的に呼んでおくと「セッション開設に成功した」ことを確認できる。 | Param | Required | Description | |---|---|---| | `connection_id` | yes | UUID。piece の `allowed_ssh_connections` に含まれている必要がある。**label / hostname / 思い出した文字列で代用してはいけない** — 必ず `SshListConnections` の `id` を渡すこと | | `cols` | no | 初回 open 時のターミナル幅。default `ssh.console.default_cols` (120) | | `rows` | no | 初回 open 時のターミナル高さ。default `ssh.console.default_rows` (32) | | `force_replace` | no | bool。default `false`。既存 session が**別の** `connection_id` にある場合の挙動を制御 (下記参照) | Return: ```json {"ok": true, "reused": , "connection_id": "...", "cols": 120, "rows": 32, "host_fingerprint": "SHA256:..."} ``` `reused: true` なら過去ターンから引き継いだ既存セッション (cd 等の state あり)。`false` なら今回新規 open。 ### connection_id mismatch の挙動 (重要) 同じ task で**別の** `connection_id` を渡した場合: - `force_replace: false` (default) → エラー返却。レスポンスに **既存セッションの connection_id が含まれる** ので、それをそのまま使うか、本当に切り替えたければ次の呼び出しで `force_replace: true` を渡す - `force_replace: true` → 旧セッションは `connection_change` 理由で閉じられ、新セッションが開く (旧 shell の state は失われる) **典型的なバグパターン**: ジョブをまたいで動作するエージェントが `connection_id` を覚えていなくて、 LLM の hallucination で適当な UUID を生成 → mismatch reject される、というケース。エラーメッセージの中に 正しい `connection_id` が出ているのでそれを使うか、Send/Snapshot で `connection_id` を省略する。 ## SshConsoleSend 入力を送る。**printable な shell コマンド (改行なし、制御文字なし、2 文字以上) には server が自動で末尾に `\n` を付加して実行する**。例: `input: "ls -la"` でも `input: "ls -la\n"` でも同じ結果。 auto-append が発火した時は response に `auto_newline_appended: true` が載るので、必要なら呼び出し側で検知できる。 raw のまま送りたい (改行を付けない) ケース: - sudo の password prompt に応答中 (echo OFF — タイプ + 別 Send で `\n`) - vim の insert mode で文字を順に打鍵 - less / top / htop 等 TUI で 1 キー操作 (`q`, `j`, `k`, space, etc.) - これらは制御文字を含むか 1 文字なので auto-append は発火しない。 | Param | Required | Description | |---|---|---| | `connection_id` | no | UUID。**省略時はこの task の active session を自動採用 (推奨)**。明示する場合は active session の id と一致する必要があり、不一致なら reject (active id が surface される) | | `input` | yes | raw 文字列。LF / CRLF / control 文字 (`\x03` Ctrl-C, `\x04` Ctrl-D, `\x1b` Esc, `\t` Tab) を透過 | | `wait_ms` | no | 送信後の screen_after 取得までの待ち時間 (default 500ms, max 5000ms) | Return: ```json { "ok": true, "bytes_sent": 7, "screen_after": "user@your-host:~$ uptime\n 12:34 ...", "new_output_bytes": 120 } ``` ### 入力フィルタ 各 line は connection 側の `deny_patterns` / `allow_patterns` (および組み込み deny-list) と照合される。1 行でも NG にひっかかると入力**全体**が reject される (部分実行はしない)。エラー例: `SshConsoleSend: line 2 rejected by builtin_deny (rm\s+-rf).` ### TUI 操作のコツ - vim 起動: `SshConsoleSend({input: "vim test.txt\n", wait_ms: 1000})` → 待ってから `SshConsoleSnapshot` で画面確認 - vim 抜ける: `SshConsoleSend({input: "\x1b:q!\n"})` (`\x1b` は Esc) - top/htop 抜ける: `SshConsoleSend({input: "q"})` - 走行中プロセス中断: `SshConsoleSend({input: "\x03"})` (Ctrl-C) - パス完成 (Tab): `SshConsoleSend({input: "ls /var/lo\t"})` (Tab だけ送って screen で候補確認) ### よくある間違い - `wait_ms` が短すぎて screen_after に出力が間に合わない → 再度 `SshConsoleSnapshot` で取り直す - printable input は server が自動で `\n` を付加するので改行忘れは基本問題ない。raw 入力したい場合 (TUI 操作等) は制御文字を含めること - 大量出力で screen_after が切れる → `SshConsoleSnapshot({kind: "scrollback"})` で取得 ## SshConsoleSnapshot | Param | Required | Description | |---|---|---| | `connection_id` | no | UUID。**省略時はこの task の active session を自動採用 (推奨)**。明示する場合は active session の id と一致する必要があり、不一致なら reject | | `kind` | no | `screen` (デフォルト) — 現在の表示画面 / `scrollback` — それ以前を含む過去の出力 | | `max_bytes` | no | scrollback の上限 (default 8192, max 65536)。tail から `max_bytes` バイト返す | Return (kind=screen): ```json {"kind":"screen","cols":120,"rows":32,"text":"...","cursor":{"x":0,"y":15}} ``` Return (kind=scrollback): ```json {"kind":"scrollback","byte_count":123456,"truncated":true,"text":"..."} ``` text は ANSI escape strip 済み (色 / cursor 移動シーケンスを除去)。raw が必要な場合は audit log を参照。 ## エラー時のリカバリ | エラー | 対応 | |---|---| | `host_key_*` | UI (Settings → User Folder → SSH Connections) で TOFU 検証してから再試行 | | `command_rejected (builtin_deny / custom_deny)` | deny-list で reject。admin に許可パターン追加を相談 (ローカルで回避してはいけない) | | `idle_timeout` / `duration_cap` | 古いセッションが閉じた。`SshConsoleEnsure` を再度呼んで開け直す | | `connection_change` | 同 task で `force_replace: true` 付き Ensure が呼ばれた → 古いセッションが閉じた | | `this task already has an active session on connection X (...)` | エラー文の中の **X が正しい id**。X を `connection_id` に使うか、Send/Snapshot で省略する。本当に切り替えたければ `force_replace: true` | | `this task has an active session on connection X, not Y` | Send/Snapshot 側で id mismatch。X を使う or 省略する | | `maintenance` | admin の対応を待つ。`complete({status: 'needs_user_input', missing_info: 'SSH maintenance window'})` で停止 | | `not initialised` | `ssh.enabled` または `ssh.console.enabled` が false / `MCP_ENCRYPTION_KEY` 未設定。admin に依頼 | | `does not declare allowed_ssh_connections` | piece YAML の movement に `allowed_ssh_connections: ['*']` 等を追加する必要あり | ## deny-list の限界 deny-list は **first line of defense** であって信頼境界ではない。`bash -c "..."` や `$VAR` 経由の動的展開は通る。多層防御 (audit + abuse lock + admin kill) で運用する。 機密値 (token / password / SSH key) は input 文字列に直接書かない。サーバー側の env / config / secrets manager から読ませる。