模块:T3 解剖 OpenAI Codex:把安全观写进类型系统
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为《学 AI 产品,从入门到精通》配套解读,音频为二次演绎配音版。
命令走进程内的 Submission Queue。事件走能写成 JSON 的 Event Queue。Rust 名叫 TurnStarted,磁盘上仍写 task_started。
本集主题:同一件事在"进"与"出"两侧各自长什么样,以及碰和不认识的 type 时该如何选择默认方向。
一个真实的三次失败:
type === "turn_started",联调那天字段对得上、type 却是 task_started;改成新名后旧夹具里的旧名还能解出来。parse_errors 加一,会话能开但少了一段生命周期。同一件事在三个地方三种失败方式 → 设计时没有把默认方向写下来。
```
//! Defines the protocol for a Codex session between a client and an agent.
//! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate
//! between user and agent.
```
出处:codex-rs/protocol/src/protocol.rs第 1–4 行 · 源码快照:openai/codex @4f39251a01· 核对日期 2026-08-22
四行注释写在模块头,不是写在外部文档里。把说话方式写死在代码最前面,是本讲最先能拿走的一条。
| 能力 | 落点 | 判断标准 |
|---|---|---|
| 区分命令通道与事件通道 | Submission / Event 两个类型 | 能说清为何前者无 serde、后者必须有 |
| 掌握两条通道的容量策略 | SQ bounded 512 / EQ unbounded | 能解释为何命令可反压、事件不可 |
| 用同一根 id 对账 | sub_id → Event.id | 能追溯一条事件对应哪次提交 |
| wire 名与代码名分离 | #[serde(rename)] + alias | 能说清改名正确顺序 |
| 处理未知 type 的三条边界 | 编译期 / JSON / resume | 能复述三条路径各自的默认方向 |
| 落盘白名单 | rollout 写入判定 | 能说明瞬时事件为何不写真源 |
Submission:关联用 id + 要执行的 Op(内核动词,当前 28 个),只派生 Debug,没有 serde。codex-rs/protocol/src/protocol.rs 第 185–200 行Event:有 serde;id 对上当初那条提交,msg 是事件本体。codex-rs/protocol/src/protocol.rs 第 1276–1283 行| 通道 | 容量 | 策略 |
|---|---|---|
| SQ(下行) | bounded 512 | 客户端连打 512 条未被 loop 收走 → 下一次 send 等待(反压) |
| EQ(上行) | unbounded | 事件可堆积、占内存,不反压这一轮 |
codex-rs/core/src/session/mod.rs 第 460–461、533–534 行session/mod.rs L918)submission_loop 按变体分发(handlers.rs L526)send_event 用 sub_id 做 Event.id(session/mod.rs L1952)TurnInput的路由结果走 oneshot,不走 Event Queue。EventMsg描述这一轮发生了什么;oneshot 只回答"这条提交有没有被接住"。出处:codex-rs/core/src/session/handlers.rs第 515–526 行
TurnStarted;serde 写出 task_started,读入时同时认 turn_started;Display 与指标走 turn_started。codex-rs/protocol/src/protocol.rs 第 1337–1340 行rename 把旧字符串钉死在序列化层,再谈迁移完成。顺序反过来(先改字符串)→ 已落盘旧文件再也读不回来。ItemStarted,旧前端看 ExecCommandBegin 或 AgentMessage → 队列上出现重复语义条目。codex-rs/core/src/session/mod.rs 第 1965–1973 行;codex-rs/protocol/src/legacy_events.rs 第 65–69 行TurnItem 的消费者留的;迁完后副本才会退场。两条提醒:
EventMsg 有 81 个变体,没有 #[serde(other)],也没标 non_exhaustive。三条路径的答案全部写在代码里:
| 路径 | 行为 | 出处 |
|---|---|---|
| 同进程、同版本 | 穷尽 match 编译不过;旧客户端不会与这份新内核链在一起 | — |
| 跨版本 JSON | MCP 把整个 Event 序列化成 codex/event;旧词表解未知 type → serde 失败,失败发生在客户端 | mcp-server/src/outgoing_message.rs 第 108–133 行 |
| resume 旧文件 | 坏行 parse_errors += 1 后 continue;未知 type 不会让会话打不开,只少一行,函数仍返回已解出的 items | rollout/src/recorder.rs 第 1009–1071 行 |
梯度规律:越靠近开发者态度越强硬(编译期编不过),越靠近用户数据态度越宽容(resume 跳行),中间的网络边界最无奈(内核已发出,只能让对方失败)。梯度对应三件事的成本——开发者改代码最便宜,用户数据丢了就没了,网络对面控制不了。
Op 正好反过来:标了 non_exhaustive,submission_loop 末尾 _ => false,未知命令被丢掉、loop 不崩。
出处:codex-rs/core/src/session/handlers.rs 第 684 行
为什么相反:事件是对外词表,漏一个变体要在编译期被看见;命令面向内部扩展,丢掉比崩掉更安全。
| 方案 | 定位 | 默认方向 | 代价 / 收益 |
|---|---|---|---|
| DSH | 事件日志是唯一真源 | 信封缺 ignorable?: true 时,读取器碰未知 type 必须拒绝重建 | 旧 harness 打不开新日志;换来"能打开就完整"。过分拒绝比静默恢复被掏空的会话更安全 |
| Grok | 事件是通知流 | Unknown 带 #[serde(other)],解不出就收成 Unknown,消费者必须静默忽略;原始类型名不保留 | 只有 6 个变体;通知丢了会话还能靠别的状态活 |
| Codex | 落盘文件既要当真源,又要保证老文件能开 | resume 跳行(比 Grok 更接近"打开",比 DSH 更接近"尽量打开") | 取中间位置 |
packages/core/session/src/types.ts 第 404–422 行 · 核对 2026-08-22crates/common/xai-tool-protocol/src/session_event.rs 第 11–65 行 · 核对 2026-08-22追到底都是定位问题:你先回答这份日志是什么,默认值自然就出来了。
serde(other)?没有时,三条边界的行为是否都写进文档?_ => false 兜底?不要只在设计文档里描述通信模型。把"用哪两条队列、谁有界谁无界、谁需要序列化"写成模块头注释(四行即可),让每个打开文件的人第一眼看到。
有界 512 与无界不是随手填的数字。在注释里写明"命令低频可反压 / 事件高频不可反压",后来的人才不会为了对称去给上行也加个上限。
新建 / 重命名事件重体时,第一步改 Rust 标识符并加 #[serde(rename = "<旧 wire 名>")] 与 alias;第二步等所有消费者就绪,再讨论是否退役旧字符串。绝不能先改 wire 名。
在事件定义旁或一张白名单表里标注每条事件是否落盘。判据是"恢复会话时是否需要它"。命令输出、审批提示、流式增量这类瞬时事件不写真源,否则 JSONL 会按 token 涨。
用三行 JSON(task_started / turn_started / future_event)做夹具,分别断言:前两行在 Codex 里是同一个变体;第三行让 MCP 原样解失败;resume 时第三行计入 parse_errors 且会话照常打开。这三行夹具能防止有人在迁移中悄悄改掉默认方向。
type 分别是 task_started、turn_started、future_event。推演 MCP 原样解、Codex resume、DSH、Grok 各自怎样。哪一行会让 MCP 失败?哪一行会让 DSH 拒绝整份日志?哪两行在 Codex 里其实是同一个变体?TurnStarted 的 serde 改成只保留 rename = "turn_started",旧 rollout 会在哪一条边界上断?命令通道和事件通道分开,命令可以带回调,事件必须能写成 JSON。wire 名和代码名分开写,改标识符时用 rename 保住磁盘。未知 type 先选一条默认方向:拒绝、跳行,或收成 Unknown。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页正文为二次演绎配音版的配套解读,与音频逐段对应。