沙箱 (sandbox) 後端
職責
agent 要執行命令、跑程式碼,但不能讓任意命令直接打到宿主機。Hermes 的終端工具支援 6 種執行後端,按安全/隔離/成本權衡選擇:Local(本機直跑)、Docker(容器隔離)、SSH(遠端機器)、Singularity(HPC 容器)、Modal(雲沙箱,direct 或 managed)、Daytona(雲開發環境)。容器硬化 + 命名空間隔離,危險命令還經審批回調攔截。
設計動機
為什麼是 6 種而不是 1 種?agent 的執行場景跨度太大——開發時只想在本機跑得快,生產跑批需要容器隔離,HPC 使用者必須在 Singularity 裡跑(沒法用 Docker),雲端無人值守要 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 六後端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(網關 (gateway) 託管)singularity:經tools/environments/singularity.py準備 scratch 與 SIFssh/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 不可時 fallback 到 direct)。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)會被放行。命名卷背後掛宿主路徑(自訂 driver)時,_docker_has_host_access看不到,會誤判為低風險。 - Modal managed 失敗的回退:
auto模式下 managed 不可用 fallback 到 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 的非互動路徑。