源码逐层解剖 · OpenAI Codex CLI

Codex CLI 源码架构全解剖

基于 GitHub 上 openai/codex 官方仓库 2026-07-06 的主干快照(commit 9c5be7e1),逐层拆解这个用 Rust 写成的编程 agent:你的一条消息从敲下回车,到最终回复出现在终端,中间发生的每一件事。所有结论都标注了 文件:行号,可直接对照 codex-rs/ 阅读。如果你刚读完《Claude Code 源码架构全解剖》,本文会顺带点出两者的设计差异。

Rust workspace · 123 个 crate core/src 约 4.9 万行 client.rs 2402 行 沙箱:bwrap / Seatbelt / Restricted Token

版本说明(读前必看):本文对应的是一个相当新的主干快照,与网上常见的旧版 Codex 资料有几处结构性差异:① 审批策略里的 on-failure 已退役为 on-request 的别名;② Chat Completions 协议支持已被彻底删除,只剩 Responses API;③ Linux 沙箱已从 Landlock 切换为内嵌编译的 bubblewrap(Landlock 降级为 legacy 后备);④ TUI 和 codex exec 都不再直连引擎,而是统一经由内嵌的 app-server JSON-RPC 协议层。旧文章里的 Op::UserTurncodex.rs 大文件等在这个版本里都已不存在。

00全景架构:一个二进制,五层结构

先建立整体心智模型,后面每一章都是对其中一层的放大。

Codex CLI 和 Claude Code 解决的是同一个问题——「让模型在你的电脑上安全地写代码」——但技术选型几乎完全相反:Claude Code 是 TypeScript + React 渲染到终端,Codex 是纯 Rust + ratatui;Claude Code 的安全边界主要靠「权限提示系统」,Codex 的重头戏是操作系统级沙箱(macOS Seatbelt / Linux bubblewrap+seccomp / Windows Restricted Token)。

先解释几个贯穿全文的 Rust 术语:crate 是 Rust 的包/模块单元(类似 npm 包),一个 workspace 把多个 crate 组织在一个仓库里统一构建;tokio 是 Rust 最主流的异步运行时(类似 Node.js 的事件循环,负责调度所有 async 任务);channel(通道)是异步任务之间传消息的管道,一端发送(tx)一端接收(rx)——Codex 的整个引擎就是围绕两条 channel 构建的。

仓库顶层:codex-rs/ 是 Rust workspace(全部核心实现,codex-rs/Cargo.toml:2-126 列出 123 个成员 crate);codex-cli/ 是 npm 薄壳(只负责按平台挑选原生二进制并透传信号,codex-cli/bin/codex.js:15-22);sdk/ 有 TypeScript 和 Python 两套 SDK;docs/ 的多数文档已外迁到官网、只剩重定向占位。

① 入口层cli · arg0 · exec · codex-cli(npm)

clap 解析 28 个子命令;arg0 用「busybox 技巧」让同一个二进制按 argv[0] 变身为沙箱助手 / apply_patch;npm 层只是选平台二进制的薄壳

② UI 层(ratatui)tui · ansi-escape · terminal-detection

immediate-mode 终端 UI:每帧重画底部 inline 视口,定稿的历史直接写进终端原生回滚缓冲(不用全屏 alt-screen)

↓ JSON-RPC(thread/turn/item)
③ 协议层(Codex 特色)app-server · app-server-protocol · protocol

所有前端(TUI / exec / IDE / SDK)统一经由 app-server JSON-RPC 驱动引擎;引擎内部则是 SQ/EQ(提交队列/事件队列)双通道协议

↓ Op 提交 / ↑ EventMsg 事件流
④ 引擎层(核心)core:ThreadManager → Session → Task → run_turn

submission_loop 消费 Op、spawn Task;run_turn 是真正的 agentic 循环:调模型 → 执行工具 → 回填结果 → 再调模型,直到没有工具调用

↓ 工具执行须过审批 + 沙箱
⑤ 工具与沙箱层core/tools · exec · apply-patch · sandboxing · execpolicy

工具注册表按模型现算工具面;每条 shell 命令过「execpolicy 规则 → 审批 → OS 沙箱 → 失败升权重试」四道关

↓ HTTPS / WebSocket
⑥ 模型 API 层core/client.rs · codex-api · model-provider-info · login

只支持 OpenAI Responses API(Chat Completions 已删除);WebSocket 优先、HTTP SSE 兜底;ChatGPT OAuth 或 API key 认证

0.1 123 个 crate 怎么分组

分组代表 crate职责
入口/分发cli · exec · tui · arg0 · responses-api-proxy命令行入口、无头模式、终端 UI、argv[0] 分发
核心引擎core · protocol · codex-client · codex-api · rollout · models-managerSession/Task/Turn 循环、SQ/EQ 协议类型、HTTP/SSE 传输、会话落盘、模型元数据
配置config · codex-home · features · keyring-store · secretsconfig.toml 分层加载、~/.codex 主目录、特性开关、凭据保管
沙箱/安全linux-sandbox · bwrap · sandboxing · execpolicy · shell-command · network-proxy · process-hardening三平台沙箱、命令安全规则、网络代理、进程加固
工具面tools · apply-patch · file-search · git-utils · hooks · skills · memories工具契约、补丁 DSL、文件搜索、钩子、技能、记忆
协议/服务化app-server 家族(6个) · mcp-server · rmcp-client · code-mode 家族IDE/SDK 的 JSON-RPC 面、MCP 双向、工具调用编程化
生态/云cloud-tasks · chatgpt · login · ollama · lmstudio · connectors · otelCodex Cloud 任务、OAuth 登录、本地模型、遥测

三个最重要的文件:如果只看三个文件,看 core/src/session/turn.rs(run_turn 主循环)、core/src/tools/orchestrator.rs(审批+沙箱+升权重试的编排)、core/src/client.rs(请求怎么组装和流式消费)。所有路径相对 codex-rs/

对比 Claude Code:Claude Code 是「一个 npm 包 + source map 可还原的单体 TS 应用」;Codex 是「123 个 crate 的 Rust workspace + 编译后单个原生二进制」。前者热改快、生态借力 Node;后者启动快、内存省,且能把 bubblewrap 沙箱的 C 源码直接编译进自己(bwrap/build.rs:14-19),不依赖系统组件。

01一条消息的一生 ★

这是全文的主线。从你按下回车,到回复完整出现,共 14 站。每一站在后面的章节里都有对应的放大详解。

1

回车:composer 产出 Submitted

底部输入框(自研 textarea,不是第三方组件)在 handle_key_event 里处理 Enter,返回 InputResult::Submitted { text, ... }。Shift+Enter/续行等在这层被排除。

tui/src/bottom_pane/chat_composer.rs:1622 · :274-278

2

ChatWidget 打包成 AppCommand::UserTurn

handle_composer_input_resultsubmit_user_message 把文本连同 cwd、审批策略、模型、推理力度打包成 AppCommand::UserTurn,经 UI 内部事件总线(AppEvent)交给 App 层。

tui/src/chatwidget/input_flow.rs:10-45 · input_submission.rs:65 · app_command.rs:34-47

3

App 层路由:steer 还是新 turn?

如果当前已有 turn 在跑,优先尝试 turn_steer(把新消息「转向注入」正在进行的任务,而不是排队);否则调 app_server.turn_start(...) 开新 turn。

tui/src/app/thread_routing.rs:562,578-580,653-669

4

JSON-RPC:turn/start 进入 app-server

TUI 本身只是内嵌 app-server 的一个客户端。ClientRequest::TurnStart 带着 thread_id、input、cwd、approval_policy 等参数发出——IDE 插件和 SDK 走的是完全相同的这条协议。

tui/src/app_server_session.rs:772-820 · app-server-protocol/src/protocol/common.rs:799

5

app-server 翻译成引擎协议 Op::UserInput

turn_processor 把 JSON-RPC 参数转成引擎的提交操作 Op::UserInput { items, thread_settings, ... },提交给对应线程。

app-server/src/request_processors/turn_processor.rs:522-536 · protocol/src/protocol.rs:543-555

6

入队 SQ:tx_sub.send(Submission)

