OpenAI Codex · 统一执行

统一入口:一条命令按特征分叉

模型只看见 exec_commandwrite_stdin。进门之后,tty、远程环境和 150 毫秒窗口会把同一条命令送到 PTY、pipe、exec-server,或者送进重试门。

课程目标读完能说清两件事。一条命令进统一入口之后,按 tty、远程环境和是否还活着,走到 PTY、pipe 或 exec-server。沙箱拒绝只有在进程很快退出时,才可能按策略再跑一次。
先玩一遍 · 进门之后往哪条路走
同一条命令:先分拉起路径,再看 150 毫秒窗口
命令
早死拒绝、PTY 会话、晚死拒绝、远程后端,四条路的分叉点不一样。
策略
这两项只影响早死之后的重试门。活过窗口的命令到不了这里。
统一入口 · exec_command等待命令
PTYtty 为真
pipe默认本地路径
exec-server远程或 snapshot
150 毫秒窗口灯还没亮
升级escalate
策略能否裸跑
deny-read能否卸沙箱
再问人批准还算不算
第二次沙箱类型
等待开始。
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. 构造 command 加 cwd 请求mod.rs L12
  2. 编排器审批:bypass、缓存或弹窗mod.rs L14
  3. 按档案选沙箱并 transformmod.rs L15
  4. 按 tty 和远程环境分到 PTY、pipe 或 exec-serverspawn.rs L97
  5. 150 毫秒内退出则检查沙箱拒绝process.rs L349
  6. 启发式或执行器旗标成立则标成 Deniedprocess.rs L307
  7. 编排器过 escalate 与策略门orchestrator.rs L356
  8. UnlessTrusted 且已批准则不再问orchestrator.rs L411
  9. 允许 unsandboxed 则第二次用 Noneorchestrator.rs L459
  10. 活过窗口则入库再 yieldprocess_manager.rs L535
  11. 迟到拒绝收成 Ok 回执exec_command.rs L384
  12. 用户 Esc 只取消 turnprotocol.rs L546
点播放,看同一条命令进门之后怎么分叉。
第一道分叉本地按 tty 走 PTY 或 pipe,远程走 exec-server。模型看见的仍是同一个工具。
第二道分叉150 毫秒内退出,编排器才可能看见沙箱拒绝。活过窗口就入库,后面再坏也没有第二次 spawn。
你能改的边界把策略拨到 Never,或打开 deny-read,主路上的无沙箱重试会被关掉。
教学示意:退出耗时与拒绝文案为课程化设定,用来展示分叉条件。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 两个工具,三种拉起方式
它解决什么问题

模型要装依赖,发出 npm install。同一轮里又开 vim 改 README。若每种执行各写一套审批和沙箱,Guardian、网络代理和审批缓存会复制三遍,改一处漏两处。

思路是什么

对外只有 exec_commandwrite_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=dumbPAGER=cat 一批值,交互程序先被削一层。

exec_command 统一入口 tty 为真 PTY 本地默认 pipe 远程或 snapshot 可写 stdin,模型能送键 普通字符会撞上 stdin 已关 exec-server 后端
教学化结构图:同一入口,按特征落到三种拉起方式。
为什么长期成立

政策逻辑集中,进程形态可以换。换语言重写,仍是入口统一、拉起方式按特征分。PTY 怎么实现,可以留在自己的仓库里。

思路二 · 台灯只亮 150 毫秒
它解决什么问题

沙箱拒了写 /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 行

刚拉起 150 毫秒内退出 检查拒绝,编排器看得见 活过窗口 先入库,编排器已是 Ok 过门则第二次 spawn 门关则把拒绝正文交给模型 回执仍是 Ok 没有第二次 spawn
教学化结构图:灯亮才可能重试,灯灭就入库。
为什么长期成立

一次性命令可以等到结束再判定。持久进程必须有截止时间。漏掉截止,跑了几秒才失败的命令会被当成拒绝再裸跑。副作用已经写到磁盘上,第二次是另一份进程,源码里没有回滚。

思路三 · 重试有门,Esc 不杀进程
它解决什么问题

模块头写:被拒之后按策略用 SandboxType::None 再试,并且靠缓存不再问一遍。NeverOnRequest、Guardian strict、带 deny-read 的档案,都会把不再问或无沙箱重试关掉。用户按 Esc 若把 vim 一起带走,下一轮找不到这个 session。

出处:codex-rs/core/src/unified_exec/mod.rs 第 7 至 8 行

思路是什么

编排器只认一种错误:SandboxErr::Denied。认出来之后过五道门。unified_exec 声明自己会升级。NeverOnRequest 默认不要无沙箱重试,把带原文的拒绝表面给调用方。档案里有 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 行

灯亮才可能重试,灯灭就入库。Esc 只停思考。
为什么长期成立

用户选中的隔离级别不能被运行时悄悄改掉。停思考和停终端是两件事。取消只停等待,不杀已登记的进程。

横向对比 · 持久终端放在哪一层

DeepSeek Harness:六个终端工具,外加 jobs

DSH 把持久 PTY 做成独立工具族:开、写、读、信号、关、列。后台发送复用 ctx.jobs,收集走 job_output,停止走 job_kill。系统提示写明:只有需要跨调用的终端状态或交互 stdin 时才用终端,一次性工作优先 shell 或读写工具。

Codex 把开和写收成两个工具,读合并进下一次 write_stdin 或空 poll。DSH 没有对位的沙箱拒绝后由编排器自动无沙箱重试。失败之后谁负责再跑一次,两边答案不同。

两侧均已核对源码 · 2026-08-22 · DSH · 终端会话

Grok:会话启动时选要不要持久

Grok 没有 unified_exec 这个模块。它把持久性做成会话级后端选择:复用父会话、ACP 客户端终端、本地持久、本地非持久。选完后端就按这个形态跑整场。

Codex 把同一问题放在单次 exec_command:进程活过 yield,就发回 process_id。Grok 整场会话共享一种后端,子 agent 复用父后端。两边都承认一次性 bash -c 保不住 cwd 和交互状态。落地位置不同。

两侧均已核对源码 · 2026-08-22

出处:packages/terminal/tool-terminal/src/index.ts 第 156 至 160 行 · crates/codegen/xai-grok-shell/src/session/acp_session_impl/spawn.rs 第 2048 至 2070 行

课堂练习
01

哪一次会第二次 spawn

同一条 npm install,先走 UnlessTrusted,再拨到 Never。然后把命令换成 sleep 2。推演:哪一次会第二次 spawn,哪一次回执里还有 process_id,为什么灯灭之后编排器看不见拒绝。

Takeaway:一条命令进统一入口,按 tty 和远程环境分到 PTY、pipe 或 exec-server。沙箱拒绝只有在 150 毫秒窗口内被认出,才可能按策略再跑。活过窗口的先入库。Esc 只停 turn。