学 AI 产品 · 专业 AI 产品经理播客第 3 章 · T3 解剖 OpenAI Codex:把安全观写进类型系统 · EP 14
第 3 章 · EP 14

exec 与 wait:跑不完的程序怎么收场 · 宿主拆分:程序挂在谁身上

时长 14:07音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 让模型写一段程序1:31
1:31给运行时做减法1:32
3:03跑不完不叫失败 · 叫还在跑1:22
4:26一秒宽限与两层不对称1:48
6:14宿主拆分 · V8 已经不在主进程1:18
7:33工具回调必须回家1:33
9:06掉线等于丢掉 isolate 和 store1:33
10:40横向对比1:49
12:30可带走的原则1:36
解读全文

ep26 · exec 与 wait · 宿主拆分:程序挂在谁身上

本集对应课程章节:OpenAI Codex · 代码模式(Code Mode 运行时与宿主拓扑)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。

一、本集解决什么工程问题

与其让模型发二十次工具调用,不如让它写一段 JavaScript。 但随之而来三个问题:

  1. 这段程序在哪跑、能干什么 —— 给模型的运行时该削到多薄?
  2. 十秒跑不完怎么办 —— 算失败,还是算「还在跑」?
  3. 挂点换了之后,故障域与状态归属怎么变?

一句话骨架:往返贵、批处理便宜;给不受信任的代码一个尽量小的环境,是安全设计的通用形状。

能力地图

维度读完本集你能做到对应证据
运行时减法复述工具说明书里「没有什么」的四条description.rs L20–25
让出协议说明 cell 与 wait 如何把「未结束」做成一等状态service.rs L77、cell_actor/mod.rs L242
宽限与不对称解释 1 秒宽限与说明书 / 协议两层参数差异service.rs L198–210
宿主拓扑说明默认已是本地子进程而非主进程内 isolatefeatures/src/lib.rs L104–111 / L921–925
回调回家说明远端只搬求值,审批仍回本机delegate.rs L26–50
掉线语义说明世代前缀与 store 归属generation.rs L49–67、tasks/mod.rs L888–899

二、给运行时做减法

动机:读五个文件再汇总 → 五次完整往返,每次把整个文件内容推入上下文,下一轮还要重读滚大的历史。贵的是往返节奏,不是工具本身。

做法:两个工具 exec / wait。exec 收一段 JavaScript 源码,扔进全新 V8 isolate 当 async module 求值;工具挂在全局 tools 对象上,名字规范化为合法 JS 标识符。

工具说明书原文要点(code-mode-protocol/src/description.rs L20–25):

条内容
1Runs raw JavaScript —— no Node, no file system, no network access, no console
2接受纯源码文本,不是 JSON、加引号字符串或 markdown 代码围栏
3首行可写 pragma:// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}
4yield_time_ms 默认 10000 ms;max_output_tokens 默认 10000 tokens
5脚本求值完毕 isolate 生命周期即结束,未 await 的 promise 被静默丢弃

关键推论:这些能力不是忘了加,是特意不给。程序要产生副作用只剩一条路 —— 走 tools,而那条路上审批与沙箱一样不少。

附带好处:要审查的面积变小。isolate 若能直接读文件,这层就得再做一套文件权限;现在它什么都做不了,权限判断留在下一层即可,代码不用写两遍。

提示1 · 减法优于监控

给不受信任的代码做减法,而不是加监控。换语言、换沙箱,该问的还是同一句:这段代码到底需要哪几样能力,其余的能不能一样都不给。


三、跑不完不叫失败 · 叫还在跑

两难:超时设三分钟 → 用户三分钟看不到动静;设十秒 → 长任务永远做不完,且前九秒的成果跟着一起丢。

问题根源:把「还没跑完」当成了失败。

做法:把正在跑的脚本做成有身份的东西 —— cell。

时刻行为
yield_time_ms 到点cell 不死,把攒下的输出整包交出,清空缓冲继续跑
exec 返回一句话:脚本还在跑,编号是多少
模型拿到编号三个选择:wait 续买时间 / terminate: true 停掉 / 先去干别的
wait 返回只返回上次让出之后的新输出(缓冲已清空,不重复占上下文)

出处:service.rs L77、cell_actor/mod.rs L242、code_mode/mod.rs L283、wait_handler.rs L24、runtime.rs L24

