統一入口:一條命令按特徵分叉
模型只看見 exec_command 和 write_stdin。進門之後,tty、遠程環境和 150 毫秒窗口會把同一條命令送到 PTY、pipe、exec-server,或者送進重試門。
- 構造 command 加 cwd 請求mod.rs L12
- 編排器審批:bypass、緩存或彈窗mod.rs L14
- 按檔案選沙箱並 transformmod.rs L15
- 按 tty 和遠程環境分到 PTY、pipe 或 exec-serverspawn.rs L97
- 150 毫秒內退出則檢查沙箱拒絕process.rs L349
- 啓發式或執行器旗標成立則標成 Deniedprocess.rs L307
- 編排器過 escalate 與策略門orchestrator.rs L356
- UnlessTrusted 且已批准則不再問orchestrator.rs L411
- 允許 unsandboxed 則第二次用 Noneorchestrator.rs L459
- 活過窗口則入庫再 yieldprocess_manager.rs L535
- 遲到拒絕收成 Ok 回執exec_command.rs L384
- 用戶 Esc 只取消 turnprotocol.rs L546
模型要裝依賴,發出 npm install。同一輪裏又開 vim 改 README。若每種執行各寫一套審批和沙箱,Guardian、網絡代理和審批緩存會複製三遍,改一處漏兩處。
對外只有 exec_command 和 write_stdin。前者開進程,後者往已有進程寫,空寫就是一次 poll。內部跟蹤的是 process_id,模型側參數名叫 session_id。管理器只準備請求,審批、選沙箱、重試交給編排器。
出處:codex-rs/core/src/unified_exec/mod.rs 第 12 至 17 行
本地拉起按兩個開關三選一。tty 為真走 PTY。tty 為假且 stdin 開着,走帶 stdin 的 pipe。否則走不帶 stdin 的 pipe。遠程環境或帶 shell snapshot 的請求不走本地 spawn,它們走 exec-server。Windows 受限令牌是單獨一條後端。
出處:codex-rs/sandboxing/src/spawn.rs 第 97 至 127 行
預設工具調用常常是 pipe。模組註釋裏的拉起 PTY,只覆蓋 tty=true 那一支。要完整終端能力,模型必須顯式打開 tty。環境變數還會釘死 TERM=dumb、PAGER=cat 一批值,互動程式先被削一層。
政策邏輯集中,進程形態可以換。換語言重寫,仍是入口統一、拉起方式按特徵分。PTY 怎麼實現,可以留在自己的倉庫裏。
沙箱拒了寫 /etc/hosts,stderr 裏是 Operation not permitted。運行時若把這條當成命令寫錯,模型會改源碼、換路徑、加 sudo。認出來,它才有機會按策略再跑一次,或者把拒絕正文餵給模型去申請權限。
本地進程接住之後,退出通道裏已經有碼、通道關了、或者 150 毫秒內退出,才會做拒絕檢查。超過這個窗口,只掛一個後台任務等退出,把還活着的進程交回去。編排器重試依賴這裏返回 SandboxDenied。進程活過 150 毫秒,編排器已經拿到 Ok,後面再死就進不了第二次 spawn。
出處:codex-rs/core/src/unified_exec/process.rs 第 38 行 · 第 349 至 367 行
exec-server 路徑有同一段超時。檢查函式自己還先等 20 毫秒,讓輸出通知有機會到達。判定還有三道短路:進程還沒退出,直接放過;已經是 SandboxType::None 且執行器沒報拒絕,也放過。其餘情況才跑共享啓發式。
出處:codex-rs/core/src/unified_exec/process.rs 第 290 至 324 行
活過窗口的進程先入庫,再開始 yield。打斷 turn 時,不能因為最後一個 Arc 被丟掉而把後台進程殺掉。yield 等到進程已經退出,管理器會再跑一次拒絕檢查。這時編排器早已返回 Ok。這個錯誤直接回 handler,收成帶正文的工具回執,process_id 置空。
出處:codex-rs/core/src/unified_exec/process_manager.rs 第 535 至 556 行 · codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs 第 384 至 407 行
一次性命令可以等到結束再判定。持久進程必須有截止時間。漏掉截止,跑了幾秒才失敗的命令會被當成拒絕再裸跑。副作用已經寫到磁盤上,第二次是另一份進程,源碼裏沒有回滾。
模組頭寫:被拒之後按策略用 SandboxType::None 再試,並且靠緩存不再問一遍。Never、OnRequest、Guardian strict、帶 deny-read 的檔案,都會把不再問或無沙箱重試關掉。用戶按 Esc 若把 vim 一起帶走,下一輪找不到這個 session。
出處:codex-rs/core/src/unified_exec/mod.rs 第 7 至 8 行
編排器只認一種錯誤:SandboxErr::Denied。認出來之後過五道門。unified_exec 聲明自己會升級。Never 和 OnRequest 預設不要無沙箱重試,把帶原文的拒絕表面給調用方。檔案裏有 deny-read 時,繞過沙箱會把這些拒絕讀靜默放行,所以 unsandboxed 關掉。Guardian 的 strict auto-review 把第一次批准只覆蓋沙箱內嘗試。第二次允許 unsandboxed 時落到 None。
出處:codex-rs/core/src/tools/runtimes/unified_exec.rs 第 159 至 161 行 · codex-rs/core/src/tools/sandboxing.rs 第 330 至 337 行 · 第 269 至 278 行 · codex-rs/core/src/tools/orchestrator.rs 第 411 至 415 行 · 第 444 至 460 行
用戶在 TUI 裏按 Esc,協議入口是 Interrupt:中止當前任務,不殺後台 terminal。要殺全部後台,另有 CleanBackgroundTerminals。先入庫再 yield,turn 令牌被取消時,進程的 Arc 還在。pipe 會話收不到普通按鍵,只有 \u{3} 會走 interrupt。
出處:codex-rs/protocol/src/protocol.rs 第 546 至 552 行
用戶選中的隔離級別不能被運行時悄悄改掉。停思考和停終端是兩件事。取消只停等待,不殺已登記的進程。
DeepSeek Harness:六個終端工具,外加 jobs
DSH 把持久 PTY 做成獨立工具族:開、寫、讀、信號、關、列。後台發送複用 ctx.jobs,收集走 job_output,停止走 job_kill。系統提示寫明:只有需要跨調用的終端狀態或交互 stdin 時才用終端,一次性工作優先 shell 或讀寫工具。
Codex 把開和寫收成兩個工具,讀合併進下一次 write_stdin 或空 poll。DSH 沒有對位的沙箱拒絕後由編排器自動無沙箱重試。失敗之後誰負責再跑一次,兩邊答案不同。
Grok:會話啓動時選要不要持久
Grok 沒有 unified_exec 這個模組。它把持久性做成會話級後端選擇:複用父會話、ACP 客戶端終端、本地持久、本地非持久。選完後端就按這個形態跑整場。
Codex 把同一問題放在單次 exec_command:進程活過 yield,就發回 process_id。Grok 整場會話共享一種後端,子 agent 複用父後端。兩邊都承認一次性 bash -c 保不住 cwd 和交互狀態。落地位置不同。
出處:packages/terminal/tool-terminal/src/index.ts 第 156 至 160 行 · crates/codegen/xai-grok-shell/src/session/acp_session_impl/spawn.rs 第 2048 至 2070 行
哪一次會第二次 spawn
同一條 npm install,先走 UnlessTrusted,再撥到 Never。然後把命令換成 sleep 2。推演:哪一次會第二次 spawn,哪一次回執裏還有 process_id,為什麼燈滅之後編排器看不見拒絕。