サンドボックスバックエンド
職務
agent はコマンドを実行し、コードを走らせるが、任意のコマンドをホスト機に直接叩かせてはいけない。Hermes のターミナルツールは 6 種の実行バックエンドをサポートし、安全/隔離/コストのトレードオフで選ぶ:Local(本機直実行)、Docker(コンテナ隔離)、SSH(遠隔機)、Singularity(HPC コンテナ)、Modal(クラウドサンドボックス (cloud sandbox)、direct または managed)、Daytona(クラウド開発環境)。コンテナ硬化 + 名前空間隔離で、危険コマンドは承認コールバックでブロックする。
設計動機
なぜ 1 種ではなく 6 種なのか? agent の実行シナリオは幅が広い——開発時は本機で素早く走らせたい、本番バッチはコンテナ隔離が必要、HPC ユーザーは(Docker を使えず)Singularity で走らなければならない、クラウドの無人運転は Modal のような秒課金クラウドサンドボックスが要る。単一のバックエンドでは覆えない。Hermes は「実行」を統一インターフェースに抽象し、バックエンドをシナリオで組み立て、危険コマンドの承認コールバックはバックエンドをまたいで共用する。Docker バックエンドは bind mount がホストパスを露出するかを専用に検出する——コンテナ隔離の安全の前提は「コンテナは外を見えない」ことで、docker run -v /:/host された時点で崩れるからだ。Modal は direct(ユーザー自身のクレデンシャル)と managed(ゲートウェイ (gateway) 管理)を区別する——前者はユーザーの請求、後者は Nous ポータルのクレジットに計上され、権限と課金パスが全く違う。これらの検出ロジックはバックエンドの中には書かず、tool_backend_helpers.py に抽出し、バックエンド選択と安全レベル判定を疎結合にする。
主要ファイル
terminal_tool ヘッダドキュメント:1-15— local / docker / modal / ssh / singularity / daytona の 6 バックエンドを列挙Docker host アクセス検出:260-290—_docker_volume_uses_host_path/_docker_has_host_access(bind mount がホストパスを露出するか検出)Singularity / Modal インポート:65-90—_get_scratch_dir(environments/singularity から)environments/singularity.py— Singularity scratch ディレクトリと SIF キャッシュModal モード解析:77-137—coerce_modal_mode/has_direct_modal_credentials/resolve_modal_backend_state(direct vs managed)_DEFAULT_BROWSER_PROVIDER:12— デフォルト provider は localサブエージェント承認コールバック:75-103—_subagent_auto_deny/_subagent_auto_approve(サンドボックス内実行の危険コマンド管理)tools/daemon_pool.py— サブエージェント常駐スレッドプール(サンドボックス実行を担う)tools/ ディレクトリ— terminal / close_terminal / read_terminal / tool_backend_helpers / environments/
データフロー
- agent がコマンド実行を決める → tool_call
terminal→tools/terminal_tool.py。 - ジョブ設定でバックエンドを選ぶ(local/docker/modal/ssh/singularity/daytona):
local:ホスト直実行(最速、デフォルト)docker:tools/terminal_tool.py:260が bind mount がホストパスを露出するか検出し、権限を決めるmodal:tools/tool_backend_helpers.py:102で direct(ユーザー自身の Modal クレデンシャル)か managed(ゲートウェイ管理)かを解析singularity:tools/environments/singularity.pyで scratch と SIF を準備ssh/daytona:遠隔/クラウド環境
- 危険コマンドは承認コールバックでブロック——主ループは対話的承認。サブエージェントはデフォルト auto-deny(
tools/delegate_tool.py:75)、cron/バッチは opt-in で auto-approve 可能。 - コマンド出力を主ループに戻す。長タスクはバックグラウンド化可能(
tools/daemon_pool.py)。
バックエンドリストの宣言はシンプルで、terminal_tool.py の冒頭コメント一行に過ぎない:「A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.」
Docker バックエンドは bind mount がホストパスかを検出する。spec の書き出しを見る:/、~、./、../ で始まるか、Windows ドライブレター C:\ のように二文字目がコロンなら、ホストパスとみなして has_host_access=True にし、以降の権限はより厳しい分岐に流れる。
def _docker_volume_uses_host_path(volume_spec: str) -> bool:
"""Return True when a docker volume spec bind-mounts a host path."""
if not isinstance(volume_spec, str):
return False
vol = volume_spec.strip()
return bool(vol) and (
vol.startswith(("/", "~", "./", "../")) or
(len(vol) >= 3 and vol[1] == ":" and vol[2] in ("/", "\\"))
)Modal 解析は三段階:direct(ユーザー自身の Modal クレデンシャル)、managed(Nous ポータル管理)、auto(デフォルト、managed 不可時に direct にフォールバック (fallback))。resolve_modal_backend_state が一箇所に集まり、バックエンドを選べなければ None を返す。has_direct_modal_credentials は MODAL_TOKEN_ID/MODAL_TOKEN_SECRET 環境変数と ~/.modal.toml ファイルの両方を見て、両方欠けて初めて direct 不可とする。managed_nous_tools_enabled は Nous Portal アカウントに問い合わせ、失敗時は fail closed で、問い合わせエラーで誤って managed パスを開けない。
Singularity は HPC シナリオの特殊な存在だ——ユーザーは Docker を入れられず、Apptainer/Singularity しか使えない。_find_singularity_executable はまず apptainer を探し、次に singularity を探し、両方なければインストール先を明示したエラーを返す。scratch ディレクトリは /scratch(HPC クラスタの標準マウント)を優先し、なければ get_sandbox_dir() / "singularity" に落ちる。コンテナは --containall --no-home + capability dropping で走らす——HPC 共有クラスタではこうしないと他のユーザーのホームディレクトリまで覗けてしまう。
境界と失敗
- Docker bind mount の誤判定:検出関数は
/、~、./、../で始まるものをホストパスとみなすが、named volume(mydata:/dataなど)は通してしまう。名前付きボリュームの背後に(カスタムドライバで)ホストパスがマウントされているとき、_docker_has_host_accessには見えず、誤って低リスクと判定する。 - Modal managed 失敗のフォールバック:
autoモードで managed 不可時に direct にフォールバックするが、ユーザーが managed だけ設定し direct クレデンシャルを設定していないと、selected_backendがNoneになり、バックエンド選択の失敗が実行時に炸裂する。ターミナル作成時点で明確なエラーが出るとは限らない。 - Singularity SIF キャッシュの膨張:SIF イメージは数百 MB から数 GB になる。scratch ディレクトリにローテーション方針がないとディスクを圧迫する。HPC ノードの
/scratchはクラスタレベルのクリーンアップ方針を持つことが多く、両者が揃わないと「クラスタがキャッシュを消したのに agent は SIF がまだあると思う」事態になる。 - 遠隔バックエンドのネットワークジッタ:SSH/Daytona のコマンド実行はネットワーク越しで、接続が切れると走行中の子プロセスの出力が失われる。出力回填は単一の同期戻りを前提とし、再接続時の再送がない。長タスクは遠隔バックエンドで最もリスクが高い。
- タイムアウトと OOM:local/docker には
script_timeoutがあるが、遠隔バックエンドのタイムアウトは遠隔プロセス自身が kill するのに頼る。ローカルスケジューラが応答を待てずに詰まるか、_run_job_script_with_claim_heartbeatのリース TTL で死んだと誤判定される。
まとめ
サンドボックス層は「実行隔離 + 危険コマンド管理」:6 バックエンドが本機から HPC、クラウドまでをカバー。Docker は bind mount を検出して権限昇格を防止、Modal は direct/managed のクレデンシャルを区別、危険コマンドは統一的に承認コールバックを経る。サブエージェントと cron は auto-deny/auto-approve の非対話パスを走る。