为什么长期成立:把「还没结束」做成一等状态,是长任务接口的通用形状 —— HTTP 202 + 轮询、任务队列 job id + poll、导出大文件先给编号。共同点是不让调用方在「一直等」和「当作失败」之间二选一。

放到 agent 上的额外价值:模型拿到中间输出后可以改主意,发现前几步结果不对直接 terminate,控制权回到会思考的那一方。

提示2 · 一秒宽限与两层不对称

  • 宽限:让出时间超过 10 秒时额外再送 1 秒才观察 —— 刚好卡边界完成的脚本不会因差几毫秒而多走一次往返(service.rs L198–210)
  • 不对称:说明书里 wait 有四个参数(cell 编号 / 让出时间 / 返回长度上限 / 是否终止),协议层请求结构体只带前两个;终止走另一条路径,长度上限是拿到结果后才截断。读源码时这两层极易混为一层。

四、宿主拆分 · V8 已经不在主进程

为什么拆出去:模型写一段 while (true) {},若与 Codex 事件循环抢同一进程,卡死会从单个 cell 扩大到整条会话。V8 堆、JIT、无条目上限的 store 表,任一在主进程爆炸都会带走 TUI 或 app-server。

出处:features/src/lib.rs L104–111(早期资料常写成「主进程里直接跑 isolate」,当前特性注释已改掉)

当前默认拓扑

条件提供方
CodeModeHost 打开,或 disable_in_process_fallback 为真ProcessOwnedCodeModeSessionProvider
两条都不成立DisabledCodeModeSessionProvider
——生产路径已无直接 new InProcessCodeModeSession

CodeModeHost 已 Stable 且默认打开;进程内求值仍存在,但沉到宿主进程内部,房东换成独立二进制 codex-code-mode-host。

出处:thread_manager.rs L455–462、features/src/lib.rs L921–925、code-mode-host/src/lib.rs L599–608

拉起细节:stdin / stdout / stderr 全管道化;Unix 上单独进程组;环境变量先 scrub。找不到可执行文件 → availability() 直接失败,不会改去主进程里 new isolate。真正的回退发生在工具模式层:宿主不可用且未关回退时,有效工具模式变 Direct,模型重新看见普通工具。


五、工具回调必须回家

常见误解:把 app-server 指到远端 --code-mode-host,以为整段 agent 都搬家了,连审批弹窗都该出现在远端。当前源码对不上。

机制:宿主把 CodeModeSessionDelegate 做成 RemoteDelegate,经 IPC 打回 Codex 主进程;主进程的 dispatch broker 才走嵌套工具。

isolate 搬家了,策略没有搬家。JS 在别处跑,副作用要绕回来问你。

出处:code-mode-host/src/delegate.rs L26–50

安全细节:WebSocket 监听器拒绝带 Origin 头的请求,挡住浏览器页面跨源连到本机宿主。app-server 的 --code-mode-host 认 http / https 为 gRPC,认 ws / wss 为 WebSocket。多个 thread 共享同一条远端连接,store 仍按会话切开。

为什么长期成立:不可信的是 JS 世界,可信的是审批和策略。把前者搬走、后者留在有 UI 的那边 —— 没有这条回路,远端宿主就必须复制整套权限系统。


六、掉线等于丢掉 isolate 和 store

两种常见预期,源码都不认

预期实际
按中断 → V8 一起掐掉,下一轮 wait 立刻看到终止turn 标 Interrupted 时取消令牌一定取消;是否再 terminate cell 取决于 CodeModeInterrupt(在开发中,默认关)。关掉时中断只取消本轮工具调用与审批,宿主上的 isolate 可继续跑。Ctrl-C ≠ terminate。出处:tasks/mod.rs L888–899
重连 → 刚才的脚本还在原 cell 里接着跑gRPC 丢 lease 即关会话;可再开租约,generation 从 1 往上加。第一代沿用原始 cell ID,第二代变 g{generation}:{cell_id};拿旧编号 wait 会收到 stale generation。本地子进程路径无前缀,只是状态机打回 New 并分配新 session-N。出处:grpc_session/generation.rs L49–67

两种重连都丢运行中的 cell 与那份 store。 store 表跟着宿主侧 SessionRuntime:无落盘、无跨进程共享、无 TTL;远端重启表就没了;会话 ID 复用会被拒绝。