Codex::submit 给操作分配唯一 id、包成 Submission,发进有界 async channel(SQ = Submission Queue,提交队列)。引擎的所有输入——用户消息、审批决定、中断——都走这一条队列,天然保序。

core/src/session/mod.rs:752-781 · protocol/src/protocol.rs:161-170

7

submission_loop 分发:造 TurnContext,spawn RegularTask

常驻的 submission_loop 收到 Op::UserInput 后,先对会话配置做快照生成本 turn 不可变的 TurnContext;若无活跃任务则 spawn_task(ctx, input, RegularTask::new())——新任务会先把旧任务以 Replaced 理由中止(一个会话同时最多一个任务)。

core/src/session/handlers.rs:703,758,260-265 · turn_context.rs:573 · tasks/mod.rs:314-323

8

TurnStarted 事件 + 用户消息入账

RegularTask 先发 EventMsg::TurnStarted(wire 名仍叫 task_started);用户消息经 hooks 处理后写进内存 history + rollout 落盘文件,并回发 EventMsg::UserMessage 让 UI 显示。

core/src/tasks/regular.rs:49-56 · session/turn.rs:481-507

9

组 Prompt,打开模型流

从 history 克隆归一化后的完整对话(for_prompt),加上按模型现算的工具表和 base_instructions,构成 PromptModelClientSession::stream() 优先走 WebSocket,失败降级 HTTP Responses API(SSE)。请求体 store:falsestream:trueprompt_cache_key=thread_id。

session/turn.rs:272-278,1112 · client.rs:1734-1786,812-892

10

SSE 事件流:文本增量实时冒泡到 UI

流上的 OutputTextDelta 被逐段转成 EventMsg::AgentMessageContentDelta 发给客户端;TUI 按「整行提交」节奏把流式 Markdown 渐进渲染进历史区(见第 8 章)。推理摘要增量、限流快照、token 用量走各自的事件变体。

session/turn.rs:2240-2271 · codex-api/src/common.rs:74-116 · tui/src/chatwidget/streaming.rs:388

11

工具调用:路由 → 审批 → 沙箱 → 执行

OutputItemDone(FunctionCall) 先把调用本身记入 history,再经 ToolRouter 按名字路由到 handler,置 needs_follow_up = true。exec 类工具统一过 ToolOrchestrator 四道关:execpolicy 规则判定 → 需要时弹审批(ExecApprovalRequest 事件 + oneshot 等待用户决定)→ 套 OS 沙箱执行 → 沙箱拒绝时按策略升权重试。工具支持并行:读锁并发、写锁独占。

stream_events_utils.rs:405-444 · tools/orchestrator.rs:134 · tools/parallel.rs:133-137

12

结果回填,循环下一轮

工具 future 放进 FuturesOrdered 有序队列,完成后 FunctionCallOutputrecord_conversation_items 同时写内存 history、rollout 落盘、回发客户端三件事。Completed 事件到达且 needs_follow_up 为真 → run_turn 循环 continue,用更新后的完整 history 再采样一次。

session/turn.rs:1853-1877,416 · session/mod.rs:2777-2794

13

终点判定:没有工具调用、没有 steer 输入

终止条件 = 模型这轮既没发 tool call、服务端也没标 end_turn:false、且用户没有中途 steer 进新输入。满足则跑 Stop hooks(可以阻止停止逼模型继续),取最后一条 assistant 消息 break 出循环。

session/turn.rs:318,371-415 · stream_events_utils.rs:443

14

收尾:TokenCount → TurnComplete → UI 恢复输入框

任务收尾时 flush rollout、发 EventMsg::TokenCount(token 用量+限流快照)和 EventMsg::TurnComplete;app-server 的事件泵把它翻译成 v2 通知 turn/completed;TUI 收到后合并流式渲染单元为可重排的 Markdown cell、收起工作指示、恢复输入框。

tasks/mod.rs:409-433,748-770 · app-server/src/request_processors/thread_lifecycle.rs:302-309 · tui/src/chatwidget/protocol.rs:67,232-250

对比 Claude Code:两者的 agentic 循环本质相同(「有 tool call 就再来一轮」),但消息进引擎的路径不同——Claude Code 是 UI 直接调用 query() 生成器函数(进程内函数调用);Codex 多了两跳:UI → JSON-RPC(app-server)→ SQ 队列(channel)。多这两跳换来的是任何前端(IDE/SDK/CI)都能用同一协议驱动引擎,以及输入天然串行化。

02启动流程:进程启动 → TUI 出现

codex 命令敲下到界面出现,经过 npm 薄壳、arg0 分发、clap 解析、配置加载、onboarding、事件循环六步。

2.1 npm 薄壳:选平台二进制

如果你用 npm install -g @openai/codex 安装,实际执行的是 codex-cli/bin/codex.js:维护「平台 → 子包」映射(如 linux x64 → @openai/codex-linux-x64codex.js:15-22),用 require.resolve 找到平台包里的原生二进制(codex.js:79-105),然后 spawn(binaryPath, argv, {stdio:'inherit'}) 启动并透传 SIGINT/SIGTERM 信号、复现退出码(codex.js:167-200)。一切逻辑都在 Rust 里,JS 层零业务。

2.2 arg0 多调用分发:一个二进制的多重身份

Rust 侧真正的 main()cli/src/main.rs:956-962)不直接跑业务,而是先包一层 arg0_dispatch_or_else。这是经典的 busybox 技巧:同一个物理二进制,根据「自己被以什么名字调用」(argv[0])决定扮演什么角色:

arg0/src/lib.rs:93-98(节选改排)
if exe_name == "codex-linux-sandbox" { codex_linux_sandbox::run_main(); // 变身 Linux 沙箱助手(never returns) } if exe_name == "apply_patch" || exe_name == "applypatch" { // 变身补丁应用器 —— 模型输出的 `apply_patch` 命令直接可执行 }

正常启动路径上还有两件事值得注意(arg0/src/lib.rs:292-312,327-437):① 加载 ~/.codex/.env过滤一切 CODEX_ 前缀变量防注入;② 在 $CODEX_HOME/tmp/arg0/ 建临时目录,对自身可执行文件做 apply_patchcodex-linux-sandbox 等名字的 symlink 并 prepend 进 PATH——这样模型在 shell 里写 apply_patch <<'EOF'... 天然可执行,Linux 沙箱也能 re-exec 出助手进程。最后在一个 16 MiB 栈的专用线程上构建 tokio 运行时(arg0/src/lib.rs:208-237)。

2.3 clap 解析:28 个子命令

clap 是 Rust 事实标准的命令行解析库(用 derive 宏把 struct 变成 CLI 定义)。顶层 MultitoolClicli/src/main.rs:94-121)把 TUI 参数平铺进根命令、子命令设为可选——所以 codex "修个 bug"(无子命令)直接进交互 TUI(main.rs:988-1001),而 codex exec / codex login / codex mcp / codex app-server / codex resume / codex cloud / codex apply / codex sandbox 等 28 个子命令(main.rs:123-212)各走各路。

2.4 TUI 启动链:run_main 的十道工序

a

危险旁路映射与参数整备

--dangerously-bypass-approvals-and-sandbox 被映射为「全放行沙箱 + 永不审批」(tui/src/lib.rs:857-861);解析 -c 覆盖、定位 ~/.codex、处理 --profile(Profile v2 = 在用户配置之上叠一个 <name>.config.tomllib.rs:902-906)。

b

配置加载(分层合并)

load_config_or_exit 得到正式 Configlib.rs:1064-1071)。配置分 7 层按优先级合并:MDM 托管 → 系统 → 企业托管 → 用户(~/.codex/config.toml)→ 项目(.codex/)→ 命令行 -c → legacy(config/src/config_layer_source.rs:6-46)。对比 Claude Code 的五层 settings,思路一致。

c

安全与登录检查

execpolicy 规则文件校验(语法错直接 exit 1,lib.rs:1145-1154)→ 登录限制 → OTel 遥测初始化。

d

run_ratatui_app:终端初始化 + 内嵌 app-server

tui::init() 建 inline 视口终端(lib.rs:1313-1320)→ 启动进程内 app-server 包成 AppServerSessionlib.rs:1348-1370)——TUI 从此刻起就是自己引擎的「客户端」。

e

Onboarding:欢迎 / 登录 / 信任目录

