统一入口:一条命令按特征分叉
模型只看见 exec_command 和 write_stdin。进门之后,tty、远程环境和 150 毫秒窗口会把同一条命令送到 PTY、pipe、exec-server,或者送进重试门。
本页解决的问题
先给结论「统一入口:一条命令按特征分叉」要解决的关键问题是什么?
模型只看见 exec_command 和 write_stdin。进门之后,tty、远程环境和 150 毫秒窗口会把同一条命令送到 PTY、pipe、exec-server,或者送进重试门。
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
- 构造 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,为什么灯灭之后编排器看不见拒绝。
「先玩一遍 · 进门之后往哪条路走」的能力藏在每次交接里
「模型要装依赖,发出 npm install 。同一轮里又开 vim 改 README。若每种执行各写一套审批和沙箱,Guardian、网络代理和审批缓存会复制三遍,改一处漏两处」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「对外只有 exec_command 和 write_stdin 。前者开进程,后者往已有进程写,空写就是一次 poll。内部跟踪的是 process_id ,模型侧参数名叫 session_id 。管理器只准备请求,审批、选沙箱、重试交给编排器」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 构造 command 加 cwd 请求 mod.rs L12
- 编排器审批:bypass、缓存或弹窗 mod.rs L14
- 按档案选沙箱并 transform mod.rs L15
成功路径不能代表系统可靠
用「同一条 npm install ,先走 UnlessTrusted ,再拨到 Never 。然后把命令换成 sleep 2 。推演:哪一次会第二次 spawn,哪一次回执里还有 process_id ,为什么灯灭之后编排器看不见拒绝」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 进门之后往哪条路走」走到「思路一 · 两个工具,三种拉起方式」
「先玩一遍 · 进门之后往哪条路走」先把问题落在「同一条命令:先分拉起路径,再看 150 毫秒窗口 播放 单步 重置 命令 npm install vim sleep 2 远程环境 早死拒绝、PTY 会话、晚死拒绝、远程后端,四条路的分叉点不一样。 策略 UnlessTrusted Never 无 deny-read 有 deny-read 这两项只影响早死之后的重试门。活过窗口的命令到不了这里。 统一入口 · exec_command 等待命令 PTY tty 为…」上;到了「思路一 · 两个工具,三种拉起方式」,讨论继续推进到「模型要装依赖,发出 npm install 。同一轮里又开 vim 改 README。若每种执行各写一套审批和沙箱,Guardian、网络代理和审批缓存会复制三遍,改一处漏两处」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 进门之后往哪条路走」:同一条命令:先分拉起路径,再看 150 毫秒窗口 播放 单步 重置 命令 npm install vim sleep 2 远程环境 早死拒绝、PTY 会话、晚死拒绝、远程后端,四条路的分叉点不一样。 策略 UnlessTrusted Never 无 deny-read 有 deny-read 这两项只影响早死之后的重试门。活过窗口的命令到不了这里。 统一入口 · exec_command 等待命令 PTY tty 为…
- 「思路一 · 两个工具,三种拉起方式」:模型要装依赖,发出 npm install 。同一轮里又开 vim 改 README。若每种执行各写一套审批和沙箱,Guardian、网络代理和审批缓存会复制三遍,改一处漏两处
- 「最后的要点」:150 毫秒内退出则检查沙箱拒绝 process.rs L349
最后的「最后的要点」把讨论落到「150 毫秒内退出则检查沙箱拒绝 process.rs L349」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。