12 KiB
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 が画面を見続ける用途に最適化されている。
ユーザーが先にセッションを開いている場合がある: タスク詳細の Console タブから、ユーザーが接続を選んで自分でセッションを起動できる。その場合
SshConsoleEnsureは既存セッションをそのまま再利用する (connection_idを省略すれば active session が採用される)。「まず console を開く」操作を AI 側でやり直す必要はない。
典型的な flow (まずこれを真似る)
// 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:
{"ok": true, "reused": <bool>, "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:
{
"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):
{"kind":"screen","cols":120,"rows":32,"text":"...","cursor":{"x":0,"y":15}}
Return (kind=scrollback):
{"kind":"scrollback","byte_count":123456,"truncated":true,"text":"..."}
text は ANSI escape strip 済み (色 / cursor 移動シーケンスを除去)。raw が必要な場合は audit log を参照。
SshConsoleRun
通常のコマンドは SshConsoleRun を使う。 raw SshConsoleSend は対話操作 (vim/REPL/sudo/TUI) と本当の中断専用。長いコマンドを Ctrl-C で殺さないこと。Ctrl-C は confirm_interrupt:true が必要。
シェルコマンドを実行し、完了まで blocking して {done, exit_code, output} を返す。SshConsoleSend のように「画面取得のタイミングを気にしてから結果を読む」作業が不要で、終了コードも自動取得できる。
| Param | Required | Description |
|---|---|---|
command |
yes | 実行するシェルコマンド |
connection_id |
no | UUID。省略時はこの task の active session を自動採用 (推奨) |
timeout_ms |
no | タイムアウト (ms)。デフォルト 120000 (2分)、最大 600000 (10分)。タイムアウト時もコマンドは kill されない |
idle_ms |
no | 出力が idle_ms ms 途切れたら早期終了と判定する。0=無効 (デフォルト) |
Return:
{
"done": true,
"exit_code": 0,
"output": "... コマンドの出力 ..."
}
done: false はタイムアウトで終了したケース。exit_code は shell が返した終了コード (非 0 はエラー)。
SshConsoleRun vs SshConsoleSend の使い分け
| 用途 | 使うツール |
|---|---|
| 通常のシェルコマンド (ls, grep, systemctl, make, ...) | SshConsoleRun |
| 対話的 TUI (vim, top, htop, tmux, ...) | SshConsoleSend + SshConsoleSnapshot |
| REPL / sudo パスワード入力 | SshConsoleSend |
| プロセス中断 (Ctrl-C) | SshConsoleSend({input: "\x03", confirm_interrupt: true}) |
| 長時間バックグラウンドを待つ | SshConsoleRun({timeout_ms: 300000}) |
よくある間違い
- 長時間コマンドに
timeout_msを指定し忘れる → デフォルト 2 分でタイムアウト。timeout_msを伸ばすこと - コマンドが止まらないからと安易に Ctrl-C を送る →
confirm_interrupt:trueを付けた SshConsoleSend が必要。SshConsoleRun の途中で別の SshConsoleSend を送ってはいけない done: falseを無視してそのまま次に進む → コマンドはまだ動いている可能性がある。SshConsoleSnapshot で状態確認
エラー時のリカバリ
| エラー | 対応 |
|---|---|
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 から読ませる。