统一入口:一条命令按特征分叉
模型只看见 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,为什么灯灭之后编排器看不见拒绝。