should_show_onboarding 判定后跑一个三步状态机(Welcome → Auth → TrustDirectory,tui/src/onboarding/onboarding_screen.rs:54-58):ChatGPT 浏览器登录或 API key,然后询问是否信任当前目录。登录后重载配置(lib.rs:1441-1468)。

f

App::run:进入主事件循环

resume/fork 选择器(如指定)→ app_server.bootstrap() 与启动期 hooks 并行(tokio::join!lib.rs:1766-1769)→ App::run 建事件通道进入 select! 循环(tui/src/app.rs:759,1169)——界面出现。

2.5 AGENTS.md 什么时候被读

Codex 的「项目记忆文件」是 AGENTS.md(对应 Claude Code 的 CLAUDE.md)。发现算法在 core/src/agents_md.rs:1-16 头注释写明:以 project_root_markers(默认 .git)向上找到项目根,然后从项目根到 cwd 沿途收集所有 AGENTS.md 顺序拼接(不越过项目根);支持 AGENTS.override.md 本地覆盖(agents_md.rs:38-39)。用户级全局指令来自 $CODEX_HOME/AGENTS.mdcodex-home/src/instructions/mod.rs:9-26),合并时用户层在前、项目层在后,以 --- project-doc --- 分隔(agents_md.rs:43-56)。

process-hardening:持有敏感凭据的进程(responses-api-proxy、linux-sandbox)会在 main 之前经 #[ctor] 执行加固(process-hardening/src/lib.rs:7-25):禁 core dump、禁 ptrace attach(防止其他进程 dump 出内存里的 API key)、清除 LD_PRELOAD 等可注入代码的环境变量。这是 TS 实现做不到的系统级防御。

03Agentic 主循环:Session / Task / Turn

整个产品的心脏。Claude Code 的心脏是一个 1729 行的 query.ts;Codex 把同样的逻辑拆成了一条清晰的对象链和两层嵌套循环。

3.1 持有关系:谁管着谁

core 内核对象链
ThreadManager [thread_manager.rs:182] 进程级工厂+注册表:HashMap<ThreadId, CodexThread> └─ CodexThread [codex_thread.rs:160] 一个会话线程的外壳(旧称 conversation) └─ Codex [session/mod.rs:389] SQ/EQ 队列对:tx_sub 发提交、rx_event 收事件 └─ Arc<Session> [session/session.rs:28] 运行时骨架:事件出口、活跃任务、输入队列、服务 ├─ SessionState [state/session.rs:26] Mutex 保护的可变数据:history、限流、压缩窗口 └─ TurnContext [session/turn_context.rs:102] 每 turn 快照生成、turn 内不可变

几个 Rust 概念:Arc 是原子引用计数的共享指针(多个异步任务共享同一对象);Mutex 是互斥锁(保证同一时刻只有一个任务改数据);trait 类似接口。Session 的注释直说设计约束:「一个会话同一时刻最多一个运行中的任务,且可被用户输入打断」(session/session.rs:27)。注意 SessionState 里的 history: ContextManager 才是对话历史的真正持有者(state/session.rs:26-47)。

3.2 SQ/EQ 协议:引擎的唯一入口和唯一出口

引擎与外界的全部交互收敛为两条 channel(session/mod.rs:538-539):SQ(Submission Queue)Submission { id, op }EQ(Event Queue)Event { id, msg }(事件的 id 关联回触发它的提交)。这是一个刻意的「协议化」设计——引擎不知道也不关心对面是 TUI、IDE 还是 CI 脚本。

Op 变体(输入)含义位置
UserInput用户消息(含附加上下文、输出 schema、thread 设置覆盖)protocol.rs:543
Interrupt中断当前任务(保留后台终端进程)protocol.rs:518
ExecApproval / PatchApproval用户对命令/补丁审批的决定protocol.rs:573,583
Compact手动触发上下文压缩protocol.rs:640
ThreadRollback回滚最近 N 个 turnprotocol.rs:652
Review / Shutdown / RunUserShellCommand代码评审任务 / 关闭 / 用户直发 shell(TUI 的 ! 模式)protocol.rs:655,661,668
EventMsg 变体(输出)含义位置
TurnStarted / TurnCompleteturn 生命周期(wire 序列化名仍是 task_started/task_complete,历史兼容)protocol.rs:1311,1320
AgentMessageContentDelta / AgentMessage流式文本增量 / 完整消息protocol.rs:1439,1327
AgentReasoning推理(thinking)摘要protocol.rs:1333
ExecCommandBegin / ExecCommandEnd命令开始/结束(UI 画执行卡片)protocol.rs:1366,1374
ExecApprovalRequest / ApplyPatchApprovalRequest请求用户审批(弹窗)protocol.rs:1379,1391
TokenCounttoken 用量 + 限流快照(UI 剩余上下文百分比的数据源)protocol.rs:1324
TurnAborted / SessionConfigured / ContextCompacted中止 / 会话就绪(spawn 后第一条必须是它)/ 压缩完成protocol.rs:1421,1342,1303

消费端是常驻的 submission_loopsession/handlers.rs:703-853):while let Ok(sub) = rx_sub.recv().await 后对 sub.op 做大 match 分发。枚举标了 #[non_exhaustive](未来可加变体),未知 op 直接忽略。

3.3 steer:中途发消息不再是「排队」而是「转向」

这个版本最有意思的语义变化:Op::UserInput 到达时若已有活跃的 Regular turn,不会中断也不会排队等下一轮,而是先尝试 steer_input——把新消息塞进 input_queue,正在跑的 run_turn 循环会在下一轮迭代开头把它吸收进对话(handlers.rs:183-275 · session/mod.rs:3827-3900)。只有「无活跃 turn」时才真正 spawn 新任务。这让「模型干活时你补一句话」变成自然的转向,而不是打断重来。

3.4 Task 与两层循环

任务抽象是 SessionTask trait(tasks/mod.rs:214-253):run(session, ctx, input, cancellation_token) → Result<Option<String>>,返回值就是最终的 assistant 消息。四个实现:RegularTask(普通对话)、CompactTask(压缩)、ReviewTask(代码评审)、UserShellCommandTask(用户直发 shell)。spawn_task 先以 Replaced 理由中止一切旧任务再起新的(tasks/mod.rs:314-323)——「一个会话一个任务」的机制保证。

RegularTask 的外层循环极短(我亲自核实过):

core/src/tasks/regular.rs:73-89
loop { let last_agent_message = run_turn( Arc::clone(&sess), Arc::clone(&ctx), ..., next_input, prewarmed_client_session.take(), cancellation_token.child_token(), ).await?; // turn 跑完但期间用户 steer 进了新输入 → 再跑一个 turn 消化它 if !sess.input_queue.has_pending_input(&sess.active_turn).await { return Ok(last_agent_message); } next_input = Vec::new(); }

内层 run_turnsession/turn.rs:142-459)才是 agentic 循环本体,每轮迭代:

阶段做什么位置
A 吸收输入把 pending 的 steer 输入并入本轮;跑 hooks 并把用户消息记入 history + rolloutturn.rs:481-507
B 组请求clone_history().for_prompt()(归一化)+ 按模型现算工具表 + base_instructions → Promptturn.rs:272-278,1112
C 压缩检查token 超限 → run_auto_compact 后 continue(第 7 章)turn.rs:346-369,797-821
D 采样run_sampling_request:带流级重试地调模型,消费 ResponseEvent 流turn.rs:1072-1167,1887
E 工具回路FunctionCall 先记 history 再路由执行;结果经 FuturesOrdered 有序回填;needs_follow_up = truestream_events_utils.rs:405-444 · turn.rs:1853-1877
F 终点判定无 tool call 且 end_turn≠false 且无 pending 输入 → Stop hooks → breakturn.rs:318,371-415

和 Claude Code 完全一致的核心洞察:没有任何计划器。「agent 自主多步工作」的全部秘密就是 needs_follow_up 这一个布尔值——模型不停要工具、循环不停喂结果。

3.5 中断:CancellationToken 与优雅降级

