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