本集对应课程章节:OpenAI Codex · 统一执行(unified_exec)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。
模型要装依赖(发一次 npm install),同一轮里又要开 vim 改文档。若每种执行各写一套审批和沙箱,Guardian、网络代理和审批缓存会复制三遍,改一处漏两处。
Codex 的答法:入口收成两个工具,差异藏进门之后。
exec_command(开进程)与 write_stdin(往已有进程写;空写即一次 poll)process_id,模型侧参数名为 session_id出处:codex-rs/core/src/unified_exec/mod.rs L12–17
| 维度 | 读完本集你能做到 | 对应证据 |
|---|---|---|
| 入口收敛 | 说清为何只暴露两个工具 | mod.rs L12–17 |
| 拉起分叉 | 按 tty / 远程环境判断走 PTY、pipe 还是 exec-server | spawn.rs L97–127 |
| 默认路径 | 解释默认工具调用为何常常是 pipe,以及 TERM=dumb 的用意 | 模块注释 + 环境变量钉值 |
| 拒绝窗口 | 说明 150 ms 窗口为何存在,超时后会发生什么 | process.rs L38、L349–367 |
| 短路与等待 | 复述三道短路与 20 ms 等待的作用 | process.rs L290–324 |
| 迟到拒绝 | 说明活过窗口的进程如何先入库、拒绝如何收成回执 | process_manager.rs L535–556、exec_command.rs L384–407 |
| 重试五道门 | 逐条列出 Denied 之后要过的门 | orchestrator.rs L411–415 / L444–460 等 |
| 中断语义 | 区分 Interrupt 与 CleanBackgroundTerminals | protocol.rs L546–552 |
本集的一句话骨架:模型只看见 exec_command 与 write_stdin;进门之后,tty、远程环境和 150 毫秒窗口会把同一条命令送到 PTY、pipe、exec-server,或者送进重试门。
阅读顺序建议:先按「拉起路径 → 拒绝窗口 → 重试门 → 中断语义」这条时间线走一遍,再回头看横向对比。两条分叉加起来,就是调用一次 exec_command 时真正发生的事。
本地拉起:两个开关三选一
| 条件 | 走哪条路 |
|---|---|
tty = true | PTY |
tty = false 且 stdin 开着 | 带 stdin 的 pipe |
| 其余 | 不带 stdin 的 pipe |
远程 / snapshot:远程环境或带 shell snapshot 的请求不走本地 spawn,走 exec-server。Windows 受限令牌是单独一条后端。
出处:codex-rs/sandboxing/src/spawn.rs L97–127
两个容易踩空的点
tty=true 那一支。默认工具调用常常是 pipe —— 要完整终端能力,模型必须显式打开 tty。TERM=dumb、PAGER=cat 等,交互程序先被削一层。这是刻意的降能力,让默认路径尽量可预测。为什么长期成立:政策逻辑集中在入口,进程形态可以换。换语言重写,仍是「入口统一 + 拉起按特征分」。
先问三个问题:远程吗?带 shell snapshot 吗?tty 打开了吗? 三个答案决定拉起方式。若目标是交互程序(vim、top),必须显式 tty=true,否则拿到的只是被削过能力的 pipe。
问题背景:沙箱拒了写 /etc/hosts,stderr 是 Operation not permitted。运行时若把它当成「命令写错」,模型会去改源码、换路径、加 sudo。只有认出这是拒绝,才有机会按策略再跑一次,或把拒绝正文喂给模型去申请权限。
判定条件(本地进程) —— 满足其一才做拒绝检查:
超过窗口 → 只挂后台任务等退出,把还活着的进程交回去。
关键后果:编排器重试依赖这里返回 SandboxDenied。进程活过 150 ms,编排器已拿到 Ok,后面再死也进不了第二次 spawn。
出处:codex-rs/core/src/unified_exec/process.rs L38、L349–367
为什么必须有窗口:一次性命令可以等结束再判定;持久进程必须有截止时间。漏掉截止 → 跑了几秒才失败的命令被当成拒绝再裸跑,而副作用已落盘,第二次是另一份进程,源码里没有回滚。
exec-server 路径有同一段超时,但检查函数自己先等 20 毫秒,让输出通知有机会到达。
三道短路
| 序 | 条件 | 结果 |
|---|---|---|
| 1 | 进程还没退出 | 直接放过 |
| 2 | 已是 SandboxType::None 且执行器未报拒绝 | 直接放过 |
| 3 | 其余 | 跑共享启发式 |
出处:process.rs L290–324
启发式的性质:靠退出码与 stderr 文本推断是否为沙箱拒绝,不是内核给的确定信号。所以窗口的设计目标不是精确,而是给编排器一个足够早、足够便宜的判断。
若一条命令跑了几秒才被沙箱拒,不要指望自动重试。它会被收成一条带正文的工具回执,process_id 置空。模型仍能读到错误信息,但不再享受第二次 spawn。
顺序是先入库,再 yield —— 打断 turn 时,不能因为最后一个 Arc 被丢掉而把后台进程杀掉。
若 yield 一直等到进程退出,管理器会再跑一次拒绝检查;但编排器早已返回 Ok,该错误不再触发第二次 spawn,而是直接回 handler,收成带正文的工具回执,process_id 置空。
出处:process_manager.rs L535–556、tools/handlers/unified_exec/exec_command.rs L384–407
完整时间线
| 情形 | 走向 |
|---|---|
| 150 ms 内退出且被认出 | 走重试门 |
| 活过窗口 | 入库并交回 process_id |
| 之后才退出 | 拒绝收成回执,不再重试 |
编排器只认一种错误:SandboxErr::Denied。认出之后还要过五道门:
| 门 | 内容 | 出处 |
|---|---|---|
| 1 | 模块声明自己会升级:被拒后按策略用 SandboxType::None 再试,靠缓存不再问一遍 | mod.rs L7–8 |
| 2 | Never / OnRequest 默认不要无沙箱重试,把带原文的拒绝表面给调用方 | unified_exec.rs L159–161 |
| 3 | 档案含 deny-read 时,绕过沙箱会把拒绝读静默放行 → 关掉 unsandboxed | sandboxing.rs L330–337 |
| 4 | Guardian strict auto-review:第一次批准只覆盖沙箱内尝试 | sandboxing.rs L269–278 |
| 5 | 第二次允许 unsandboxed 时落到 None;UnlessTrusted 且已批准则不再问 | orchestrator.rs L411–415 / L444–460 |
你能改的边界:把策略拨到 Never,或打开 deny-read,主路上的无沙箱重试即被关掉。用户选中的隔离级别不能被运行时悄悄改掉。
协议入口是 Interrupt:中止当前任务,不杀后台 terminal。要杀全部后台另有 CleanBackgroundTerminals。
配合「先入库再 yield」,turn 令牌被取消时进程 Arc 仍在管理器中,终端还在。
交互差异:pipe 会话收不到普通按键,只有控制字符 \u{3} 会走 interrupt。
为什么长期成立:停思考、停等待、停进程是三种语义。取消只应停掉等待 —— 否则用户按一下 Esc,正在跑的 vim 就没了,下一轮找不到这个 session。
实现取消功能时,先分清:停的是模型推理、等待,还是已登记的进程。三者必须分开,否则一次误操作会带走用户还在用的会话。
| 项目 | 放法 | 特点 |
|---|---|---|
| DeepSeek Harness | 独立工具族:六个终端工具 + jobs(开、写、读、信号、关、列) | 后台发送复用 ctx.jobs,收集走 job_output,停止走 job_kill。系统提示写明:只有需要跨调用保留终端状态或交互 stdin 时才用终端。没有对位的「沙箱拒绝后由编排器自动无沙箱重试」 |
| Codex | 开与写收成两个工具,读合并进下一次 write_stdin 或空 poll | 持久性落在单次 exec_command:进程活过 yield 就发回 process_id |
| Grok | 会话级后端选择 | 复用父会话、ACP 客户端终端、本地持久、本地非持久;选完后端跑整场,子 agent 复用父后端。无 unified_exec 模块 |
两边都承认:一次性 bash -c 保不住 cwd 与交互状态,只是落地位置不同。
出处:packages/terminal/tool-terminal/src/index.ts L156–160、crates/codegen/xai-grok-shell/src/session/acp_session_impl/spawn.rs L2048–2070
执行层改动合并前逐项自查:
ttytty=true(交互程序)TERM=dumb、PAGER=cat)是否被意外覆盖process_idNever / deny-read 下是否关闭无沙箱重试\u{3} 路径是否仍然有效同一条 npm install:先走 UnlessTrusted,再把策略拨到 Never,然后把命令换成 sleep 2。推演:
process_id?openai/codex,commit 4f39251a01,核对日期 2026-08-22。AGENTS.md(2026-08-22);Grok 依据 tool-terminal 与 acp_session_impl/spawn.rs(2026-08-22)。一条命令进统一入口,按 tty 和远程环境分到 PTY、pipe 或 exec-server。沙箱拒绝只有在 150 毫秒窗口内被认出,才可能按策略再跑。活过窗口的先入库。Esc 只停 turn。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为二次演绎的解读与音频稿件版本,音频与本页配套发布。