Ctrl-C 从 TUI 一路变成 Op::Interrupt(tui interaction → JSON-RPC turn/interruptturn_processor.rs:1383),引擎侧 interrupt_taskabort_all_tasks(Interrupted)session/mod.rs:3913-3920)。handle_task_aborttasks/mod.rs:829-905)的次序体现了「优雅优先」:先 cancellation_token.cancel()(协作式取消,流消费点用 .or_cancel() 响应)→ 等一个宽限期 → 超时才 handle.abort() 硬杀 tokio 任务 → 最后往 history 写一个 <turn_aborted> 标记并 flush rollout——这样 resume 恢复会话时模型能知道上一轮是被打断的,不会误以为工具调用凭空消失。

3.6 history 记录与 rollout 落盘

统一写入口 record_conversation_itemssession/mod.rs:2777-2794)一次做三件事:写内存 history、包成 RolloutItem 落盘、回发客户端。落盘由 RolloutRecorderrollout/src/recorder.rs:78-119)后台任务串行化,每条带时间戳序列化为一行 JSON,追加到 ~/.codex/sessions/rollout-<日期>-<thread_id>.jsonlrecorder.rs:1519,1811-1833)。codex resume 恢复时从新到旧反向扫描 JSONL,处理压缩检查点/回滚/中断标记后重建 history(session/rollout_reconstruction.rs:113-150)。这与 Claude Code 的 transcript JSONL 思路一致,但 Codex 把「重建」做成了显式的逆向重放算法。

04工具系统

契约与运行时分离:tools crate 定义「发给模型的长相」,core/tools 定义「在本机怎么跑」。工具表不是静态的——每个 turn 按模型和配置现算。

4.1 ToolExecutor 与四态曝光

tools/src/tool_executor.rs:50-70(trait 核心)
pub trait ToolExecutor<Invocation>: Send + Sync { fn tool_name(&self) -> ToolName; fn spec(&self) -> ToolSpec; // 发给 API 的 JSON 契约 fn exposure(&self) -> ToolExposure { Direct } // 四态曝光 fn supports_parallel_tool_calls(&self) -> bool { false } // fail-closed:默认串行 fn handle(&self, invocation: Invocation) -> ToolExecutorFuture; }

ToolExposure 四态tool_executor.rs:15-37):Direct(初始工具表可见)/ Deferred(不进初始表,模型用 tool_search 工具按需发现——节省上下文的「延迟加载工具」)/ DirectModelOnly(排除出 code mode)/ Hidden(只可内部路由,模型看不见)。「模型看不到 ≠ 不能执行」:注册表收下全部 runtime,可见表只含 Direct(tools/spec_plan.rs:250-260)。

ToolSpectools/src/tool_spec.rs:15-53)序列化后就是 Responses API 的 Tool 对象,有两种关键形态:function(JSON Schema 参数,全部 strict:false)和 custom/freeform——不用 JSON,模型直接输出符合 Lark 语法(一种描述文法的 DSL)的原始文本,apply_patch 和 code-mode 的 exec 都走这种(免去了「往 JSON 字符串里塞多行 diff 还要转义」的痛苦)。

4.2 执行链:router → registry(hooks) → parallel → orchestrator

1

路由

模型流回来的 ResponseItem::FunctionCall / CustomToolCallbuild_tool_call 翻译成三类 payload(Function/Custom/ToolSearch),按 ToolName 查 HashMap 找 handler,查不到回「unsupported call」。

tools/router.rs:113-161 · registry.rs:405-442,734-739

2

hooks 包裹

PreToolUse hooks 可 Block 或改写入参;PostToolUse hooks 可拒绝结果或注入 feedback——和 Claude Code 的 hook 语义几乎一比一。

registry.rs:495-539,638-656

3

并行门:读写锁

声明 supports_parallel_tool_calls 的工具拿读锁互相并发;默认工具拿写锁独占执行——一个 RwLock 优雅实现了 Claude Code 里「只读并发、写串行」的同款调度。

tools/parallel.rs:48,133-137

4

orchestrator:审批 + 沙箱 + 升权重试

exec 类工具统一编排:审批判定 → 选沙箱首跑 → 命中沙箱拒绝时(可选地再问一次用户后)去沙箱重试。文件头注释自述整个流程(第 5 章详解)。

tools/orchestrator.rs:1-8,134

4.3 五个代表性工具

shell_command —— 每次一个新进程,10 秒默认超时
  • 参数handlers/shell_spec.rs:154-222):command / workdir(描述强调「永远设 workdir、别用 cd」)/ timeout_ms(默认 10000)/ login,外加沙箱申请参数 sandbox_permissions / justification
  • 执行run_exec_likehandlers/shell.rs:63-245)先拦截 apply_patch 调用,再经 orchestrator 到 core/src/exec.rs spawn 进程。关键常量:超时退出码 124(exec.rs:65)、输出硬 cap 1 MiB(exec.rs:76)、流式增量事件每调用最多 10000 条(exec.rs:80)、孙进程继承 fd 导致的 read 卡死有 2 秒排水超时兜底(exec.rs:89)。
  • 对比 Claude Code 的 BashTool:都不是持久 shell;但 Codex 没有「偷偷 pwd 记住 cd」的技巧——它直接在工具描述里禁止 cd、强制 workdir 参数。
exec_command / write_stdin —— PTY 持久会话(unified_exec)

较新模型(shell_type 为 UnifiedExec 的模型家族)拿到的不是 shell_command 而是这一对:exec_commandPTY(伪终端)里跑命令,yield_time_ms(250ms–30s)内没跑完就返回一个 session_id,模型随后用 write_stdin 向会话写入按键/轮询输出(handlers/shell_spec.rs:88-152)——于是模型能操作 REPL、vim、交互式安装器。会话上限 64 个,输出缓冲用「头尾各半」结构(保留稳定前缀+最新后缀,unified_exec/head_tail_buffer.rs:29-43),常量集中在 unified_exec/mod.rs:65-75。老 shell_command 同时以 Hidden 态保留可路由(spec_plan.rs:659-674)。

apply_patch —— Lark 文法的 freeform 工具 + shell 拦截双入口

Codex 不给模型 Edit/Write 这种「精确字符串替换」工具,而是一个自定义 patch DSL:

handlers/apply_patch.lark(全文法)
start: begin_patch hunk+ end_patch begin_patch: "*** Begin Patch" LF add_hunk: "*** Add File: " filename LF add_line+ delete_hunk: "*** Delete File: " filename LF update_hunk: "*** Update File: " filename LF change_move? change? change_move: "*** Move to: " filename LF change_context: ("@@" | "@@ " /(.+)/) LF change_line: ("+" | "-" | " ") /(.*)/ LF
  • 双入口:① 作为 freeform 工具(模型直接输出补丁原文,不裹 JSON,handlers/apply_patch_spec.rs:9-27);② shell 拦截——run_exec_likemaybe_parse_apply_patch 识别 apply_patch <<'EOF' heredoc 形式(apply-patch/src/invocation.rs:112-137),没有该工具的模型家族也能经 shell 用它。
  • 容错应用:上下文匹配三级递降——精确 → 忽略行尾空白 → 两端 trim(apply-patch/src/seek_sequence.rs:12-60);parser 默认宽松模式(为 gpt-4.1 容错,parser.rs:54)。对比 Claude Code 的 Edit 工具「old_string 必须唯一精确匹配」,两家选了相反的哲学:Codex 容错宽松、Claude 严格拒绝。
  • 流式 UI:参数增量经 StreamingPatchParser 消费、500ms 节流发 PatchApplyUpdated 事件(handlers/apply_patch.rs:57-140)。
update_plan —— 纯 UI 工具

参数是 plan: [{step, status ∈ pending|in_progress|completed}](至多一个 in_progress,handlers/plan_spec.rs:7-58)。执行只做一件事:发 EventMsg::PlanUpdate 给 UI 画清单,固定返回 "Plan updated"(handlers/plan.rs:22,91-95)——和 Claude Code 的 TodoWrite 一样,是给人看的「进度锚点」,对模型没有任何副作用。

