# OpenAI Codex 源码分析（加深版）：把「本地编程 Agent」拆成可逐点核对的工程系统

> **分析对象**：[openai/codex](https://github.com/openai/codex)（**Codex CLI**；≠ ChatGPT 网页版 Codex Web）  
> **基于 commit**：`bb1af235ea2822d7a40f75ef52e4d6a2cde84da2`（2026-07-28，`Load thread titles concurrently during session startup (#35779)`）  
> **分析日期**：2026-07-28 · **文档版本**：**加深版 · 含场景推演（第23–28章）**（按「一点一阐释」重写；目标对齐 Open Design / Claude Code 深入版颗粒度）  
> **许可**：Apache-2.0（`LICENSE`，Copyright 2025 OpenAI）  
> **规模**：`codex-rs/` ≈63MB · **2783** 个 `.rs` · Cargo workspace **126** members · `session/turn.rs` **2741** 行 · `session/mod.rs` **4154** 行  
> **npm**：`@openai/codex`（`codex-cli/package.json`）→ `bin/codex.js`  
> **写法**：每个关键点固定四拍——**①这个点在解决什么 → ②源码长什么样（文件:行号）→ ③为什么这样设计 → ④读者检查清单**。不编造行号；不确定处标明。  
> **配套**：[三方深度对比](Claude-Code-vs-OpenCode-vs-Codex-深度对比.md) · 交互解析站 [`openai-codex-解析.html`](../openai-codex-解析.html)（**5 个可运行实验台**）  
> **参考树**：`参考项目/codex/`

---

## 目录

**第一部分 · 它是什么**
1. [项目概览：先把产品边界钉死](#ch1)
2. [全景架构：126 crate 与「抵制 core 膨胀」](#ch2)
3. [启动与多入口：MultitoolCli 逐命令](#ch3)

**第二部分 · 心脏（最重要）**
4. [Thread · Session · 任务系统](#ch4)
5. [外层循环：RegularTask](#ch5)
6. [内层循环：run_turn 逐步拆（上）——进门到采样](#ch6)
7. [内层循环：run_turn 逐步拆（下）——采样后与停机](#ch7)
8. [run_sampling_request 与 try_run_sampling_request](#ch8)
9. [压缩家族：pre / mid / previous-model](#ch9)
10. [上下文六条宪法](#ch10)

**第三部分 · 手与闸门**
11. [工具装配：ToolSpec · built_tools · Router](#ch11)
12. [编排器：approval → sandbox → attempt → retry](#ch12)
13. [Shell 路径：从参数到 ExecApprovalRequirement](#ch13)
14. [apply_patch：handler + arg0 第二人格](#ch14)
15. [沙箱三平台与 Landlock 规则](#ch15)
16. [AskForApproval 与审批缓存](#ch16)

**第四部分 · 上下文与编排**
17. [AGENTS.md 发现算法逐行](#ch17)
18. [Skills · Plugins · Hooks · 子 Agent](#ch18)

**第五部分 · 界面与对外**
19. [TUI · Exec · Doctor](#ch19)
20. [App-server · MCP · Rollout](#ch20)
21. [认证与模型通道](#ch21)

**第六部分 · 总结**
22. [独特设计、取舍、读法、对照](#ch22)

**第七部分 · 场景级逐步推演（把点连成故事）**
23. [场景：交互会话里执行 `rm -rf build`](#ch23)
24. [场景：一次多文件 apply_patch](#ch24)
25. [场景：上下文将爆窗时的 mid-turn 压缩](#ch25)
26. [场景：stop hook 要求续跑](#ch26)
27. [场景：CI 中 `codex exec` + Never](#ch27)
28. [场景：AGENTS.md 链与 override 谁赢](#ch28)

---


<h2 id="ch1">第 1 章 项目概览：先把产品边界钉死</h2>

### 点 1.1 · 「本地 CLI」和「Codex Web」不是同一个东西

**①解决什么**  
用户搜索 “Codex” 会撞上三样：ChatGPT 网页云端 Agent、IDE 扩展、本仓库 CLI。混谈会导致「为什么源码里没有网页会话页」的无效问题。

**②源码 / 文档长什么样**  
根 `README.md` 明确写：CLI runs locally；IDE 另链；Cloud 去 chatgpt.com/codex。本分析只覆盖 **本 monorepo 实现的本地运行时**。

**③为什么**  
OpenAI 用同一品牌覆盖多表面，但工程仓库只承载可开源的本地机库。云端产品不必、也不应出现在这个 tree 里。

**④检查清单**  
- [ ] 你要分析的是 `codex` 二进制还是网页？  
- [ ] 是否误把 app-server 当成「网页后端全集」？

### 点 1.2 · 一句话工程定义

**Codex CLI = Rust Agent 运行时 + Responses API 工具面 + OS 沙箱 + AGENTS.md 说明书加载 + rollout 会话资产。**

拆开：

| 零件 | 落点 |
|---|---|
| 运行时 | `codex-rs/core`（Session / turn） |
| 工具面 | `codex-rs/tools` + `core/tools/handlers` |
| 沙箱 | `sandboxing` + `linux-sandbox` + macOS Seatbelt + Windows Token |
| 说明书 | `codex-rs/core/src/agents_md.rs` |
| 会话资产 | `rollout` |
| 门面 | `cli` / `tui` / `exec` / `app-server*` |

### 点 1.3 · 许可与发行

- Apache-2.0 · Copyright 2025 OpenAI（`LICENSE`）  
- 安装：`curl …/codex/install.sh` · `npm i -g @openai/codex` · brew cask  
- npm 包只是薄包装：`codex-cli/package.json` 的 `bin.codex` → `bin/codex.js`，真正逻辑在 Rust 二进制  

### 点 1.4 · 和本系列邻居的「物种差」

| 邻居 | 关系 |
|---|---|
| Claude Code | 同类产品（本地编程 Agent），闭源，工具全家桶 |
| OpenCode | 同类开源，Bun/Effect，服务器即本体，多 provider |
| Open Design | **宿主**：把 `codex` 当引擎之一，自己不写循环 |
| MiMo-Code | OpenCode fork，工具 ABI 向 Codex 风格靠拢 |

```mermaid
flowchart LR
  Brand["Codex 品牌"] --> CLI["本仓库 CLI"]
  Brand --> IDE["IDE 扩展"]
  Brand --> Web["Codex Web 云"]
  CLI --> Core["codex-rs"]
```

> 🧠 **一句话**：先承认「品牌大、仓库专」，后面每一章才站得住。

---


<h2 id="ch2">第 2 章 全景架构：126 crate 与「抵制 core 膨胀」</h2>

### 点 2.1 · 为什么要拆成一百多个 crate？

**①** 大型 Rust 产品用 crate 边界代替「文件夹约定」，强制依赖方向。  
**②** `codex-rs/Cargo.toml` 的 `members` 列表约 126 项：`core`、`tui`、`cli`、`tools`、`protocol`、`sandboxing`、`apply-patch`、`rollout`、`login`、`app-server*`…  
**③** 根 `AGENTS.md:72-83` **明文抵制**继续往 `codex-core` 塞东西——承认历史已经 bloated。  
**④检查**：新功能该进新 crate 还是 core？官方政策答案是前者。

### 点 2.2 · 四层楼梯

```mermaid
flowchart TB
  subgraph L1["门面层"]
    CLI["cli Multitool"]
    TUI["tui"]
    EX["exec"]
    AS["app-server*"]
  end
  subgraph L2["会话层"]
    TH["CodexThread"]
    SE["Session"]
    TK["tasks/*"]
  end
  subgraph L3["回合层"]
    RT["run_turn"]
    SR["run_sampling_request"]
    OR["ToolOrchestrator"]
  end
  subgraph L4["副作用层"]
    HD["handlers"]
    SB["SandboxManager"]
    FS["exec-server FS"]
  end
  CLI --> TH
  TUI --> TH
  EX --> TH
  AS --> TH
  TH --> SE --> TK --> RT --> SR --> OR --> HD --> SB
```

### 点 2.3 · 体积信号（本机检出）

| 路径 | 信号 |
|---|---|
| `tui/` ~14MB | 交互面是产品门面 |
| `core/` ~11MB | 作战室；政策说别再塞 |
| `turn.rs` 2741 行 | 单文件心脏 |
| `session/mod.rs` 4154 行 | Session 巨石 |
| `orchestrator.rs` 533 行 | 审批+沙箱状态机集中处 |

### 点 2.4 · 工程文化（读架构必读）

根 `AGENTS.md` 还规定：模块目标 <500 LoC、超 800 请拆；TUI 中心文件少加方法；`just test` 不用裸 `cargo test`；上下文注入必须 `ContextualUserFragment`。  
**这些不是风格指南装饰，是理解「为什么文件长这样」的钥匙。**

---


<h2 id="ch3">第 3 章 启动与多入口：MultitoolCli 逐命令</h2>

### 点 3.1 · 默认路径：无子命令 → TUI

**②** `cli/src/main.rs:90-119`：`MultitoolCli`；`bin_name = "codex"`；`subcommand_negates_reqs = true`；字段 `interactive: TuiCli`。  
**③** 用户心智是「敲 codex 就聊天」；运维能力用子命令展开，不污染默认。  

### 点 3.2 · 子命令分类（`Subcommand` 约 122–211 行）

**A. 跑任务**  
- `exec`/`e`：非交互  
- `review`：非交互 review  

**B. 身份**  
- `login` / `logout`  

**C. 扩展面**  
- `mcp` / `mcp-server`  
- `plugin`  
- `app-server` / `remote-control` / 平台 `app`  

**D. 会话资产**  
- `resume` / `fork` / `archive` / `unarchive` / `delete`  

**E. 安全与诊断**  
- `sandbox`：在沙箱里跑命令（`HostSandboxArgs`）  
- `doctor`  
- `debug`（`DebugCommand`）  
- `execpolicy`（hidden）  

**F. 其它**  
- `apply`/`a`：把 agent diff `git apply`  
- `cloud`：云任务（实验）  
- `features` / `update` / `completion`  
- 隐藏：`responses-api-proxy`、`stdio-to-uds`  
  > 注：`exec-server` 虽在 `main.rs:206-207` 标了 `/// [EXPERIMENTAL]` 文档注释，但**没有 `hide = true`**，因此是公开可见的子命令，不应列入隐藏组（核查修正 2026-08-04）。  

### 点 3.3 · 设计取舍

| 取舍 | 含义 |
|---|---|
| 瑞士军刀 CLI | 学习成本高，但运维闭环全在一个 binary |
| 隐藏危险/内部命令 | 减少误用；分析时要用 `--help` 与源码对照 |
| macOS/Windows 条件编译 `app` | 桌面不是全平台一等 |

```mermaid
flowchart LR
  A["codex"] --> B{"subcommand?"}
  B -->|无| C["TUI"]
  B -->|exec| D["非交互 turn"]
  B -->|resume| E["rollout 恢复"]
  B -->|mcp-server| F["自己当 MCP"]
  B -->|sandbox| G["沙箱调试"]
```

---


<h2 id="ch4">第 4 章 Thread · Session · 任务系统</h2>

### 点 4.1 · CodexThread 是门面

**①** UI/协议不要直接碰 Session 内部锁。  
**②** `codex-rs/core/src/codex_thread.rs:182` `struct CodexThread`；`submit` 约 225 行把 `Op` 交进去。  
**③** 门面模式让 TUI / app-server / exec 共用同一提交语义。  
**④** 找「用户点了发送之后第一站」→ 从 Thread.submit 跟进。

### 点 4.2 · Session.submit 家族

`codex-rs/core/src/session/mod.rs:793` 起：

- `submit(op)`  
- `submit_with_trace`  
- `submit_with_id`  
- `submit_user_input_with_client_user_message_id`（813+）  

同一会话还有 `active_turn`、`input_queue`、services（model client、exec policy、plugins…）。

### 点 4.3 · 插话与空闲启动（inject）

`codex-rs/core/src/session/inject.rs`：

- `inject_if_running`：模型跑着时把输入塞进当前 turn  
- `try_start_turn_if_idle`：空闲才开新 turn；Plan 模式会拒绝某些自动 turn（约 89–92 行）  

**③** 这解释了产品里「边跑边打字」为何不丢字，以及 Plan 为何更「安静」。

### 点 4.4 · 任务不是只有 Regular

`tasks/` 下还有压缩、review 等任务类型（`RegularTask` 在 `codex-rs/core/src/tasks/regular.rs`）。Session 用 `spawn_task` 挂不同 `SessionTask` 实现。

---

<h2 id="ch5">第 5 章 外层循环：RegularTask</h2>

### 点 5.1 · 文件虽短，地位极高

`codex-rs/core/src/tasks/regular.rs` 全文约百行，但是 **UI 回合 → run_turn** 的适配器。

### 点 5.2 · run() 逐步

| 步 | 行号约 | 做什么 |
|---|---|---|
| 1 | 47–58 | 发 `TurnStarted`（含 mode、context window、trace） |
| 2 | 59 | `set_server_reasoning_included(false)` |
| 3 | 60–74 | 消费 startup prewarm：Cancelled→早退；Ready→带预热 session |
| 4 | 77–92 | **外层 loop**：`run_turn`；无 pending 则返回；有 pending 则 `next_input=[]` 再跑 |

### 点 5.3 · 两层 loop 对照表（死记）

| 层 | 位置 | 循环原因 |
|---|---|---|
| 外 | RegularTask | 用户又插话了 / 队列里还有触发项 |
| 内 | run_turn | 模型还要工具 / stop hook 续跑 / 压缩后继续 |

```mermaid
stateDiagram-v2
  [*] --> Prep: TurnStarted + prewarm
  Prep --> Inner: run_turn
  Inner --> Inner: pending input
  Inner --> [*]: no pending
```

### 点 5.4 · 两个只在源码里看得见的细节

外层 loop 全文就这么几行（`codex-rs/core/src/tasks/regular.rs:76-92`），但有两处值得单独指出来：

```rust
let mut next_input = input;
loop {
    let last_agent_message = run_turn(
        ..., next_input,
        prewarmed_client_session.take(),          // ← ①
        cancellation_token.child_token(),         // ← ②
    ).await?;
    if !sess.input_queue.has_pending_input(&sess.active_turn).await {
        return Ok(last_agent_message);
    }
    next_input = Vec::new();                      // ← ③
}
```

**① `.take()`：预热只给第一圈**
`prewarmed_client_session` 是 `Option`，`.take()` 会把它置空。所以**只有外层循环的第一次 `run_turn` 拿得到预热连接**，之后每一圈都是 `None`，走 `sess.services.model_client.new_session()`。
> ③为什么：预热是为了**掩盖冷启动延迟**——用户按下回车那一刻的等待。第二圈已经在对话中了，没有可掩盖的首字延迟，再占着一条预热连接没有意义。

**② `child_token()`：每圈一个子令牌**
不是把同一个 `cancellation_token` 传下去，而是每圈派生一个子令牌。父令牌取消会连带取消所有子令牌，但**单圈内部的取消不会反向污染整个 task**。

**③ `next_input = Vec::new()`：第二圈开始不带新输入**
这是「插话」语义的落点：第一圈带着用户这次的输入跑；如果跑完发现队列里还有东西（用户在模型输出期间又打了字），**下一圈传空 input**——因为那些插话已经在 `input_queue` 里，由 `run_turn` 内部的 drain 逻辑负责取，不能再从外面塞一遍。

> 🧠 **取舍**：把「插话」放在外层而不是塞进 sampling 重试，避免把**用户输入**和**模型 tool-followup** 搅成同一种控制流。
> 代价是外层要多一次完整的 `run_turn` 进门开销（pre-compact、MCP 唤醒、step context 冻结全部重来）——**Codex 选择了控制流清晰，而不是省这一次开销**。

---

<h2 id="ch6">第 6 章 内层循环：run_turn 逐步拆（上）——进门到采样</h2>

> 文件：`codex-rs/core/src/session/turn.rs`（**2741 行**）。入口：`run_turn` @ **153**。

### 点 6.1 · 函数签名在声明什么契约

```text
run_turn(sess, turn_context, turn_extension_data, input,
         prewarmed_client_session, cancellation_token)
  -> CodexResult<Option<String>>
```

返回的 `Option<String>` 是「最后一条 agent 消息」类结果，供外层/hooks 使用。`cancellation_token` 贯穿——取消不是事后礼貌，是一等参数。

### 点 6.2 · 进门第一件事：pre_sampling_compact（167–184）

调用 `run_pre_sampling_compact`（定义 @ **983**）：

1. 先 `maybe_run_previous_model_inline_compact`——**换模型 / compaction hash 变了**时用旧模型做兼容压缩（1047–1056 注释）  
2. 看 `context_window_token_status`；若 `token_limit_reached`，捕获 step_context 后 `run_auto_compact(..., CompactionPhase::PreTurn)`  

**①解决什么**：避免带着已经爆窗的历史去采样。  
**③为什么先压再记新输入**：注释 TODO（163–166）承认理想是「估算即将到来的 token 再压」，当前顺序仍是折中。  
**④检查**：你改压缩阈值时，pre-turn 与 mid-turn（后文）是否语义一致？

### 点 6.3 · 解析本轮要唤醒的 MCP（187–197）

`required_mcp_servers_for_input`：按用户输入决定 **lazy 唤醒**哪些 MCP，而不是每次全开。  
取消则记 hooks/inputs 后返回。

### 点 6.4 · 冻结 step_context（199–214）

`capture_step_context_with_required_mcp_servers`：**本步工具可见集、世界状态、配置快照**冻结。  
后面采样、工具调用必须共享同一视图（注释 294：context / advertised tools / tool calls 同一 request view）。

**③** 这是「可复现的一步」——调试「为什么这轮没有某工具」时，先看 step_context 而不是全局 config。

### 点 6.5 · world_state 与 diff roots（215–218）

`tokio::join!`：一边 `record_context_updates_and_set_reference_context_item`，一边算 `turn_diff_display_roots`。  
并行是为了启动延迟；语义上两者都服务「本 turn 的可见世界」。

### 点 6.6 · skills / plugins 注入（220–230）

`build_skills_and_plugins`：失败返回 `Ok(None)` 早退——技能装配不是可选装饰，失败就不开跑。

### 点 6.7 · hooks 与记录输入（232–256）

- `run_pending_session_start_hooks`  
- `run_hooks_and_record_inputs`  
- 合并 connector selection  
- 记录 `PreviousTurnSettings`（model slug、comp_hash、realtime）  
- 把 injection_items 写入 conversation  

**④检查**：session start hook 返回「该停」时，是否误以为是模型空回复？

### 点 6.8 · 进入 loop 前的两个策略开关（236 赋值 · 264–269 注释）

- `can_drain_pending_input`：turn 开头常为 false——**先采样本轮 input，再排空插话**  
- auto-compact 后续跑时也会推迟 drain（注释 266–268）  

```mermaid
flowchart TB
  A["run_turn 进入"] --> B["pre_sampling_compact"]
  B --> C["MCP required"]
  C --> D["capture_step_context"]
  D --> E["skills/plugins"]
  E --> F["hooks + record input"]
  F --> G["loop: sampling"]
```

---

<h2 id="ch7">第 7 章 内层循环：run_turn 逐步拆（下）——采样后与停机</h2>

### 点 7.1 · loop 头：pending + reminders（270 起 loop）

每圈可能：

1. drain pending（若允许）并跑 hooks  
2. `rollout_budget::maybe_record_reminder`  
3. 取得/重建 `step_context`（有 pending 用户输入时按输入重算 MCP）  
4. `time_reminder::maybe_record_current_time_reminder`  
5. 更新 world_state  
6. `clone_history().for_prompt(modalities)` 构造采样输入  

### 点 7.2 · 调用采样（344–356）

`run_sampling_request(...)`（定义 @ **1308**）。成功得到：

- `model_needs_follow_up`  
- `last_agent_message`  

### 点 7.3 · 采样后状态机（357–512）——逐分支

**分支 A · 需要 follow-up**

- `accept_mailbox_delivery_for_current_turn`（364–369）  
- `can_drain_pending_input = true`  
- 计算 `needs_follow_up = model_needs_follow_up || has_pending_input`  
- 打大量 token usage trace（387–401）  

**分支 B · should_roll_over（新窗 / token 顶）**（419–458）

- 条件：`needs_follow_up && (new_context_window_request || token_limit_reached)`  
- `run_auto_compact(..., CompactionPhase::MidTurn)`，注入策略 `BeforeLastUserMessage`  
- 压缩后可能再跑 session start hooks  
- `can_drain_pending_input = !model_needs_follow_up` 后 `continue`  

注释 430：**相信压缩能拉回窗口，因此不怕无限循环**——这是显式产品赌注。

**分支 C · 不需要 follow-up → 停机协议**（461–510）

1. `run_turn_stop_hooks`  
2. 若 `should_block` 且有 continuation fragments → 记入历史、开 mailbox、`stop_hook_active=true`、`continue`（钩子要求续跑）  
3. 若 block 但无 prompt → Warning 后忽略  
4. 若 `should_stop` → `break`  
5. legacy after-agent hook 可能 `Ok(None)`  
6. 否则 `break`

**分支 D · 错误**

- `TurnAborted` 上抛  
- 其它错误继续在后续 match 臂处理（517+）  

### 点 7.4 · 一张「停得下来吗」决策表

| 信号 | 行为 |
|---|---|
| 模型还要工具 | continue |
| 用户插话在队列 | continue |
| token 顶且还要继续 | mid-turn compact 后 continue |
| stop hook block+prompt | 注入后续 continue |
| stop hook stop | break |
| 都没有 | break，把 last_agent_message 带回 |

```mermaid
flowchart TB
  S["sampling 返回"] --> F{"needs_follow_up?"}
  F -->|是| R{"should_roll_over?"}
  R -->|是| C["mid-turn compact"] --> S
  R -->|否| S
  F -->|否| H["stop hooks"]
  H --> B{"block?"}
  B -->|是+有prompt| S
  B -->|否| K{"stop?"}
  K -->|是| X["break"]
  K -->|否| X
```

> 🧠 **一句话**：Codex 的「停」不是 `if !tool_calls`，而是 **follow-up × 压缩 × stop hook** 的三方协议。

---

<h2 id="ch8">第 8 章 run_sampling_request 与 try_run_sampling_request</h2>

### 点 8.1 · run_sampling_request（1308–1405）：带重试的外壳

逐步：

1. 取 `base_instructions`  
2. 建 `ToolCallRuntime`（router + sess + step_context + diff tracker）  
3. 启动 `code_mode_service.start_turn_worker`（code mode 并行工人）  
4. **重试 loop**：  
   - 第一次用传入 input；之后从 history `for_prompt`  
   - `build_prompt`  
   - `try_run_sampling_request`  
   - 成功则返回 `(output, original_input)`  
   - `ContextWindowExceeded` / `UsageLimitReached`：更新状态后返回错  
   - 其它：若 retryable，走 `handle_retryable_response_stream_error` 并 `record_sampling_retry`

**③** WebSocket/流式错误可恢复时不把整 turn 打死；但上下文爆窗与额度硬错误立即失败。

### 点 8.2 · try_run_sampling_request（2165+）：真·收流

关键结构：

- `feedback_tags!` 打上 model、approval_policy、sandbox_policy、effort、auth、features（2176–2183）——**可观测性一等**  
- `client_session.stream(prompt, ...)` 开流（2195–2208），可 cancel  
- `FuturesOrdered` 的 `in_flight`：**并行工具调用**的未来队列（2209）  
- `needs_follow_up` / `last_agent_message` 累加器  
- Plan 模式专用 parser 与 `PlanModeStreamState`（2221–2223）  
- 外层 `loop` 拉 stream event（2228+）  

模型是否支持并行：`supports_parallel_tool_calls`（约 1289 行，在 built path 附近）。

### 点 8.3 · 为什么要两层 sampling 函数？

| 层 | 职责 |
|---|---|
| `run_sampling_request` | 重试、换 prompt input、额度/窗错误 |
| `try_run_sampling_request` | 单次 stream 生命周期、工具并发、UI 事件 |

拆开后测试与 tracing（`receiving_stream` span）更干净。

### 点 8.4 · 读者实验

1. 断网模拟：看 retryable 路径是否触发  
2. Plan 模式发消息：看 `plan_mode` 分支事件是否不同  
3. 打开 parallel tool 的模型：观察 `FuturesOrdered` 是否多任务重叠  

---

<h2 id="ch9">第 9 章 压缩家族：pre / mid / previous-model</h2>

### 点 9.1 · 触发源对照

| 相位 | 函数/调用点 | 原因枚举 |
|---|---|---|
| PreTurn | `run_pre_sampling_compact` @983 | `token_limit_reached` |
| MidTurn | run_turn 内 `should_roll_over` @431 | ContextLimit + 新窗请求 |
| Previous model | `maybe_run_previous_model_inline_compact` @1051 | comp_hash 变或更小窗 |

### 点 9.2 · comp_hash_changed（1014–1019）

**只有双方都有 hash 且不同才触发**——缺 hash 不瞎压。这是为了避免「信息不完整时误压缩」毁掉 cache。

### 点 9.3 · previous-model compact 的前置条件（1032–1040）

> ⚠️ **核查修正（2026-08-04）**：原稿把 1032–1040 段当成 previous-model 压缩的「开关前置条件」，这是**张冠李戴**。

`turn.rs:1032-1041` 真实是 `capture_current_model_fallback_step_context`（定义 @1026），其 doc 注释自述用途是 *"Captures the current model's request-scoped state for **retrying** previous-model compaction"*——它只决定**失败时能否拿到用于重试的 fallback step context**，不是压缩本身的开关；它的 `Ok(None)` 返回只是「不捕获回退上下文」，不是「不压缩」。

真正的触发闸门在 `maybe_run_previous_model_inline_compact`（`turn.rs:1057-1119`），三条触发路径**都不检查 auth backend 或 provider 是否 OpenAI**：
1. `comp_hash_changed` 为真（@1071 直接压缩）；或
2. `previous_model_limit_reached && slug 不同 && old_context_window > new_context_window`（@1116-1118 压缩）。

**结论修正**：第三方 provider 上 previous-model 压缩**照常触发**；Codex-backend + OpenAI 的额外判断仅用于「压缩失败时能否回退到 fallback step context」，而非「是否压缩」。原稿「不在第三方 provider 上玩这套兼容压缩」一句应删除。

### 点 9.4 · InitialContextInjection

Mid-turn 使用 `BeforeLastUserMessage { world_state, step_context }`——压缩后要把世界状态按正确位置塞回，避免「摘要盖住最后一句用户话」。

### 点 9.5 · 与上下文宪法的关系

压缩是六条宪法的执行器之一：历史只增量、有界、保护 cache。乱改压缩注入位置会直接违反 `AGENTS.md:91-100`。

---

<h2 id="ch10">第 10 章 上下文六条宪法</h2>

根 `AGENTS.md:91-100` 原文精神转写：

| # | 规则 | 工程含义 |
|---|---|---|
| 1 | No history rewrite | 禁止「偷改已发送前缀」 |
| 2 | 少动前缀 | 保护 prompt cache，省钱降延迟 |
| 3 | 有界 + 硬顶 | 防记忆泄漏式无限注入 |
| 4 | 单条 ≤10K tokens | 硬限制 |
| 5 | 可能 >1k 的新条目 = P0 人工审 | 流程门禁 |
| 6 | 必须 `ContextualUserFragment` | 类型系统强制登记 |

**对照**

- Claude Code：CLAUDE.md 收集规则 + system-reminder  
- OpenCode/MiMo：外置 MEMORY/checkpoint  
- Codex：**先管「模型可见历史」本身的洁癖**，外置记忆是另一层（memories crates 存在，但宪法写的是 history）

**④检查清单（给 PR reviewer）**  
- [ ] 新注入是否走 Fragment？  
- [ ] 是否改写了旧 history 条目？  
- [ ] 单条是否可能 >1k？有没有标 P0？  

---

<h2 id="ch11">第 11 章 工具装配：ToolSpec · built_tools · Router</h2>

### 点 11.1 · ToolSpec：给 Responses API 的「菜单 JSON」

`codex-rs/tools/src/tool_spec.rs:19-53`：

| 变体 | wire `type` | 含义 |
|---|---|---|
| Function | function | 常规函数工具 |
| Namespace | namespace | 命名空间分组 |
| ToolSearch | tool_search | 工具搜索 |
| WebSearch | web_search | 托管搜索（external/indexed 开关） |
| Freeform | custom | 自由形 |

`name()` @56-66；`create_tools_json_for_responses_api` @79 序列化进请求。

**③** Codex 不对模型隐瞒「这是 OpenAI Responses 工具协议」——与 Claude Code 的 Anthropic tool 块是平行宇宙。

### 点 11.2 · built_tools（codex-rs/core/src/session/turn.rs:1459+）：一回合的工具宇宙

> 核查修正（2026-08-04）：原稿只写「1459+」未给文件路径。真实位置是 `codex-rs/core/src/session/turn.rs:1459`（非 `tools/` 目录下），签名 `built_tools(sess, turn_context, environments, mcp, step_store, prepared_recommendations) -> (Vec<ToolInfo>, Arc<ToolRouter>)`。

输入：Session、TurnContext、environments、MCP binding、extension store、prepared recommendations。  
输出：`(Vec<ToolInfo>, Arc<ToolRouter>)`。

中间会：拉 MCP tools、按 apps_enabled 合并 connectors、处理 tool_suggest 推荐候选、再装配 router。  
**①** 「这一步模型能看见什么工具」是算出来的，不是全局常数。

### 点 11.3 · handlers 目录是「手」的实现表

`codex-rs/core/src/tools/handlers/mod.rs` 导出：Shell、ApplyPatch、ViewImage、Plan、MCP、multi_agents、unified_exec、RequestUserInput/Permissions、Plugins、ContextRemaining、WaitForEnvironment、Dynamic…  
> 核查修正（2026-08-04）：`CodeMode` **不在** `handlers/mod.rs` 导出表内——它是 `codex-rs/core/src/tools/code_mode/`，由 `tools/mod.rs:2` 的 `pub(crate) mod code_mode;` 声明，是 `handlers` 的**兄弟模块**而非子模块。

**和 Claude Code 的差异（讲清楚）**：

| | Claude Code | Codex |
|---|---|---|
| 文件读取 | 常有独立 Read 工具 | 多落在 shell / 其它 |
| 补丁 | Edit/Write | **apply_patch 一等** |
| 搜索代码 | Grep/Glob | 常 shell；web 用托管 WebSearch |
| 并行 | 产品定义 | `FuturesOrdered` + 模型 capability |

### 点 11.4 · ToolCallRuntime

在 `run_sampling_request` 里用 router+session+step+diff 构造（1323–1328）。流式过程中工具调用交给它调度，而不是散落在 match 臂里。

---

<h2 id="ch12">第 12 章 编排器：approval → sandbox → attempt → retry</h2>

### 点 12.1 · 文件头即说明书

`codex-rs/core/src/tools/orchestrator.rs:4-7` 注释写死顺序：

> approval → select sandbox → attempt → retry with escalated sandbox on denial（**不再重新审批**，因已批准）

这是整章的骨架。

### 点 12.2 · ExecApprovalRequirement 三态（`codex-rs/core/src/tools/sandboxing.rs:156-175`）

| 态 | 含义 |
|---|---|
| `Skip { bypass_sandbox, proposed_amendment }` | 不必问人；可能首尝试就免沙箱 |
| `NeedsApproval { reason, proposed_amendment }` | 要问人；可附带「以后类似命令免问」的策略修正提案 |
| `Forbidden { reason }` | 直接禁止 |

### 点 12.3 · default_exec_approval_requirement（198–234）

**它是两段式的，顺序不能混**（这点很容易读错，展开说）：

```rust
// 第一段：只看 policy × fs.kind，算出 needs_approval
let needs_approval = match policy {
    Never                        => false,
    OnRequest | Granular(_)      => matches!(fs.kind, FileSystemSandboxKind::Restricted),
    UnlessTrusted                => true,
};

// 第二段：needs_approval 为真时，Granular 才有机会把它升级成 Forbidden
if needs_approval && matches!(policy, Granular(cfg) if !cfg.allows_sandbox_approval()) {
    Forbidden { reason: "approval policy disallowed sandbox approval prompt" }
} else if needs_approval { NeedsApproval { .. } }
else { Skip { bypass_sandbox: false, .. } }
```

真值表（`allows_sandbox_approval()` 记作 **ASA**）：

| policy | FS = Restricted | FS ≠ Restricted |
|---|---|---|
| `Never` | Skip | Skip |
| `UnlessTrusted` | NeedsApproval | **NeedsApproval**（与 FS 无关） |
| `OnRequest` | NeedsApproval | Skip |
| `Granular` · ASA=true | NeedsApproval | Skip |
| `Granular` · **ASA=false** | **Forbidden** | **Skip** ← 注意这一格 |

> ⚠️ **最右下那一格是最容易想错的**：`Granular` 把沙箱审批关掉了，但文件系统**不是** Restricted 时，结果是 **Skip 而不是 Forbidden**。
> 因为 `Forbidden` 挂在 `needs_approval` 这个前提下——**不需要问人的时候，"不许问人"这条配置根本不参与判定**。
>
> **③ 精妙点仍然成立，但要说准**：当系统**本来打算弹窗**、而策略又禁止弹窗时，Codex 选择 **Forbidden 而不是静默放行**——防止「我关了弹窗 = 我允许一切」的心智错误。
> 而在本来就不需要问的场景里，它不会平白多拒绝一次。**这是一条精确的规则，不是一刀切。**

### 点 12.4 · SandboxOverride（236–271）

`sandbox_override_for_first_attempt`：

1. 若策略含 **denied-read**，禁止 unsandboxed（否则否认读会失效）→ `NoOverride`  
2. 若 ExecPolicy Allow 且 `bypass_sandbox=true` → 首尝试可 Bypass  
3. 若请求了 escalated permissions → Bypass  
4. 否则 NoOverride  

`unsandboxed_execution_allowed`（279–283）：有 denied-read 限制则 **false**。  
`sandbox_permissions_preserving_denied_reads`（285–298）：escalated 遇上 denied-read 时 **降回 UseDefault**，避免「抬权=拆掉唯读拒绝」。

> 🧠 **一句话**：抬权与「拒绝读取」冲突时，Codex 选择 **保住拒绝读取**，而不是满足抬权。

### 点 12.5 · Approvable / Sandboxable / ToolRuntime traits（312–406）

- `approval_keys`：apply_patch 可多 key（多文件）  
- `should_bypass_approval`：已批准或 Never  
- `wants_no_sandbox_approval`：UnlessTrusted 总要；Never 不要；Granular 看配置  
- `ToolRuntime::run(req, SandboxAttempt, ctx)`：真正执行  

### 点 12.6 · SandboxAttempt 结构（408–425）

携带：`SandboxType`、是否请求了沙箱、权限画像、linux-sandbox exe、legacy landlock、Windows level/private desktop、network proxy、cancellation…  
**这是「一次尝试」的完整环境说明书。**

```mermaid
flowchart LR
  R["Tool request"] --> A["ExecApprovalRequirement"]
  A -->|Forbidden| X["拒绝"]
  A -->|NeedsApproval| U["问人 / 缓存"]
  A -->|Skip| S["选 SandboxType"]
  U --> S
  S --> T["SandboxAttempt.run"]
  T -->|沙箱拒绝且可升级| S2["escalated retry 不再问人"]
```

---

<h2 id="ch13">第 13 章 Shell 路径：从参数到 ExecApprovalRequirement</h2>

### 点 13.1 · run_exec_like 总流程（`codex-rs/core/src/tools/handlers/shell.rs:63`）

1. 取 filesystem、环境变量覆盖  
2. `Feature::ExecPermissionApprovals` 是否开启  
3. `apply_granted_turn_permissions` 合并本 turn 已授权利  
4. 规范化/校验 additional permissions（109–120）  
5. **抬权守卫**（122–138）：若请求 sandbox override 且未预批准，且策略不是 OnRequest → **直接 RespondToModel 拒绝**（文案写明不要在非 OnRequest 下要抬权）  
6. **拦截 apply_patch**（140–157）：命令里若是 patch，转交 `intercept_apply_patch`——shell 里偷偷打补丁也会被抓到结构化路径  
7. 发 ToolEmitter begin 事件  
8. `create_exec_approval_requirement_for_command`（175–190）  
9. 组装 `ShellRequest`，`ToolOrchestrator::run` + `ShellRuntime`  

### 点 13.2 · 为什么 shell 要拦截 apply_patch？

防止模型用 `apply_patch <<'EOF'` 走「纯 shell 字符串」绕过补丁审批键（多文件 key）与解析器。  
**结构化补丁必须走结构化闸门。**

**而且这个拦截不止一处。** 全仓搜 `intercept_apply_patch` 只有两个调用点，它们恰好是**模型能跑命令的两条路**：

| 调用点 | 这条路是什么 |
|---|---|
| `codex-rs/core/src/tools/handlers/shell.rs:142` | 经典 `shell` 工具 |
| `codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs:314` | **unified_exec 后端**（持久 shell 会话） |

> 🔑 **这才是完整的论证**。只堵一条路等于没堵——模型换个后端就绕过去了。
> 换句话说：**「结构化补丁必须走结构化闸门」这条不变量，是靠"在每一个能执行命令的入口都插同一个拦截器"来维持的**，而不是靠某一个工具的自觉。
>
> **④检查**：如果你要给 Codex 加第三种执行后端，`intercept_apply_patch` 是必须一起接上的东西——否则就开了一个静默的补丁旁路。

### 点 13.3 · Shell 后端变体（tool_config.rs）

Classic shell vs unified_exec vs zsh-fork：特性旗标组合决定后端；注释强调不能静默落到未开启后端。  
**④检查**：开 zsh-fork 是否同时满足 feature + 用户壳 + 路径？

### 点 13.4 · 和「安全命令」叙事的关系

UnlessTrusted 依赖「已知安全只读」判定（协议注释 @ `codex-rs/protocol/src/protocol.rs:909-911`）。Shell 路径上真正落地的是 exec_policy + orchestrator，而不是只靠模型自觉。

---

<h2 id="ch14">第 14 章 apply_patch：handler + arg0 第二人格</h2>

### 点 14.1 · 独立 crate

`codex-rs/apply-patch`：`parse_patch`、`StreamingPatchParser`、`Hunk`… 可在 ExecutorFileSystem 上落盘。

### 点 14.2 · Handler

`codex-rs/core/src/tools/handlers/apply_patch.rs`（**666 行**）：实现审批 keys（每文件）、与 orchestrator 集成。  
多文件一次 patch → 多个 approval keys（sandboxing 注释 65–70、316–321）。

### 点 14.3 · arg0 第二人格（`codex-rs/arg0/src/lib.rs`）

| 机制 | 行号约 | 作用 |
|---|---|---|
| `APPLY_PATCH_ARG0 = "apply_patch"` | 20 | 可执行文件名匹配则进入 patch main |
| 兼容 `applypatch` | 21 | 防拼写 |
| 特殊 argv1 | 114+ | 显式内部调用 |
| UNIX symlink / Windows bat | 322+ | 让 PATH 里能直接敲到 |

**①** 分发成本：用户环境里「有一个 apply_patch 命令」但其实是同一 Codex 二进制。  
**③** 与 linux-sandbox 的 arg0 技巧同家族——**一个产物，多种人格**。

### 点 14.4 · CLI `codex apply`

把 agent 产出的 diff `git apply` 到工作树——人在回路的「接受补丁」出口，不同于模型工具路径。

```mermaid
flowchart TB
  M["模型"] --> T1["apply_patch 工具"]
  M --> T2["shell 里的 apply_patch 文本"]
  T2 --> I["intercept_apply_patch"]
  T1 --> H["ApplyPatchHandler"]
  I --> H
  H --> O["Orchestrator"]
  O --> P["apply-patch crate 落盘"]
  U["用户"] --> A["codex apply"] --> G["git apply"]
```

---

<h2 id="ch15">第 15 章 沙箱三平台与 Landlock 规则</h2>

### 点 15.1 · SandboxType 与平台选择

`codex-rs/sandboxing/src/manager.rs:34-73`：

```rust
None | MacosSeatbelt | LinuxSeccomp | WindowsRestrictedToken
```

`get_platform_sandbox`：macOS→Seatbelt；Linux→Seccomp；Windows 看开关→Token 或 None。

### 点 15.2 · SandboxablePreference

`Auto | Require | Forbid`（54–58）——工具可声明自己要不要沙箱。

### 点 15.3 · transform 边界（注释 107–110）

`SandboxExecRequest` 只在执行边界（exec-server / 等价处）从 PathUri 转原生路径。编排层保持 URI——**远程执行与本地执行同一权限模型**。

### 点 15.4 · Linux Landlock（`codex-rs/linux-sandbox/src/landlock.rs`）

> ⚠️ **核查修正（2026-08-04）**：原稿把 Landlock 讲成 Linux 现役的**文件系统**沙箱，这是**框架倒置**。源码自述 Landlock 的文件系统规则集当前**未启用**（遗留/回退路径），真正在跑的 Linux 文件系统沙箱是 **bubblewrap**（见 15.7）。

`landlock.rs:41` 原文：「Filesystem restrictions are intentionally handled by bubblewrap.」`:135-136`：「this is currently unused because filesystem sandboxing is performed via bubblewrap. It is kept for reference and potential fallback use.」

所以 Landlock 模块里**真正生效**的是网络/进程侧的 seccomp 约束（`PR_SET_NO_NEW_PRIVS` + 网络过滤，`landlock.rs:42-88`），而非 FS ruleset（`:144-158` 的 `/` 只读规则当前 dead code）。读这份代码时不要把 `install_filesystem_landlock_rules_on_current_thread` 当成主路径。

### 点 15.5 · macOS Seatbelt

根 `AGENTS.md:8-10`：子进程带 `CODEX_SANDBOX=seatbelt`；测试在 Seatbelt 下会 early exit 某些用例。  
**沙箱是测试矩阵的一维，不是可选插件。**

### 点 15.6 · Windows

`codex-rs/core/src/windows_sandbox.rs`：elevated setup、private desktop、模式持久化、metrics。非 Windows 目标直接 bail。

### 点 15.7 · bwrap（Linux 文件系统沙箱主实现）

`codex-rs/sandboxing/src/bwrap.rs`：bubblewrap 才是 Linux **文件系统**沙箱的主实现（Landlock FS 规则集已退为遗留/回退）。`bwrap.rs:24` 在系统 bwrap 缺 user namespace 时会警告——少数环境无法走 bubblewrap 时才可能回退到 Landlock FS 路径。**阅读顺序应与 15.4 对调**：先看 bwrap，再看 Landlock 的「未启用」注释，避免误以为 Landlock 在扛 FS 隔离。**（核查修正 2026-08-04）**

### 点 15.8 · 与审批正交（再强调）

| 层 | 问题 |
|---|---|
| AskForApproval | 人同不同意 |
| SandboxType | 内核允不允许 |

两边都过，副作用才发生。

---

<h2 id="ch16">第 16 章 AskForApproval 与审批缓存</h2>

### 点 16.1 · 四档语义（`codex-rs/protocol/src/protocol.rs:908-931`）

| 档 | 一句话 |
|---|---|
| UnlessTrusted / untrusted | 只放过已知安全只读；其余问人 |
| OnRequest（默认） | 模型决定何时请求 |
| Granular | 分类允许/拒绝；可禁弹窗但变 Forbidden |
| Never | 不问；失败回模型（CI 友好） |

### 点 16.2 · 审批缓存 with_cached_approval（71–117）

- key 可序列化  
- 全部 `ApprovedForSession` 才跳过  
- 批准后按 key 写入，子集请求可复用  

### 点 16.3 · 网络审批

orchestrator 还挂 `begin_network_approval` / deferred 模式——网络不是 FS 沙箱的附赠，是另一条审批带。

### 点 16.4 · 场景剧本

| 场景 | 建议组合 |
|---|---|
| 交互日常 | OnRequest + 平台默认沙箱 |
| CI | Never + Restricted FS + 无网络或代理 |
| 高敏感 | UnlessTrusted + denied-read 列表 |
| 自动化但要审计 | Granular 打开关键类，关掉危险类 |

---

<h2 id="ch17">第 17 章 AGENTS.md 发现算法逐行</h2>

### 点 17.1 · 常量与分隔符（`codex-rs/core/src/agents_md.rs:38-44`）

- `DEFAULT_AGENTS_MD_FILENAME = "AGENTS.md"`  
- `LOCAL_AGENTS_MD_FILENAME = "AGENTS.override.md"`  
- 用户说明与项目文档之间：`\n\n--- project-doc ---\n\n`  

### 点 17.2 · load_project_instructions（53–81）

对每个 turn environment：

1. 取 `ExecutorFileSystem`（可为远程）  
2. `read_agents_md`  
3. 错误只打 log，不毁掉整次加载  
4. 非空才 Some  

**③** 多环境（本地 + 远端 exec-server）可以各自有说明书。

### 点 17.3 · 字节预算：一条**先到先得、会饿死后来者**的链

这是整章最容易被读漏、但后果最实在的机制。默认值先说清楚（`codex-rs/config/src/config_toml.rs:68`）：

```rust
pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;   // 32 KiB
```

`codex-rs/core/src/config/mod.rs:207` 把它别名成 `AGENTS_MD_MAX_BYTES`，`:4071` 兜底。`0` 则整个功能禁用（`agents_md.rs:95-99`）。

**关键在于这 32 KiB 不是「每个文件的上限」，而是整条链共享的一个额度**（`agents_md.rs:107-143`）：

```python
remaining = max_total                      # :107
for p in paths:                            # paths 已经是 root → cwd 顺序
    if remaining == 0: break               # :111-113  额度耗尽，后面的文件根本不读
    data = read(p)
    if size > remaining:
        data.truncate(remaining)           # :122-124  跨过额度的那个文件被【拦腰截断】
        warn("project doc exceeds remaining budget; truncating")
    text = utf8_lossy(data)
    if text.strip():                       # :133  空白文件不计入
        entries.push(...)
        remaining -= len(data)             # :144  只有非空内容才扣额度
```

**三个非直觉的后果**：

| 行为 | 后果 |
|---|---|
| `remaining` 沿 **root → cwd** 递减 | **仓库根的 AGENTS.md 优先吃额度**；越深的目录越可能吃不到 |
| 跨过额度的文件被 `truncate` 而不是跳过 | 你会拿到**半截说明书**——而且是从中间断的，可能正好切掉最重要的那条规则 |
| 扣减写在 `if !text.trim().is_empty()` 里面 | **空白/纯空行的 AGENTS.md 不消耗额度**，放着不碍事 |

> ⚠️ **实战陷阱**：一个 monorepo，根 `AGENTS.md` 写了 30 KB 的通用规范，`apps/api/AGENTS.md` 写了 5 KB 的服务专属规则——
> **后者只会被读进 2 KB，剩下 3 KB 静默丢失**，日志里只有一条 `warn`。
> 你会觉得"模型怎么不守我们服务的规矩"，但问题不在模型。
>
> **④检查**：
> - [ ] 你的根 AGENTS.md 是不是已经接近 32 KiB？
> - [ ] 越具体、越该赢的规则，是不是恰好在链的末端（最容易被截断的位置）？
> - [ ] 出问题时先看有没有 `project doc exceeds remaining budget; truncating` 这条 warn

### 点 17.3b · 同一目录下谁赢：candidate_filenames

`agents_md.rs:234-248` 决定**每个目录**试哪些文件名、按什么顺序：

```rust
names.push(LOCAL_AGENTS_MD_FILENAME);      // "AGENTS.override.md"   ← 先
names.push(DEFAULT_AGENTS_MD_FILENAME);    // "AGENTS.md"
for candidate in &config.project_doc_fallback_filenames {   // 用户可配的后备名
    if !candidate.is_empty() && !names.contains(&candidate) { names.push(candidate); }
}
```

探测时 `return Ok(Some(candidate))` —— **每个目录只取第一个命中，不叠加**（`:220-227`）。

所以「override」的准确语义是：**同目录内 `AGENTS.override.md` 会让 `AGENTS.md` 完全失效**（不是合并、不是追加），但**不影响其它目录**——链上别的层照常参与。

> 📌 这和 Claude Code 的 `CLAUDE.local.md` 心智不同：那边是多来源叠加，这边是**每层单选 + 层间串联**。

### 点 17.3c · 出口有两套文本：legacy vs environment-labeled

`LoadedAgentsMd` 存的是**带出处的条目列表**（`entries: Vec<InstructionEntry>`，每条带 `InstructionProvenance`），不是一个拼好的字符串。真正拼装在 `text()`（`:310-316`）：

```rust
pub fn text(&self) -> String {
    if self.has_multiple_project_environments() { self.environment_labeled_text() }
    else { self.legacy_text() }
}
```

**多环境（本地 + 远端 exec-server 同时在场）时会切到带环境标签的版本**——因为此时"这条规则属于哪台机器"变成了必须表达的信息。单环境则走 legacy。

**分隔符只在一个地方出现**（`:333-337`，注释写得很直白）：

```rust
// The project-doc marker tells the model where workspace-scoped
// instructions begin, so it is only needed on the transition
// from user or internal instructions to project instructions.
let separator = if is_project && !previous_was_project { AGENTS_MD_SEPARATOR } else { "\n\n" };
```

> 🔑 **务必别记错**：`--- project-doc ---` **不是链上每个 AGENTS.md 之间的分隔符**。
> 它只出现**一次**——在「用户/内部指令」切换到「项目指令」的那个交界处。
> 链上相邻两个项目文档之间用的是普通的 `\n\n`。
>
> **③为什么**：这个标记是给模型的**语义路标**（"从这里开始是这个工作区的规矩"），不是排版分隔线。放多了反而稀释它的信号。

### 点 17.4 · agents_md_paths（155–220）逐步

1. 合并非 Project 层 config，解析 `project_root_markers`（默认 `.git`）  
2. `find_nearest_ancestor_with_markers` 找项目根  
3. 从 cwd 走到 root，dirs 列表 **reverse** 成 root→cwd  
4. 无根则只搜 cwd  
5. 对每个目录，按 `candidate_filenames` 探测文件（override 优先逻辑在 candidate 列表里）  
6. 并发探针上限 `MAX_CONCURRENT_ANCESTOR_PROBES = 256`（49）  

### 点 17.5 · 与 Claude Code CLAUDE.md 对照

| | CLAUDE.md | AGENTS.md（Codex） |
|---|---|---|
| 方向 | 多来源叠加（managed/user/项目…） | root→cwd 链 + 用户 instructions |
| 覆盖 | @导入等 | `AGENTS.override.md` |
| 根判定 | 产品规则 | `project_root_markers` 可配 |
| 远程 FS | 视实现 | 一等（ExecutorFileSystem） |

### 点 17.6 · 狗食递归

openai/codex 仓库自己的根 `AGENTS.md` 约束「改这个仓库的 agent」——Seatbelt 环境变量、别动沙箱常量、core 膨胀政策、上下文六条。  
**产品加载器与工程宪法同名同构。**

```mermaid
flowchart TB
  CWD["cwd"] --> UP["向上找 markers"]
  UP --> ROOT["project root"]
  ROOT --> DOWN["root→cwd 目录列"]
  DOWN --> F["每层 AGENTS.md / override"]
  F --> CAT["拼接 + 字节顶"]
  CAT --> CTX["注入模型上下文"]
```

---

<h2 id="ch18">第 18 章 Skills · Plugins · Hooks · 子 Agent</h2>

### 点 18.1 · Skills / Plugins 进入 turn 的时机

`run_turn` 在采样前 `build_skills_and_plugins`（定义 @720，调用 @220）。失败 → 整 turn `Ok(None)`。  
`prepare_tool_recommendations` / `built_tools` 再处理 tool_suggest 与 connector 合并。

### 点 18.2 · Hooks 生命周期

| 时机 | 函数线索 |
|---|---|
| Session start | `run_pending_session_start_hooks` |
| 记录用户输入 | `run_hooks_and_record_inputs` |
| Pre tool | registry `run_pre_tool_use_hooks` |
| Post tool | `run_post_tool_use_hooks` |
| Turn stop | `run_turn_stop_hooks`（可 block 续跑） |
| Legacy after agent | `run_legacy_after_agent_hook` |

Stop hook 能 **挡住结束并注入续跑 prompt**（第 7 章）——hooks 是控制面，不是日志。

### 点 18.3 · MCP 命名：前缀是 legacy、去重靠 SHA-1

这块比"加个前缀"复杂得多，值得摊开。

**① 前缀不是无条件的**（`codex-rs/codex-mcp/src/tools.rs:22`）：

```rust
const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
```

注意常量名里的 **LEGACY**。加不加由 `Config::prefix_mcp_tool_names()` 决定（`codex-rs/core/src/config/mod.rs:1756-1759`）：

```rust
pub(crate) fn prefix_mcp_tool_names(&self) -> bool {
    !self.features.enabled(Feature::NonPrefixedMcpToolNames)
        || self.non_prefixed_mcp_tool_servers.is_some()
}
```

读法：**默认加前缀**；要去掉必须开 `Feature::NonPrefixedMcpToolNames`；但只要配了 `non_prefixed_mcp_tool_servers`（按服务器豁免的名单），就**又回到全局加前缀 + 名单内单独豁免**的模式（`tools.rs:141`）。

**② 模型看到的名字有硬约束**（`tools.rs:107-109` 的 doc comment）：

> Raw MCP server/tool names are kept on each `ToolInfo` for protocol calls, while `callable_namespace` / `callable_name` are **sanitized and, when necessary, hashed** so every model-visible name is **unique and <= 64 bytes**.

`MAX_TOOL_NAME_LENGTH = 64`（`:226`）。

**sanitize 本身比想象中粗暴**（`codex-rs/codex-mcp/src/mcp/mod.rs:450-465`）：

```rust
for c in name.chars() {
    if c.is_ascii_alphanumeric() || c == '_' { sanitized.push(c); }
    else { sanitized.push('_'); }
}
if sanitized.is_empty() { "_".to_string() } else { sanitized }
```

保留集只有 **`[A-Za-z0-9_]`**。两个后果值得记：

| 输入 | 结果 |
|---|---|
| `repo-tools`（连字符） | `repo_tools` —— **`-` 也保不住** |
| `仓库管理`（中文） | `____` —— **每个汉字各变一个下划线** |
| 全部非法字符 | `"_"` |

> ⚠️ **中文命名的 MCP server 会出事**：`仓库管理` 和 `部署运维` 都是四个字，sanitize 之后**变成完全一样的 `____`**，于是被判撞名，各自挂上哈希后缀，模型最终看到 `mcp_______f61079/____` 这种东西——**完全丧失可读性**，工具选择质量随之下降。
>
> **④检查**：接 MCP server 时，server / namespace / tool 名一律用 ASCII，并且别用连字符。

**③ 去重是两轮、且用内容哈希而不是序号**（`:153-195`）：

| 轮次 | 检测什么 | 撞了怎么办 |
|---|---|---|
| 第一轮 | 同一个 `callable_namespace` 底下映射到**多个不同的原始身份** | `append_namespace_hash_suffix`——给 namespace 追加哈希后缀 |
| 第二轮 | 同一个 `(namespace, name)` 对应**多个原始身份** | `append_hash_suffix`——给 name 追加哈希后缀 |

哈希是 **SHA-1 取前若干位**（`:237-246`）：

```rust
let mut hasher = Sha1::new();
hasher.update(s.as_bytes());
...
format!("_{}", &hash[..CALLABLE_NAME_HASH_LEN])
```

**④ 最后按 `raw_tool_identity` 排序**（`:196`）再产出。

> 🔑 **为什么用哈希而不是 `_1` `_2` 序号**：序号依赖**遍历顺序**——今天 serverA 先连上就叫 `foo`，明天 serverB 先连上它就叫 `foo`，模型看到的工具名会在两次会话之间漂移，**破坏 prompt cache 也破坏模型的工具记忆**。
> 哈希由**内容**（`server_name \0 callable_namespace \0 connector_id` 拼成的 `raw_namespace_identity`）决定，**同一个工具永远是同一个名字**，与连接顺序无关。末尾那次显式 `sort_by(raw_tool_identity)` 是同一个动机的第二道保险。
>
> **这条设计原则可以直接搬走**：*凡是要给模型稳定暴露的标识符，去重后缀必须由内容决定，不能由顺序决定。*
>
> **④检查**：你看到工具名尾部有个 `_a1b2c3` 时，不要以为是随机的——那是撞名的证据，说明你接了两个同名 server/tool。

### 点 18.4 · 子 Agent 注册表（`codex-rs/core/src/agent/registry.rs`）

- 限制同会话子 thread 数  
- nickname + “the 2nd” 后缀  
- `exceeds_thread_spawn_depth_limit`  
- SessionSource::SubAgent 携带 depth  

**④检查**：递归 spawn 是否被 depth 卡住？nickname 冲突时是否可读？

### 点 18.5 · ModeKind（`codex-rs/protocol/src/config_types.rs:630`）

**这里容易读错，说清楚：枚举有 4 个变体，但只有 2 个对外可见。**

```rust
pub enum ModeKind {
    Plan,
    #[default]
    #[serde(alias = "code", alias = "pair_programming",
            alias = "execute", alias = "custom")]
    Default,
    #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)]
    PairProgramming,          // 隐藏变体
    #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)]
    Execute,                  // 隐藏变体
}
```

| 你在配置里写 | 实际落到哪个变体 |
|---|---|
| `plan` | `Plan` |
| `default` / **`code`** / **`pair_programming`** / **`execute`** / **`custom`** | **全部落到 `Default`** |
| —— | `PairProgramming` / `Execute` 无法从配置到达（`skip_deserializing`） |

**关键区分**：`code`、`execute` 这些是**挂在 `Default` 上的 serde 别名**，不是独立模式。
而 `PairProgramming` / `Execute` 是**真实存在但被藏起来**的变体——它们 `skip_serializing` + `skip_deserializing` + `schemars(skip)` + `ts(skip)`，即不进 JSON、不进 JSON Schema、不进 TS 类型定义。

**「只有两个模式」这件事是由一个常量钉死的**（`:653`）：

```rust
pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];
```

**③为什么这么做**：把「历史上存在过的模式」保留为变体（老 rollout 反序列化、内部代码路径还可能引用），但**同时切断它们的所有对外表面**。这是一个渐进废弃的标准手法——比直接删掉变体安全，比留着让用户能配到要干净。

**④检查**：你看到 `mode = "execute"` 时，不要以为进了 `Execute` 变体——它进的是 `Default`。

---

<h2 id="ch19">第 19 章 TUI · Exec · Doctor</h2>

### 点 19.1 · TUI 是默认脸

无子命令进 TUI。`AGENTS.md` 对 `chatwidget.rs` 等中心文件：**少加方法、请拆模块、目标 <500 LoC**。  
读 TUI 要按「编排文件 vs 功能模块」分，不要在 1400 行 app.rs 里找业务真相——业务在 core。

### 点 19.2 · Exec 非交互

`codex exec` → `codex_exec::Cli`。典型配对：`AskForApproval::Never` + 紧沙箱。  
文档 `docs/exec.md` 外链官网。

### 点 19.3 · Doctor

诊断安装、config、auth、runtime。出问题先 `codex doctor` 再读源码。

### 点 19.4 · Debug 子命令

`debug models` / `debug prompt-input` / app-server 消息——把「模型看得见什么」打印出来，是上下文纪律的调试出口。

---

<h2 id="ch20">第 20 章 App-server · MCP · Rollout</h2>

### 点 20.1 · App-server = 给编辑器的插座

一簇 crate：`app-server`、`protocol`、`daemon`、`client`、`transport`…  
CLI：`app-server`、`remote-control`、平台 `app`。  
根 `AGENTS.md` 有 app-server API 最佳实践专节——**破坏性变更要搜这些面**。

### 点 20.2 · MCP 双角色

| 角色 | 入口 |
|---|---|
| 客户端 | `codex mcp` 管理外部 servers |
| 服务端 | `mcp-server` stdio 暴露自己 |

Turn 内 lazy 唤醒 required servers（第 6 章）。

### 点 20.3 · Rollout 会话资产（codex-rs/rollout/src/lib.rs）

- 目录：`sessions` / `archived_sessions`  
- 压缩、物化引用、搜索 snippet、state_db  
- 交互来源：Cli、VSCode、自定义 atlas/chatgpt  
- CLI：`resume`/`fork`/`archive`/`delete`  

**③** 会话是文件系统上的一等资产，备份/分叉/审计都建立在这套 API 上。

### 点 20.4 · 与「服务器即本体」的 OpenCode 对照

| | OpenCode | Codex |
|---|---|---|
| 本体隐喻 | HTTP 服务器 | core 库 + 多前端 |
| IDE | 客户连 server | app-server 协议簇 |
| 本地默认 | TUI 嵌 server | TUI 直驱 core |

---

<h2 id="ch21">第 21 章 认证与模型通道</h2>

### 点 21.1 · 登录三件套（login/）

| 方式 | 线索 |
|---|---|
| ChatGPT OAuth/PKCE | server.rs / pkce.rs |
| Device code | `device_code_auth.rs`（`run_device_code_login` @234） |
| API Key | CLI `run_login_with_api_key`；env 遥测区分 OPENAI_API_KEY / CODEX_API_KEY |

### 点 21.2 · 商业闭环

推荐 ChatGPT 套餐登录——与 Claude Code「订阅」叙事对称：CLI 是套餐权益的本地表面。

### 点 21.3 · 模型与工具协议

采样走 Responses API 形状（ToolSpec 注释链到 platform.openai.com）。  
Workspace 含 `ollama`、`lmstudio` 等 crate——**本地/OSS 路径存在**，默认路由以运行时 config / `codex debug models` 为准，本文不钉死易变的默认 slug。

### 点 21.4 · previous-model compact 的认证前提

第 9 章：只有 Codex backend + OpenAI provider 才做 previous-model 兼容压缩。  
换第三方 provider 时，不要指望这套行为。

---

<h2 id="ch22">第 22 章 独特设计、取舍、读法、对照</h2>

### 点 22.1 · 三个最独特的设计（展开讲）

**A. OS 沙箱一等公民**  
不是提示词写「请小心」，而是 Seatbelt/Landlock/Token + denied-read 与抬权冲突时保拒绝。  

**B. 审批×沙箱×execpolicy 三层合成**  
Orchestrator 把人审、内核、命令策略合成 `SandboxAttempt`；Granular 关弹窗→Forbidden 防误解。  

**C. 上下文宪法进仓库**  
六条 + Fragment 类型 + `debug prompt-input`（真实命令为 `run_debug_prompt_input_command` → `codex_core::build_prompt_input`，全仓无 `debug_input` 标识符——核查修正 2026-08-04）。  

### 点 22.2 · 三处必须知道的取舍

1. **core/turn 仍巨** — 政策抵制膨胀，历史债还在；读法是「按函数跳」不是线性通读。  
2. **用户文档外置** — `docs/` 多跳转；离线只靠源码与本分析。  
3. **工具面偏 shell+patch** — 与部分模型分布对齐；细粒度导航教学不如 CC 图鉴友好，要靠沙箱兜 shell 过宽。  

### 点 22.3 · 建议阅读顺序（加深版）

1. `cli/src/main.rs` 子命令图（30 分钟）  
2. `codex-rs/core/src/tasks/regular.rs` 全文（15 分钟）  
3. `codex-rs/core/src/session/turn.rs:153-520` 进门+停机（2 小时）—注意仓库有 3 个同名 `turn.rs`，正主是 `core/src/session/` 这个  
4. `turn.rs:1308-1405` + `2165-2260` 采样外壳（1 小时）  
5. `orchestrator.rs` 头注释 + ``codex-rs/core/src/tools/sandboxing.rs:156-300``（2 小时）  
6. `handlers/shell.rs:63-220`（1 小时）  
7. `agents_md.rs:1-220`（1 小时）  
8. `manager.rs` SandboxType + `landlock.rs` 规则安装（1 小时）  
9. 对照《Claude-Code-vs-OpenCode-vs-Codex-深度对比》  

### 点 22.4 · 选型骨感

```mermaid
flowchart TB
  Q["本地编程 Agent"] --> A["闭源极致体验"]
  Q --> B["开源多模型厨房"]
  Q --> C["开源官方 + OS 沙箱"]
  A --> CC["Claude Code"]
  B --> OC["OpenCode"]
  C --> CX["Codex CLI"]
```

### 点 22.5 · 未知清单（加深后仍诚实）

| 项 | 状态 |
|---|---|
| Seatbelt profile 生成细节 | 需继续下钻 macOS 专用模块（本版未逐行拆 plist） |
| App-server 全 RPC 表面 | 边界已定，方法表未全录 |
| 默认模型 slug | 易变；用 debug models |
| Cloud tasks 协议 | 标实验，未深挖 |
| memories 读写完整产品语义 | crates 存在，未当主线展开 |

---

## 附录 A · 「一点一页」索引（按主题跳读）

| 你想搞懂 | 去章节 |
|---|---|
| 两层 loop | 第 5–7 章 |
| 为什么停不下来 / 突然停了 | 第 7 章停机表 |
| 流式重试 | 第 8 章 |
| 压缩何时发生 | 第 9 章 |
| 能不能改 history | 第 10 章 |
| 工具从哪来 | 第 11 章 |
| 审批和沙箱谁先谁后 | 第 12 章 |
| shell 抬权被拒 | 第 13 章 |
| patch 多文件只问一次 | 第 14、16 章 |
| Linux 只能写哪些目录 | 第 15 章 |
| AGENTS.md 为啥没读到 | 第 17 章 |
| stop hook 续跑 | 第 7、18 章 |
| 会话怎么备份 | 第 20 章 |
| CI 怎么配 | 第 16.4 节 |

## 附录 B · 源码路径速查

| 主题 | 路径 |
|---|---|
| CLI | `codex-rs/cli/src/main.rs` |
| RegularTask | `codex-rs/core/src/tasks/regular.rs` |
| run_turn | `codex-rs/core/src/session/turn.rs:153` |
| run_sampling_request | 同文件 `:1308` |
| try_run_sampling_request | 同文件 `:2165` |
| Orchestrator | `codex-rs/core/src/tools/orchestrator.rs` |
| 审批/沙箱 traits | `codex-rs/core/src/tools/sandboxing.rs` |
| Shell | `codex-rs/core/src/tools/handlers/shell.rs` |
| ApplyPatch handler | `.../handlers/apply_patch.rs` |
| ToolSpec | `codex-rs/tools/src/tool_spec.rs` |
| AskForApproval | `codex-rs/protocol/src/protocol.rs:908` |
| SandboxType | `codex-rs/sandboxing/src/manager.rs:35` |
| Landlock | `codex-rs/linux-sandbox/src/landlock.rs` |
| AGENTS.md | `codex-rs/core/src/agents_md.rs` |
| arg0 | `codex-rs/arg0/src/lib.rs` |
| Rollout | `codex-rs/rollout/src/lib.rs` |
| 宪法 | `AGENTS.md` |

## 附录 C · 规模快照（bb1af23）

| 项 | 值 |
|---|---|
| workspace members | 126 |
| `.rs` 文件 | 2783 |
| turn.rs | 2741 |
| session/mod.rs | 4154 |
| orchestrator.rs | 533 |
| apply_patch handler | 666 |
| agents_md.rs | 480 |
| sandboxing manager | 715 |

---



---

# 第七部分 · 场景级逐步推演

> 前面按子系统拆点；这里按**真实用户动作**把点串成时间线。每一步都回指章节与行号。

<h2 id="ch23">第 23 章 场景：交互会话里执行 `rm -rf build`</h2>

### 故事

你在 TUI 说：「删掉 build 目录」。模型决定调用 shell。

### 逐步推演

| 步 | 发生什么 | 回指 |
|---|---|---|
| 1 | 无子命令，进 TUI，提交 Op | 第 3、4 章 |
| 2 | RegularTask 发 TurnStarted，调 run_turn | 第 5 章 |
| 3 | pre compact / MCP / step_context / skills | 第 6 章 |
| 4 | run_sampling_request → stream → FunctionCall(shell) | 第 8 章 |
| 5 | Shell `run_exec_like`：合并权限、不抬权则过守卫 | 第 13 章 `shell.rs:122-138` |
| 6 | `create_exec_approval_requirement_for_command` | 第 13 章 |
| 7 | Orchestrator：若 NeedsApproval → 弹窗；缓存 ApprovedForSession | 第 12、16 章 |
| 8 | 选 SandboxType（如 macOS Seatbelt） | 第 15 章 |
| 9 | SandboxAttempt 执行；若沙箱拒绝且可升级 → **不再问人**重试 | orchestrator 头注释 |
| 10 | 输出回模型；needs_follow_up？可能再采样确认 | 第 7 章 |

### 关键分叉

**A. 策略 = Never（误配在交互）**  
不问人，直接尝试；沙箱仍可能挡。失败回模型——用户可能看到模型「尝试失败」而非弹窗。

**B. 策略 = UnlessTrusted**  
`rm` 几乎必问。

**C. 请求 escalated + 非 OnRequest**  
`shell.rs:122-138` **直接 Reject**，文案教育模型别要抬权。

**D. 存在 denied-read 路径**  
即使批准抬权，`sandbox_permissions_preserving_denied_reads` 也可能强制继续沙箱（第 12.4 节）。

### 读者检查

- [ ] 你能指出「问人」发生在 orchestrator 还是 shell 前置守卫吗？（两处都有）  
- [ ] 沙箱拒绝后的升级还会不会弹窗？（按设计：不会再审）

---

<h2 id="ch24">第 24 章 场景：一次多文件 apply_patch</h2>

### 故事

模型一次 patch 改 `src/a.ts` 与 `src/b.ts`。

### 逐步推演

1. 工具可以是 `apply_patch` handler，或 shell 文本被 `intercept_apply_patch` 拦住（第 13.2、14 章）。  
2. `Approvable::approval_keys` 返回 **两个路径 key**（sandboxing 注释 316–321）。  
3. `with_cached_approval`：要 **两个 key 都已 ApprovedForSession** 才跳过；批准后分别写入。  
4. 下次只改 `a.ts` → 单 key 命中缓存 → 可不问。  
5. 落盘走 apply-patch crate 解析 Hunk。  

### 为什么这比「整次 tool call 一个布尔」好？

| 方案 | 问题 |
|---|---|
| 整次批准 | 下次改无关文件也免问，过宽 |
| 每文件 key | 粒度对齐真实风险；子集可复用 |

### 对照 Claude Code

CC 的 Edit 工具通常单文件语义；Codex 显式把「多文件一次补丁」写进审批缓存契约。

---

<h2 id="ch25">第 25 章 场景：上下文将爆窗时的 mid-turn 压缩</h2>

### 故事

长会话中模型还要继续调工具，但 token_status 显示触顶。

### 逐步推演

1. sampling 返回 `model_needs_follow_up=true`（第 7.2）。  
2. `needs_follow_up` 为真；`token_limit_reached` 或 `new_context_window_request` → `should_roll_over`（419–420）。  
3. `run_auto_compact(..., MidTurn, BeforeLastUserMessage{world_state, step_context})`（431–443）。  
4. 若成功：可能再跑 session start hooks；`can_drain_pending_input = !model_needs_follow_up`；`continue` 回到 loop。  
5. 注释 430：相信压缩能拉回窗口，接受潜在多圈。  

### 与 pre-turn 压缩差别

| | PreTurn | MidTurn |
|---|---|---|
| 时机 | 采样前 | 采样后还要继续时 |
| 输入 | 尚未把本轮当重点 | 要保住 last user message 相对位置 |
| Injection | DoNotInject 等 | BeforeLastUserMessage |

### 失败

压缩抛非 Abort → emit turn error → `Ok(None)`（449–452）。用户侧表现为 turn 错误结束，而不是静默丢历史。

---

<h2 id="ch26">第 26 章 场景：stop hook 要求续跑</h2>

### 故事

模型似乎说完了（无 tool follow-up），但安装了 Stop hook 要检查「测试是否通过」。

### 逐步推演

1. `!needs_follow_up` 进入停机协议（461+）。  
2. `run_turn_stop_hooks` → `should_block=true` 且带 continuation fragments。  
3. `build_hook_prompt_message` 成功 → 记入 conversation、accept mailbox、`stop_hook_active=true`、`continue`（470–486）。  
4. 下一圈 sampling 把 hook 提示当上下文，可能再跑测试命令。  
5. 若 block 但无 prompt → Warning「ignoring the block」（488–494）。  

### 产品含义

**Hooks 可以否决「模型自觉结束」**——这是企业合规/质量门的钩子，不是 UI 装饰。

### 对照 Open Design Design Jury

OD 用会话内多回合评审；Codex stop hook 是 **本地可脚本化的门禁**。方向类似（不让轻易收工），机制不同。

---

<h2 id="ch27">第 27 章 场景：CI 中 `codex exec` + Never</h2>

### 推荐组合

- `codex exec "..."`  
- `AskForApproval::Never`  
- FS Restricted + 网络受限或代理  
- 无 TUI  

### 逐步差异（相对交互）

| 点 | 交互 | CI |
|---|---|---|
| 门面 | TUI | exec |
| 审批 | OnRequest 常见 | Never |
| 失败 | 可问人升级 | 直接回模型或失败退出 |
| 会话 | resume/fork 资产 | 常一次性 rollout |

### 风险

Never **不是**「跳过沙箱」。若有人把 SandboxType 配成 None + DangerFullAccess，CI 会变成真·裸奔——审查配置时两层都看。

---

<h2 id="ch28">第 28 章 场景：AGENTS.md 链与 override 谁赢</h2>

### 故事

仓库根有 `AGENTS.md`，`apps/api/AGENTS.override.md` 存在，cwd 在 `apps/api`。

### 逐步推演

1. markers 找到 git root。  
2. search_dirs = `[root, ..., apps/api]`（root→cwd）。  
3. 每层 candidate_filenames：override 优先于默认（第 17 章）。  
4. `apps/api` 层读到 override 文件则用它（同层）。  
5. 拼接受 `project_doc_max_bytes` 切割。  
6. 与用户 instructions 用 `--- project-doc ---` 连接。  

### 常见「读不到」原因清单

- [ ] `project_doc_max_bytes = 0`  
- [ ] cwd 在 root 外且无 markers  
- [ ] 文件名既不是 AGENTS.md 也不是 override / fallback  
- [ ] 远程 environment 的 FS 看不到本地路径  
- [ ] 内容全空白被 skip  

---

## 附录 D · 与 Claude Code / OpenCode 的「同题不同答」详表

| 题目 | Claude Code | OpenCode | Codex（本文） |
|---|---|---|---|
| 用户插话 | 产品队列 | 视实现 | RegularTask 外层 + input_queue |
| 主循环文件 | 深入版第6章 | prompt.ts | turn.rs run_turn |
| 停机 | 终止原因全集 | Goal/doom 等（MiMo 加强） | follow-up × compact × stop hook |
| 补丁 | Edit/Write | edit 或 apply_patch | apply_patch + intercept |
| 权限 | 五道闸门 | ruleset | AskForApproval × ExecApprovalRequirement × Sandbox |
| 沙箱 | 产品层为主 | 规则/可选 | OS 级三平台 |
| 说明书 | CLAUDE.md | memory/config | AGENTS.md 链 |
| 会话 | 产品存储 | DB/文件 | rollout 文件资产 |
| 开源 | 否 | MIT | Apache-2.0 |

---

## 附录 E · 术语表（读源码对照）

| 术语 | 含义 |
|---|---|
| Turn | 一次用户触发的任务单元；可含多次采样 |
| StepContext | 单步冻结的工具/配置/世界视图 |
| SamplingRequest | 一次模型流式请求 |
| ToolOrchestrator | 审批+沙箱+重试中枢 |
| SandboxAttempt | 一次带类型的执行尝试 |
| Rollout | 会话落盘与回放资产 |
| arg0 人格 | 同二进制按 argv0 分发 |
| comp_hash | 压缩兼容性哈希 |


## 结语

加深版把 Codex 从「又一个 Agent」拆成 **可逐点核对的控制系统**：外层插话、内层工具、压缩开窗、stop hook、审批三态、沙箱抬权冲突、AGENTS.md 硬预算。  
你要的不是更多形容词，而是 **每个点都能指到行号、说出取舍、列出检查清单**——本文按这个标准重写。

---

*本文基于 `openai/codex` commit `bb1af235ea2822d7a40f75ef52e4d6a2cde84da2`（2026-07-28）逐点核对写成。所有 `文件:行号` 引用均已用 `grep -n` / `sed -n` 回验，歧义文件名（`turn.rs` / `shell.rs` / `protocol.rs` / `config_types.rs` 在本仓库各有 3–6 个同名文件）一律补全 crate 路径。统计数字来自本地全量检出（`codex-rs` 下 2 783 个 `.rs`、workspace 126 members），社区数据由 GitHub REST API 于 2026-07-29 取得（102 170★ / 15 331 fork）。*

**对照阅读** → [三方深度对比](Claude-Code-vs-OpenCode-vs-Codex-深度对比.md) · [交互解析站](../openai-codex-解析.html)（5 个可运行实验台）· [总目录](../总目录.md) · [横向对比与总结评价](横向对比与总结评价.md)