重连恢复的是「还能再 exec」,不是「刚才那段脚本」。cell 寿命按会话算,不按 turn 算。

七、横向对比

项目拓扑超时状态
Codex默认本地子进程;宿主进程内才是 isolate。可再换远端让出续跑(cell + wait),「未结束」是一等状态store / load 同会话共享,跨会话隔离;无单条大小上限、无 TTL
DeepSeek Harness进程内 worker_threads.Worker。crate 头注释第一句写死立场:这是 containment,不是 security boundary;模型代码按 bash 等价对待到点终止 worker。一次 run_code 必须在预算内结束,超时即失败,没有续跑状态。默认六万 / 六十万毫秒程序世界随 worker 一起死,不做池化、不做跨运行状态;要传递就写进工具结果或落到工作区文件
Claude Code / Grok无对位实现。隔离出现在 git worktree、远端 CCR 会话、子 agent worktree —— 是工作区隔离,不是 JS 宿主拆分留给普通 shell 工具——

DSH 的代价与收益:实现简单,宿主不用维护活着的 cell;代价是一次运行必须预算内结束。Codex 反过来:模型要多学一个 wait 协议,cell 会占着会话资源。

没有对位物本身就是结论:Claude Code 与 Grok 把「跑模型写的代码」留给了普通 shell 工具。

八、审查清单

  • 给模型的运行时是否做了减法(无 fs / net / console),而非加监控
  • 副作用是否只能走 tools,且那条路上的审批与沙箱未被绕过
  • 输入是否为纯源码文本(非 JSON / 围栏)
  • 长任务是否返回编号让调用方续问,而非在「等」与「失败」间二选一
  • wait 是否只返回新增输出(缓冲已清空)
  • 让出预算是否理解两层差异:说明书四个参数 vs 协议层两个参数
  • 托管模型代码的 isolate 是否与主进程同命运(默认应拆出去)
  • 远端宿主是否只搬求值;审批 / execpolicy / Guardian 是否仍回本机
  • WebSocket 是否拒绝带 Origin 的请求
  • 中断语义是否明确:Ctrl-C 是否等于 terminate(Codex 默认不等)
  • store 是否有清理责任人与容量上限(Codex 无单条上限、无 TTL)

提示3 · 课堂练习:旧 cell 还能不能 wait

同一段脚本在 gRPC 宿主上跑到一半,连接断了又连上:

  • 模型拿着原来的 cell_id 去 wait,会看到什么?
  • 本地子进程路径会不会给这个编号改名?
  • 两种路径的 store 还在不在?
  • 进阶:把 CodeModeInterrupt 拨到关,按中断再 wait 一次 —— 答案会不会变?为什么用户按停止并不自动等于 terminate?

提示4 · 让出预算怎么算才不亏

一段程序要跑四十秒,让出预算默认十秒:

  • 模型一共要发几次 wait?
  • 每次拿到的是全部输出还是新增的那一段?
  • 为什么把默认值改成三十秒并不总是更划算?

九、约束说明

  1. 源码口径:依据本地仓库 openai/codex,commit 4f39251a01,核对日期 2026-08-22;代码块保留源码原文。
  2. 教学化内容:子任务数量、耗时与四种宿主切换为课程化设定,用于展示结构差异;逻辑轨迹行号对应真实源码。
  3. 横向对比口径:DSH 依据 code-runtime-worker-thread 与 README(2026-08-22);Claude Code 与 Grok 为检索未找到对位实现。
  4. 易混项:exec-server 搬走的是 shell、PTY 与文件系统 RPC,不跑 JavaScript;嵌套 tools.exec_command 可再走进去,属于下一层执行拆分,两条路不要收成同一个远端。

让模型写程序,把 N 次往返压成一次。给这段程序的运行时做减法,没有 Node、文件系统、网络和 console,副作用只能走工具那条已经有审批的路。预算到点先交作业再续跑,把「还没跑完」做成一等状态。默认拓扑里 V8 已经不在主进程;远端只搬走求值,审批仍回本机;宿主崩了或掉线,cell 和 store 一起没了;Ctrl-C 默认不 terminate 还在跑的 cell。


来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为二次演绎的解读与音频稿件版本,音频与本页配套发布。