web_search 与 view_image —— hosted 工具与多模态入口
  • web_search 不在本地执行:它是 hosted spec(tools/hosted_spec.rs:20-52),由 OpenAI 服务端执行搜索,按 WebSearchMode(Cached/Indexed/Live/Disabled)映射外网访问参数。本地只负责把动作渲染成 UI 卡片(core/src/web_search.rs:18-30)。
  • view_image:读本地图片转 data URL 作为 InputImage 注入对话(handlers/view_image.rs:52-134);是少数声明可并行的工具。注意 Codex 没有独立的文本 Read 工具——读文件一律走 shell(cat/sed),这是与 Claude Code 工具面最大的差异之一。

4.4 输出回填与截断:头尾保留,中间截断

工具输出回填为 ResponseInputItem::FunctionCallOutput { call_id, output }。截断策略与 Claude Code 的「保尾截断」不同:Codex 用 truncate_middle——预算对半分给前缀和后缀,中间插省略标记(utils/string/src/truncate.rs:7-36;token 按 4 字节 ≈ 1 token 估算)。模型级默认预算 10000 字节(protocol/src/openai_models.rs:680),exec 输出外壳统一附 Exit code / Wall time / Total output linescore/src/tools/mod.rs:77-102)。

4.5 code_mode:把工具调用变成写 JavaScript

Codex 特有的实验能力:开启后模型拿到一个 freeform 的 exec 工具,直接写 JS 代码在嵌入的 V8 隔离环境(deno_core,无 Node、无文件系统、无网络,code-mode-protocol/src/description.rs:12-18)里执行;所有普通工具变成全局 tools.* 对象上的异步函数——await tools.exec_command(...)。JS 里的嵌套调用经 DispatchBroker 回灌到同一套工具运行时(tools/parallel.rs:395-398)。这就是「工具编排编程化」:模型可以用循环、条件、变量把 10 次工具调用写成一段代码一次跑完,省下 9 轮模型往返。CodeModeOnly 模式下普通工具从可见表隐藏、只留 exec 入口(spec_plan.rs:460-471)。

05权限与沙箱 ★

Codex 的重头戏。Claude Code 的安全主线是「权限提示系统」(沙箱是辅助);Codex 反过来:OS 级沙箱是默认防线,审批弹窗是沙箱之外的例外通道。

5.1 两个正交的策略轴

第一轴 AskForApproval(何时问人),定义在 protocol/src/protocol.rs:901-925

untrusted

只有安全白名单里的「纯读」命令自动放行,其余一律弹窗

on-request(默认)

模型自己决定何时申请升权;旧的 on-failure 已退役为它的 serde 别名(protocol.rs:910)

granular

按 5 个开关细分(沙箱升权/规则/技能/权限申请/MCP 询问);开关关闭时对应类别自动拒绝而非弹窗

never

永不问人,失败直接回给模型(CI 场景)

第二轴 SandboxPolicy(能碰什么)protocol.rs:988-1036):danger-full-access / read-only / workspace-write(cwd 可写 + 网络默认禁)/ external-sandbox(进程已在外部沙箱内)。workspace-write 下每个可写根自动挂上受保护子路径——.git.codex.agents 保持只读protocol/src/permissions.rs:23-31,991-1031),注释明说这是防 agent 改 .git/hooks 之类的提权入口。

5.2 一条命令的安全判定链

1

拆解 shell 包装

bash -lc "a && b | c" 先被拆成子命令数组逐个评估;拆不开的复杂脚本标记 used_complex_parsing(禁止沉淀成 allow 规则)。审批缓存的 key 也做归一化:/bin/bash -lcbash -lc 等价、复杂脚本收敛成 __codex_shell_script__ 形态——防止「approve for session」被写法差异绕过。

exec_policy.rs:759-797 · command_canonicalization.rs:14-38

2

execpolicy 规则匹配(Starlark)

