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

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

时长 13:55音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 两个工具背后藏着三条路1:39
1:39第一道分叉 · 按 tty 和远程环境选拉起方式1:48
3:27第二道分叉 · 台灯只亮一百五十毫秒1:29
4:57三道短路与二十毫秒等待1:40
6:38活过窗口的进程 · 先入库再交回去1:14
7:52重试有五道门2:03
9:55Esc 只停思考1:24
11:20横向对比与可带走的原则2:35
解读全文

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

本集对应课程章节: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-serverspawn.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 与 CleanBackgroundTerminalsprotocol.rs L546–552

本集的一句话骨架:模型只看见 exec_command 与 write_stdin;进门之后,tty、远程环境和 150 毫秒窗口会把同一条命令送到 PTY、pipe、exec-server,或者送进重试门。

阅读顺序建议:先按「拉起路径 → 拒绝窗口 → 重试门 → 中断语义」这条时间线走一遍,再回头看横向对比。两条分叉加起来,就是调用一次 exec_command 时真正发生的事。

二、第一道分叉 · 按 tty 与远程环境选拉起方式

本地拉起:两个开关三选一

条件走哪条路
tty = truePTY
tty = false 且 stdin 开着带 stdin 的 pipe
其余不带 stdin 的 pipe

远程 / snapshot:远程环境或带 shell snapshot 的请求不走本地 spawn,走 exec-server。Windows 受限令牌是单独一条后端。

出处:codex-rs/sandboxing/src/spawn.rs L97–127

两个容易踩空的点

  1. 模块注释里写的「拉起 PTY」只覆盖 tty=true 那一支。默认工具调用常常是 pipe —— 要完整终端能力,模型必须显式打开 tty。
  2. 环境变量会钉死一批值:TERM=dumb、PAGER=cat 等,交互程序先被削一层。这是刻意的降能力,让默认路径尽量可预测。

为什么长期成立:政策逻辑集中在入口,进程形态可以换。换语言重写,仍是「入口统一 + 拉起按特征分」。

提示1 · 判断一份执行改动走哪条路

先问三个问题:远程吗?带 shell snapshot 吗?tty 打开了吗? 三个答案决定拉起方式。若目标是交互程序(vim、top),必须显式 tty=true,否则拿到的只是被削过能力的 pipe。


三、第二道分叉 · 台灯只亮 150 毫秒

问题背景:沙箱拒了写 /etc/hosts,stderr 是 Operation not permitted。运行时若把它当成「命令写错」,模型会去改源码、换路径、加 sudo。只有认出这是拒绝,才有机会按策略再跑一次,或把拒绝正文喂给模型去申请权限。

判定条件(本地进程) —— 满足其一才做拒绝检查:

  • 退出通道里已经有码
  • 通道已关闭
  • 150 毫秒内退出

超过窗口 → 只挂后台任务等退出,把还活着的进程交回去。

关键后果:编排器重试依赖这里返回 SandboxDenied。进程活过 150 ms,编排器已拿到 Ok,后面再死也进不了第二次 spawn。

出处:codex-rs/core/src/unified_exec/process.rs L38、L349–367

为什么必须有窗口:一次性命令可以等结束再判定;持久进程必须有截止时间。漏掉截止 → 跑了几秒才失败的命令被当成拒绝再裸跑,而副作用已落盘,第二次是另一份进程,源码里没有回滚。


四、三道短路与 20 毫秒等待

exec-server 路径有同一段超时,但检查函数自己先等 20 毫秒,让输出通知有机会到达。

三道短路

序条件结果
1进程还没退出直接放过
2已是 SandboxType::None 且执行器未报拒绝直接放过
3其余跑共享启发式

出处:process.rs L290–324

启发式的性质:靠退出码与 stderr 文本推断是否为沙箱拒绝,不是内核给的确定信号。所以窗口的设计目标不是精确,而是给编排器一个足够早、足够便宜的判断。

提示2 · 迟到拒绝的正确预期

若一条命令跑了几秒才被沙箱拒,不要指望自动重试。它会被收成一条带正文的工具回执,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
2Never / OnRequest 默认不要无沙箱重试,把带原文的拒绝表面给调用方unified_exec.rs L159–161
3档案含 deny-read 时,绕过沙箱会把拒绝读静默放行 → 关掉 unsandboxedsandboxing.rs L330–337
4Guardian strict auto-review:第一次批准只覆盖沙箱内尝试sandboxing.rs L269–278
5第二次允许 unsandboxed 时落到 None;UnlessTrusted 且已批准则不再问orchestrator.rs L411–415 / L444–460

你能改的边界:把策略拨到 Never,或打开 deny-read,主路上的无沙箱重试即被关掉。用户选中的隔离级别不能被运行时悄悄改掉。


七、Esc 只停思考

协议入口是 Interrupt:中止当前任务,不杀后台 terminal。要杀全部后台另有 CleanBackgroundTerminals。

配合「先入库再 yield」,turn 令牌被取消时进程 Arc 仍在管理器中,终端还在。

交互差异:pipe 会话收不到普通按键,只有控制字符 \u{3} 会走 interrupt。

为什么长期成立:停思考、停等待、停进程是三种语义。取消只应停掉等待 —— 否则用户按一下 Esc,正在跑的 vim 就没了,下一轮找不到这个 session。

提示3 · 中断语义自查

实现取消功能时,先分清:停的是模型推理、等待,还是已登记的进程。三者必须分开,否则一次误操作会带走用户还在用的会话。


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

项目放法特点
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


九、审查清单

执行层改动合并前逐项自查:

  • 是否复用了统一入口,而非新增一套审批 / 沙箱路径
  • 拉起方式判断是否正确覆盖:远程 / shell snapshot / tty
  • 是否需要显式 tty=true(交互程序)
  • 环境变量钉值(TERM=dumb、PAGER=cat)是否被意外覆盖
  • 拒绝判定是否在 150 ms 窗口内完成;超时路径是否入库并交回 process_id
  • 迟到拒绝是否被收成带正文的回执,而非误触发第二次 spawn
  • 重试是否走完五道门;Never / deny-read 下是否关闭无沙箱重试
  • 中断语义是否区分 Interrupt 与 CleanBackgroundTerminals
  • pipe 会话的控制字符 \u{3} 路径是否仍然有效
  • 是否存在回滚:第二次 spawn 前,副作用是否已落盘且不可撤销

提示4 · 课堂练习:哪一次会第二次 spawn

同一条 npm install:先走 UnlessTrusted,再把策略拨到 Never,然后把命令换成 sleep 2。推演:

  • 哪一次会第二次 spawn?
  • 哪一次回执里还有 process_id?
  • 为什么灯灭之后编排器看不见拒绝?

十、约束说明

  1. 源码口径:依据本地仓库 openai/codex,commit 4f39251a01,核对日期 2026-08-22。
  2. 教学化内容:退出耗时与拒绝文案为课程化设定,用于展示分叉条件;逻辑轨迹右侧行号对应真实源码。
  3. 横向对比口径:DSH 依据其终端会话与 AGENTS.md(2026-08-22);Grok 依据 tool-terminal 与 acp_session_impl/spawn.rs(2026-08-22)。
  4. 启发式边界:沙箱拒绝判定基于退出码与 stderr 文本推断,不是确定性信号,存在误判与漏判空间。

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


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