用户/管理员可在 $CODEX_HOME/rules/*.rules 用 Starlark 语法(Bazel 同款的 Python 方言)写前缀规则:prefix_rule(pattern=["rm"], decision="forbidden", justification="...")。命中 Allow 且每个子命令段都被显式覆盖才允许绕过沙箱。审批 UI 里的「记住这个决定」会把规则在线追加写盘 + 热更新;但 python3 -cbash -lcsudo 等「万能解释器前缀」被硬编码禁止沉淀为 allow 规则。

exec_policy.rs:49-51,307-311,352-366,376-424,52-99 · execpolicy/tests/basic.rs:171-176

3

未命中规则的启发式兜底

先查 known-safe 白名单(cat/ls/grep/rg/git status...,且 find 排除 -exec、rg 排除 --pre 等能执行代码的旗标,shell-command/src/command_safety/is_safe_command.rs:12-199);再查危险命令名单。普通命令在 on-request 策略下不弹窗、直接让沙箱执行约束,只有模型显式带 with_escalated_permissions 申请升权才触发弹窗。

exec_policy.rs:627-743

4

产出三态

ExecApprovalRequirement:Skip(可带 bypass_sandbox)/ NeedsApproval(可附建议沉淀的规则)/ Forbidden。补丁侧有平行的 assess_patch_safety:补丁完全落在可写路径内(含 .. 归一化防目录穿越)且平台沙箱可用才自动放行。

tools/sandboxing.rs:162-181 · safety.rs:32-110,135-203

5.3 审批弹窗的握手:oneshot channel

需要问人时,引擎在 request_command_approval 里创建一个 oneshot channel(一次性管道,等价于 Claude Code 那个「Promise + 队列」握手):把发送端存进 pending map、发 EventMsg::ExecApprovalRequest 给 UI,然后 rx.await 挂起等待(session/mod.rs:2122-2194)。用户在 TUI 弹窗里选择后,决定以 Op::ExecApproval { id, decision } 走 SQ 回来,handler 按 id 找到 oneshot 回填(handlers.rs:364-404)。ReviewDecision 有 7 个变体(protocol.rs:4043-4078):Approved / ApprovedForSession / Denied / Abort,外加两个「顺便落规则」变体(ApprovedExecpolicyAmendment、NetworkPolicyAmendment)和 TimedOut。收不到回复默认 Abort——fail-closed。

5.4 三平台沙箱实现

统一入口 SandboxType = None / MacosSeatbelt / LinuxSeccomp / WindowsRestrictedToken(sandboxing/src/manager.rs:36-76)。

Linux:内嵌 bubblewrap + seccomp(Landlock 已降级为 legacy)

  • 文件系统:bubblewrap(bwrap,容器技术里常用的无特权命名空间沙箱)构建「默认只读 + 显式可写根叠加 + .git/.codex 保持只读」的文件系统视图(linux-sandbox/src/bwrap.rs:1-11,注释明说语义对齐 macOS Seatbelt)。妙处在 bwrap/build.rs:14-19bubblewrap 的 C 源码被 vendor 进仓库直接编译进 Codex 二进制——不依赖各发行版千差万别的系统 bwrap。
  • 网络与危险系统调用:seccomp(内核的系统调用过滤器)默认拒绝 connect/bind/listen 等网络调用、socket 仅放 AF_UNIX;并无条件封杀 ptraceio_uring_*(io_uring 是绕过 seccomp 网络拦截的知名通道)(linux-sandbox/src/landlock.rs:165-240)。被 seccomp 击杀的进程退出码 159(128+SIGSYS)会被识别为「沙箱拒绝」。
  • 为什么要独立入口:seccomp/bwrap 的限制必须在目标子进程自身、exec 前施加且不可逆——所以命令被改写成 codex-linux-sandbox --permission-profile ... -- 原命令,经 arg0 分发由同一个二进制的「沙箱人格」执行(sandboxing/src/landlock.rs:6,23-60),父进程不被自己的 seccomp 污染。
  • Landlock(Linux 5.13+ 的无特权文件系统 LSM)保留为 legacy 后备,仅 feature 开启时使用(linux-sandbox/src/landlock.rs:1-11,139-163)。

macOS:Seatbelt(sandbox-exec)

用硬编码路径 /usr/bin/sandbox-exec(防 PATH 投毒,sandboxing/src/seatbelt.rs:26-30)加载 SBPL(一种 Lisp 语法的沙箱策略语言)profile 执行命令。base policy 开头即「默认全拒」:

sandboxing/src/seatbelt_base_policy.sbpl:7-14(节选)
(deny default) (allow process-exec) (allow process-fork) (allow signal (target same-sandbox)) (allow file-write-data (require-all (path "/dev/null") (vnode-type CHARACTER-DEVICE)))

可写根不写进策略文本,而是经 -D WRITABLE_ROOT_0=... 参数传入、策略里用 (subpath (param "WRITABLE_ROOT_0")) 引用——避免路径转义注入(seatbelt.rs:623-769)。放网时才叠加 network profile(只放 DNS/证书校验等系统服务)。

Windows:Restricted Token(默认关闭)

不是 AppContainer,而是 CreateRestrictedToken + WRITE_RESTRICTED 旗标(只对操作检查受限 SID,实现「读多写受限」,windows-sandbox-rs/src/token.rs:459-472);文件系统靠 ACL、网络靠 WFP(Windows 过滤平台)block 过滤器(windows-sandbox-rs/src/wfp.rs:1-30)。三档配置默认 Disabledprotocol/src/config_types.rs:269-274)——Windows 沙箱是最新、默认不启用的一块。

5.5 沙箱拒绝之后:升权重试的安全阀

命令在沙箱里失败且判定为「沙箱拒绝」(靠关键词 + 退出码识别,避免把业务错误误判,sandboxing/src/denial.rs:6-53)时,orchestrator 的重试路径(tools/orchestrator.rs:289-440)层层把关:工具须声明 escalate_on_failure;审批策略须允许问人(never/on-request 下默认不重试);最关键的安全阀——若策略含 denied-read 路径,禁止用「去沙箱重试」变相提权(denied reads 只在沙箱里存在,去沙箱等于绕过,tools/sandboxing.rs:281-289)。通过全部关卡后带着理由("command failed; retry without sandbox?")再次征询用户,批准才裸跑。

5.6 网络:不是开关,是受管代理

workspace-write 下网络默认禁。放行不是「打开开关」,而是走 network-proxy crate 的受管代理:Linux 沙箱切到 ProxyRouted 模式(seccomp 只放本地代理桥的连接),出网统一由代理裁决;被拦请求带原因码翻译成人话,on-request 下触发带 host 的审批,用户批准可沉淀为 execpolicy 网络规则(allow https_connect access to <host>)持久化(core/src/network_policy_decision.rs:26-100 · exec_policy.rs:426-470)。

对比 Claude Code:Claude Code 的 bash 沙箱注释直言「排除列表只是便利功能,真正的安全边界是权限提示系统」;Codex 恰好相反——沙箱是硬边界(seccomp 杀进程、Seatbelt 拒 syscall),审批弹窗只在「要跳出沙箱」时出现。这也解释了两者默认体验差异:Codex 在 workspace-write 下大部分命令不弹窗直接跑(反正有沙箱兜底),Claude Code 则更依赖逐条审批与 allow 规则。

06模型 / API 层:Responses API、WebSocket、认证

core/src/client.rs(2402 行)+ codex-api crate。这一版只支持 OpenAI Responses API——Chat Completions 已被物理删除。

6.1 WireApi 只剩一条路

model-provider-info/src/lib.rs:55-82(节选)
pub enum WireApi { /// The Responses API exposed by OpenAI at `/v1/responses`. #[default] Responses, } // 反序列化 "chat" 直接报错: "chat" => Err(custom("wire_api = \"chat\" is no longer supported..."))

这意味着 Ollama / LM Studio 这类本地模型也走 Responses API 形态的端点(内置 provider gpt-oss,localhost:11434/1234,lib.rs:422-423,495-534)。自定义 provider 只能新增不能覆盖内置(lib.rs:462-493)。

6.2 stream():WebSocket 优先,HTTP SSE 兜底

对象分两级:session 级 ModelClientturn 级 ModelClientSession——后者缓存 WebSocket 连接和从响应头拿到的 x-codex-turn-state 粘性路由 token(client.rs:246-280)。stream()client.rs:1734-1786)先试 WS,失败或不支持则落回 stream_responses_api(HTTP + SSE)。降级是单向棘轮:流级重试预算耗尽后 force_http_fallback 把本 session 永久钉在 HTTP 上(client.rs:492-511,1793-1803)。

请求体组装 build_responses_requestclient.rs:812-892)的关键字段:

字段取值说明
instructionsbase_instructions主 system prompt = models-manager/prompt.md("You are a coding agent running in the Codex CLI..."),可由服务端 /models 目录按模型下发覆盖
input归一化后的完整 history含 AGENTS.md、环境上下文、全部对话与工具结果
storefalse(仅 Azure 为 true)不落 OpenAI 服务端存储;配合剥 item id
stream / tool_choicetrue / "auto"永远流式
prompt_cache_keythread_id服务端 prompt cache 按会话线程复用前缀
reasoning + includeeffort/summary + ["reasoning.encrypted_content"]推理内容以加密态往返(store:false 下维持推理链)

对比 Claude Code 的 prompt caching:Claude Code 要在客户端精心摆 4 个 cache_control 断点;Codex 只发一个 prompt_cache_key,缓存断点完全交给服务端——这是 Responses API(有状态、服务端管理)与 Messages API(无状态、客户端管理)的协议级差异。

6.3 SSE 事件与超时

SSE 解析下沉到 codex-api crate。事件循环 process_sse_with_treatmentcodex-api/src/sse/responses.rs:478-583)每次 timeout(idle_timeout, stream.next())——默认 300 秒无事件判死(model-provider-info/src/lib.rs:26);流在 response.completed 之前关闭视为错误。response.failed 按 error.code 分类(上下文超限/配额/过载/无效请求),限流错误的重试延迟是用正则从错误文案里抠出来的("Please try again in 11.054s",sse/responses.rs:585-609)。

6.4 三层重试

  • 单请求 HTTP 层:默认最多 4 次,退避 = 200ms × 2^(n-1) × 0.9~1.1 抖动;只重试 5xx 与传输错误,429 不在这层重试(交上层按服务端指示的延迟处理)(codex-client/src/retry.rs:23-73 · model-provider-info/src/lib.rs:261-267)。
  • 流级重连:流中断整个请求重发,默认 5 次,UI 显示 "Reconnecting... n/5"(core/src/responses_retry.rs:22-79)。
  • 传输降级:WS → HTTP 永久降级(见 6.2);401 触发 token 刷新后重试(client.rs:1432-1451)。

6.5 认证:ChatGPT OAuth 或 API key

AuthModeprotocol/src/auth.rs:9-30)主线两条:ApiKey(环境变量 OPENAI_API_KEY)和 Chatgpt(订阅计划直接用)。OAuth 细节:本地起 127.0.0.1:1455 回调服务器(占用则退 1457,login/src/server.rs:54-56),标准 PKCE S256(login/src/pkce.rs:12-27),issuer auth.openai.com。token 存 $CODEX_HOME/auth.json(新版优先系统 keyring,成功后删除文件回退,login/src/auth/storage.rs:38-61);主动刷新条件 = JWT 过期前 5 分钟或距上次刷新超 8 天(login/src/auth/manager.rs:177-178,2520-2542)。ChatGPT 计划用户不走 api.openai.com,而是 https://chatgpt.com/backend-api/codexmodel-provider-info/src/lib.rs:38,241-258)。这与 Claude Code 的「API key vs Claude 订阅 OAuth」双轨一模一样,连 localhost 回调监听的手法都相同。

07上下文管理与压缩:四种 compact

上下文窗口有限。Codex 的续命手段从 Claude Code 的「客户端三级压缩」演化成了「归一化 + 四种压缩实现按能力分发」。

7.1 history 归一化:发请求前的清洗

ContextManager 是内存 history 的持有者,发请求前 for_prompt() 做三步清洗(core/src/context_manager/history.rs:121-146,359-367):① 为没有输出的工具调用合成 "aborted" 输出(API 要求 call 与 output 严格配对,中断后的孤儿 call 会 400——与 Claude Code 的「合成 tool_result」同款问题同款解法;合成 id 用固定 UUID 命名空间,注释警告改了会「改变模型可见 ID、炸掉 prompt cache」,normalize.rs:15-63);② 删孤儿输出;③ 模型不支持图片时把图片换成占位文本。

7.2 触发条件:90% 水位

每次采样前检查(session/turn.rs:797-821):token 用量 ≥ auto_compact_token_limit 即触发。该上限默认 = 模型上下文窗口的 90%protocol/src/openai_models.rs:441-452),context_window 元数据来自 models-manager(内置表 + 服务端 /models 目录 + 本地缓存,兜底 272000,models-manager/src/model_info.rs:95-97)。此外切换到更小窗口的模型也会触发预压缩(turn.rs:835-909)。

7.3 四种实现按能力分发

feature: TokenBudget

① 开新窗口(不摘要)

直接 start_new_context_window,跳过一切摘要。compact_token_budget.rs:20-24

feature: RemoteCompactionV2

② 远端压缩 v2

往 input 末尾 push CompactionTrigger 项走流式 /responses,服务端做摘要;保留消息预算 64K token。compact_remote_v2.rs:52-56

OpenAI/Azure

③ 远端压缩 v1

一次性 POST /responses/compact,超时 = idle_timeout × 4。client.rs:513-618

兜底

④ 本地摘要压缩

把摘要指令当用户消息发给模型自己写交接摘要(最像 Claude Code 的 auto-compact)。compact.rs:220-377

本地压缩(④)的细节:摘要 prompt 开头是 "You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task."(prompts/templates/compact/prompt.md,可用 compact_prompt 配置覆盖);压缩请求本身超窗则从头删 item 重试(保住前缀缓存);成功后重建 history = 最近的用户消息(倒序回填、总预算 20000 token)+ 摘要作为一条 user 消息compact.rs:53,598-656),并向用户发警告「长会话多次压缩会降低模型准确性」。分发逻辑在 session/turn.rs:917-977

对比 Claude Code:Claude Code 的 compact 全在客户端(9 小节固定摘要格式 + 重注入读过的文件);Codex 的首选路径是把压缩也交给服务端(毕竟 Responses API 是有状态协议,服务端本来就有完整对话)。此外 Codex 没有 microcompact(清旧工具输出)这一级——工具输出在入账时就被 truncate_middle 钳到 10K 字节了,脏东西根本进不了 history。

08TUI 层:ratatui 的 inline viewport

不是全屏应用。Codex 把「定稿的历史」直接写进终端原生回滚缓冲,底部只保留一个动态高度的活动视口——这是与 Claude Code(fork 版 Ink 整屏管理)最有辨识度的架构差异。

8.1 渲染模型:两条正交路径

ratatui 是 Rust 主流的 immediate-mode 终端 UI 库:没有持久 widget 树,每帧由代码把 UI 画进离屏 Buffer,库对比前后帧只重画变化的单元格(和 Ink 的帧 diff 殊途同归)。crossterm 是底层终端驱动(raw mode、按键流、ANSI 命令)。

两条渲染路径
① 活动视口(每帧重画) schedule_frame() → TuiEvent::Draw → chat_widget.desired_height(width) 动态定高 → tui.draw(...) 在 SynchronizedUpdate 内重画底部视口 tui.rs:880-935 ② 定稿历史(一次性注入 scrollback,之后不再管) InsertHistoryCell → 设置滚动区域 DECSTBM("\x1b[r") 限定在视口上方 → 逐行 Print,行被"顶进"终端原生回滚缓冲 insert_history.rs:216-245,332-337

好处:历史滚动交给终端本体(鼠标滚轮、搜索、复制都是原生体验),Codex 只需渲染当前活动区;tui::init() 的注释直说「inline viewport; history stays in normal scrollback」(tui/src/tui.rs:375-376)。alt-screen(全屏)只在 Ctrl+T 转录浏览等 overlay 时按需进入(tui.rs:734-766)。帧率由 FrameRequester 合并调度(actor 模式,多次请求合并成一次 draw)。

8.2 事件循环与流式渲染

主循环是四路 select!tui/src/app.rs:1169):UI 内部事件 / 活跃会话事件 / 终端按键流 / app-server 通知。流式文本的渲染节奏很讲究(tui/src/chatwidget/streaming.rs:388-427):Markdown 增量先进 MarkdownStreamCollector只在换行边界提交整行(未完成的尾行留在活动视口里实时变化);提交动画由专用线程发 CommitTick、平滑模式一 tick 一行,落后时批量泄洪。流结束后整段流式单元被替换为持有原始 Markdown 源、可随终端 resize 重排的 AgentMarkdownCellapp_event.rs:671-683)。历史区渲染单元统一实现 HistoryCell trait(history_cell/mod.rs:189,按 approvals/exec/patches/plans 等分类)。

8.3 底部面板与输入

  • composer 是自研 textarea(含 Vim 模式、kill buffer、粘贴突发检测,bottom_pane/textarea.rs:1-40);弹窗/模态是压在 composer 之上的 view 栈(bottom_pane/mod.rs:208)。
  • 斜杠命令是一个 enum(/model /permissions /review /new /resume /fork /diff /status /usage /quit...,注释特意写「不要按字母排序!枚举顺序即弹窗展示顺序」,tui/src/slash_command.rs:12)。注意命令叫 /permissions 而非旧文档里的 /approvals
  • @文件补全:光标处检测 @ 前缀 token → 异步 FileSearchManager 模糊搜索 → 弹窗回填(chat_composer.rs:2371-2373)。
  • 审批弹窗:ExecApprovalRequest 转成列表选择视图,用户选择后经 AppCommand::ExecApproval 回流引擎(bottom_pane/approval_overlay.rs:72-80,349-386)。
  • Ctrl-C 分层消费:弹窗先吃(关自己)→ 有任务在跑则中断 → 空闲双击退出(chatwidget/interaction.rs:373-420)。
  • 渲染质感:Markdown 用 pulldown-cmark 解析 + 自研表格排版;语法高亮包的是 syntect(约 250 种语言);diff 渲染带行号 gutter、背景色随终端明暗自适应(tui/src/markdown_render.rs · render/highlight.rs:1-4 · diff_render.rs:1-32)。

09app-server:所有前端的统一协议层

Codex 特有的关键子系统。这一版最大的架构演进:TUI、codex exec、IDE 插件、SDK 全部收敛为同一个 JSON-RPC 协议的客户端。

9.1 为什么多此一层

引擎内部的 SQ/EQ 是 Rust 类型的 channel 协议,进程外用不了。app-server 把它包装成双向 JSON-RPC(双向 = 客户端能调服务器方法,服务器也能反过来请求客户端,比如审批弹窗)。于是同一个引擎能被四种前端驱动:TUI(进程内嵌,tui/src/lib.rs:1348-1370)、codex exec(进程内客户端,exec/src/lib.rs:787)、IDE 扩展(codex app-server 子进程 stdio)、Python SDK(app-server --listen stdio://)。协议类型还能一键导出 TypeScript 定义和 JSON Schema 给前端用(app-server-protocol/src/lib.rs:8-13)。

9.2 协议长什么样

方向代表方法位置(common.rs)
客户端 → 服务器thread/start · thread/resume · thread/fork · turn/start · turn/steer · turn/interrupt · review/start · model/list · account/login/start:476-995
服务器 → 客户端(请求)item/commandExecution/requestApproval · item/fileChange/requestApproval · mcpServer/elicitation/request:1456-1475
服务器 → 客户端(通知)thread/started · turn/completed · item/agentMessage/delta · item/reasoning/textDelta:1609-1697

概念映射:thread(会话线程)/ turn(一次用户输入触发的完整回合)/ item(回合内的条目:消息、命令执行、文件变更、推理)。app-server 内部有事件泵把引擎的 EventMsg 翻译成 v2 通知(app-server/src/request_processors/thread_lifecycle.rs:302-309)。对比 Claude Code:其 SDK 走的是自定义 stdin/stdout 控制协议且只服务 SDK;Codex 让自家 TUI 也吃自家协议(dogfooding),保证协议永远是完整的一等公民。

10扩展生态

无头模式、MCP 双向、SDK、云任务、hooks/skills/plugins——Codex 的生态位与 Claude Code 高度同构,但落点各有取舍。

10.1 codex exec:为 CI 而生的无头模式

参数面(exec/src/cli.rs):--json 输出 JSONL 事件流(thread.started / turn.completed / item.completed / error 每行一个 JSON,exec/src/exec_events.rs:11-36);--output-schema 用 JSON Schema 约束最终回答形状;-o 把最后一条消息写文件;- 从 stdin 读 prompt(与位置参数同给时 stdin 作为 <stdin> 块追加);--ephemeral 不落盘会话。CI 友好的硬性设计:turn 失败/中断或收到错误 → 进程退出码 1exec/src/lib.rs:988-1050)。子命令 codex exec resume --last 恢复上次会话继续跑。

10.2 MCP:既当客户端又当服务器

  • 当客户端:config.toml [mcp_servers] 配置外部 server(stdio 或 streamable HTTP 两种 transport;required=true 时初始化失败直接让 codex exec 报错退出,config/src/mcp_types.rs:156-201,434-462)。底层用官方 Rust SDK(rmcp 1.8.0),OAuth/elicitation/重试齐全。工具命名已核实:发给模型的是 {server}__{tool}(双下划线),mcp__ 前缀只在 hooks 视角保留为 legacy 兼容(core/src/tools/handlers/mcp.rs:29-65)。
  • 当服务器codex mcp-server 把 Codex 自己暴露为 stdio MCP server,提供 codex(起会话跑任务)和 codex-reply(按 thread_id 续聊)两个工具(mcp-server/src/codex_tool_config.rs:117-120,224-237)——其它 agent 可以把 Codex 当工具调。审批经 MCP elicitation 机制回传调用方。

10.3 SDK:两种语言两种接法

TypeScript SDK 的 Thread.runStreamed() 返回异步生成器,底层 spawn codex exec --experimental-json 消费 JSONL(sdk/typescript/src/exec.ts:86,151-152);Python SDK 则 spawn codex app-server --listen stdio:// 走 JSON-RPC(sdk/python/src/openai_codex/client.py:213,252-263)。同一引擎,两种传输——也侧面印证了 app-server 是终态方向。

10.4 hooks / skills / plugins / 云任务

机制要点位置
Hooks10 种事件(PreToolUse / PermissionRequest / PostToolUse / Pre+PostCompact / SessionStart / UserPromptSubmit / SubagentStart+Stop / Stop);handler 三型:Command / Prompt / Agentprotocol.rs:1471-1490 · core/src/hook_runtime.rs:163-433
SkillsSKILL.md 文件;发现根:项目 .codex/skills、~/.agents/skills、$CODEX_HOME/skills、/etc/codex/skills、项目根到 cwd 沿途 .agents/skillscore-skills/src/loader.rs:121,294-400
Pluginscodex plugin add/list/remove + marketplace 体系(支持 npm 分发源、官方精选市场)cli/src/main.rs:1064-1092 · core-plugins/src/lib.rs:31-32
Review 模式codex review --uncommitted/--base/--commit:专用 ReviewTask + 专用 prompt 做代码评审cli/main.rs:130 · core/src/tasks/review.rs:49
Codex Cloudcodex cloud 浏览云端任务 / codex apply 把云端 agent 产出的 diff 落到本地(git apply);exec 可 --attempts 1-4 做 best-of-Ncloud-tasks/src/cli.rs:15-49 · chatgpt/src/apply_command.rs:22-40
Resume / Forkcodex resume(默认弹选择器 / --last)、codex fork、archive/delete;数据源就是 rollout JSONLcli/main.rs:180-193 · tui/src/resume_picker.rs

与 Claude Code 生态对照:斜杠命令 Codex 是内置 enum(不可用 markdown 自定义;旧版的 ~/.codex/prompts 自定义 prompt 机制在本快照中未找到实现,疑似已被 skills 吸收——此点未完全核实);子 agent 机制存在(multi_agents 工具族、spawn_subagent,thread_manager.rs:711)但本文未深入展开。

11文件索引速查

按「我想看 X」检索,路径相对 codex-rs/。

我想看…去这里
进程入口 / 子命令定义cli/src/main.rs:94,123-212,956 · arg0/src/lib.rs:58-163
TUI 启动链tui/src/lib.rs:850,1282 · tui/src/app.rs:759,1169
配置分层 / -c 覆盖core/src/config/mod.rs:610,1229 · config/src/config_layer_source.rs:6-46
SQ/EQ 协议类型protocol/src/protocol.rs:161(Submission),515(Op),1254(Event),1267(EventMsg)
会话对象链core/src/thread_manager.rs:182 · codex_thread.rs:160 · session/mod.rs:389 · session/session.rs:28 · state/session.rs:26
提交分发 / steercore/src/session/handlers.rs:703,183 · session/mod.rs:3827
Task 抽象 / 中止core/src/tasks/mod.rs:214,314,829 · tasks/regular.rs:73
run_turn 主循环 / 采样core/src/session/turn.rs:142,1072,1887 · stream_events_utils.rs:405
工具契约 / 曝光四态tools/src/tool_executor.rs:15-70 · tool_spec.rs:15-53
工具表现算 / 路由 / 并行core/src/tools/spec_plan.rs:171,613 · router.rs:113 · registry.rs:405 · parallel.rs:133
审批+沙箱编排core/src/tools/orchestrator.rs:134,289 · tools/sandboxing.rs:162,248,285
shell / PTY 会话 / 截断core/src/exec.rs:58-89 · unified_exec/mod.rs:65-75 · utils/string/src/truncate.rs:7
apply_patch DSLapply-patch/src/parser.rs:37 · seek_sequence.rs:12 · invocation.rs:112 · handlers/apply_patch.lark
审批策略 / 沙箱策略枚举protocol/src/protocol.rs:901,988 · permissions.rs:23,991
命令安全判定core/src/exec_policy.rs:269,627 · shell-command/src/command_safety/is_safe_command.rs:12 · command_canonicalization.rs:14
Linux 沙箱(bwrap+seccomp)linux-sandbox/src/bwrap.rs:1 · landlock.rs:165 · bwrap/build.rs:14
macOS Seatbelt / Windowssandboxing/src/seatbelt.rs:623 + seatbelt_base_policy.sbpl · windows-sandbox-rs/src/token.rs:460
模型客户端 / 请求组装core/src/client.rs:246,812,1734 · codex-api/src/common.rs:74,199
SSE 解析 / 重试codex-api/src/sse/responses.rs:325,478 · codex-client/src/retry.rs:23 · core/src/responses_retry.rs:22
认证 / OAuthlogin/src/server.rs:54 · pkce.rs:12 · auth/manager.rs:177,2520 · model-provider-info/src/lib.rs:38
history 归一化 / 压缩core/src/context_manager/{history.rs:121,normalize.rs:17} · session/turn.rs:917 · compact.rs:220 · compact_remote_v2.rs:52
rollout 落盘 / resume 重建rollout/src/recorder.rs:78,1811 · core/src/session/rollout_reconstruction.rs:113
TUI 流式渲染 / scrollback 注入tui/src/chatwidget/streaming.rs:388 · insert_history.rs:216 · tui.rs:375,880
底部面板 / 斜杠命令 / 审批弹窗tui/src/bottom_pane/{mod.rs:208,textarea.rs,approval_overlay.rs:72} · slash_command.rs:12
app-server 协议方法表app-server-protocol/src/protocol/common.rs:476-1697 · app-server/src/request_processors/turn_processor.rs:522
exec 无头 / SDK / MCP 双向exec/src/{cli.rs,lib.rs:787,exec_events.rs:11} · sdk/typescript/src/exec.ts:86 · mcp-server/src/codex_tool_config.rs:117 · core/src/tools/handlers/mcp.rs:29
hooks / skills / AGENTS.mdcore/src/hook_runtime.rs:163 · core-skills/src/loader.rs:121 · core/src/agents_md.rs:1-56

11.1 未核实与存疑清单

  • 仓库内 config.md 的配置项清单已外迁在线文档,本文以 core/config.schema.json 佐证配置键存在性。
  • ~/.codex/prompts 自定义斜杠 prompt 机制在本快照中未找到实现(疑似被 skills 体系吸收),未核实。
  • network-proxy 的 MITM/证书注入、Windows 沙箱 ACL 布点、guardian 自动审批子系统只做了机制定性,未逐行核实。
  • WebSocket 传输的具体 URL 路径、SUBMISSION_CHANNEL_CAPACITY 数值、TUI CommitTick 间隔毫秒值未查。
  • spawn_task 中连续两次 abort_all_tasks(tasks/mod.rs:320-321)疑似防御式写法,意图未考证。