OpenAI Codex 架构解析
OpenAI Codex 目前已经宣布开源,并且明确了开源的部分是 harness 核心模块^1^,所以 Codex 非常适合作为拆解 harness 架构的样本。核心拆解 codex-rs 目录下的 136 个 crate^2^。(注:crate 是 Rust 的编译单元。)
架构分析
如下图所示,Codex 的顶层结构可归纳为七个部分。

- Core 引擎 (
core、core-api、core-plugins、state、tools、protocol):Agent 主循环、会话与轮次状态、工具注册与执行,以及客户端与引擎之间的线路类型。 - 前端层 (
tui、exec、app-server):分别是交互式终端、非交互式批处理、长驻服务进程。三者并列依赖同一个 Core。 - 执行与沙箱 (
sandboxing、linux-sandbox、bwrap、execpolicy、exec-server、shell-command、shell-escalation、process-hardening):技术边界层,模型能否访问某个文件或某个端口,由这一层裁定。 - 会话持久化 (
rollout、rollout-trace、thread-store、history、memories/read、memories/write):执行轨迹、线程索引、记忆的读写分离。 - 模型接入 (
model-provider、model-provider-info、models-manager、ollama、lmstudio、responses-api-proxy):多后端接入,支持本地模型。 - 上下文装配 (
context-fragments、prompts、skills、hooks、config、features):决定每一次模型请求往上下文里放什么。 - 扩展机制 (
ext/下 14 个 crate):extension-api为契约本身,其余十三个是依该契约挂载的能力。
此外还有另外的四组横切能力:
- 双向 MCP(
mcp-server将 Codex 暴露为 MCP server,rmcp-client使其作为 MCP client 连接他方) - Code Mode(
code-mode四个 crate,令模型以编写代码的方式编排工具,而非逐个发出函数调用) - 云与协同(
cloud-tasks、agent-roles、agent-graph-store) - 可观测(
otel、analytics、diagnostics、response-debug-context)。
Core 引擎
core 是唯一的特权内核。其内部的子目录划分本身即构成一份架构说明,本文按职责将其归为七类,以下逐一展开。
会话、轮次与步
一个长时间运行的 Agent 会话,若只具备 "一次对话" 这一种粒度,在工程上将立即面临三个问题,即:
- 进程崩溃之后从何处恢复?
- 用户中途追加的输入在哪个边界被领取?
- 模型所见的工具集与实际可执行的工具集如何保证一致?
Codex 的处理方式是将粒度划分为三层。
- Thread(线程) :一段完整的会话,具有稳定的
ThreadId,对应用户视角中 "这个任务" 的单位。 - Turn(轮):一次用户请求及其后 Agent 的全部工作,是持久化的单位。
- Step(步):一个 turn 内部的一次采样请求,及该次采样所产出的工具调用与执行。一个 turn 可包含多个 step。

这三层在代码中均有对应实体,位于 core/src/session/ 之下:session.rs、turn.rs、step_context.rs、turn_context.rs、turn_input.rs、turn_suspension.rs、input_queue.rs、context_window.rs、token_budget.rs、world_state.rs、rollout_reconstruction.rs、multi_agents.rs、thread_settings.rs。
值得注意的是 input_queue.rs 这一文件。用户的中途输入之所以能够成为 "一等公民",原因在于它有一个具名实体承载,主循环在明确的边界上从该队列领取新输入。对于耗时较长的编译、测试或远程命令而言,这条顺序规则决定了用户的中断究竟是一项可靠的控制,还是一次结果不确定的竞争。
两级冻结:TurnContext 与 StepContext
这一处是本文认为整个 Codex 架构中迁移价值最高的设计,因此展开得较为详细。
其所要解决的问题:在一次 Agent 运行过程中,有相当多的状态会中途变化,用户可能切换模型,某个 MCP server 可能刚接入或刚断开,权限规则可能因用户批准了一次操作而扩大,工作目录亦可能被切换。而模型的 Plan 规划始终基于某一时刻的世界状态做出。若模型在时刻 A 观察到工具集 X 并据此发出一个工具调用,而执行时世界已变为 B、工具集已变为 Y,则会产生两类故障,其一是模型发出一个无法被满足的调用,其二也是更为严重的一类,副作用落在了模型并不知悉的环境之中。
Codex 的处理方式是将配置分为两级冻结:
- TurnContext:turn 级的配置快照,包括模型、环境与鉴权等在一轮之内不应变化的配置。
- StepContext:单次采样请求级的快照,包括这一步可见的工具集、MCP 绑定与审批策略。它在循环的每一轮迭代中重新解析,将中途的模型切换、MCP 变化,以及新输入所带来的 MCP 需求一并纳入。

工具调用由 ToolRouter 负责,将模型发出的每个函数调用解析到具体的处理器,而这张路由表按 turn 组装。仓库文档给出的理由是关键所在,这种分离确保工具执行时所用的配置,与当初向模型宣告时的配置完全同一。
笔者认为这是 Coding Agent 中一条常被忽略、却极为重要的正确性条件,即:模型依据哪一个世界做出规划,运行时就必须依据哪一个世界执行。就自研 Agent 而言,其参考意义在于三点:
- 每次采样之前应有一个明确的 "决策视图" 对象,历史、环境、工具定义与权限均由其提供,而非各自读取全局状态。
- 该视图对象必须是不可变快照,中途的变更只能作用于下一步,不得回溯改写已经宣告出去的那一份。
- 宣告与执行必须共用同一来源。若宣告工具集的代码与执行工具的代码各自读取一份配置,则错位只是时间问题。
上下文装配:四十多个具名片段
core/src/context/ 目录下有四十余个文件(按前述计数方式,下同),每个文件负责一种要注入上下文的片段,例如 base_instructions.rs、developer_instructions.rs、user_instructions.rs、environment_context.rs、compaction_summary.rs、permissions_instructions.rs、hook_additional_context.rs、token_budget_context.rs、turn_aborted.rs、plugin_instructions.rs、multi_agent_role_instructions.rs、current_time_reminder.rs、model_switch_instructions.rs、guardian_policy.rs、inter_agent_message.rs,此外还有一个 world_state/ 子目录。

笔者认为这是整个代码仓库中最能体现 Codex 上下文工程成熟度的一处。作为对照,多数项目的做法是在单个函数内做字符串拼接,条件分支逐次累加,最终无法回答某一句提示词来自何处、在何种条件下出现。Codex 的做法是将每一种待注入的内容都实现为具名、有归属、可单独测试的模块。
这一做法带来三点收益:
- 上下文的构成可枚举,出现问题时能够定位到具体是哪一个片段污染了模型的判断。
- 片段可独立演进,新增一种注入无须改动其他片段的代码。
- 也是最具实际价值的一点,token 预算可以核算,因为每一份消耗都有明确归属。
值得注意的是 turn_aborted.rs 与 model_switch_instructions.rs 这两个片段的存在,据此可以确认,在中断与模型切换之后,系统会主动向上下文补入一段说明,告知模型此前发生了什么。这一类 "将 harness 自身动作告知模型" 的片段,是长会话不发生偏移的关键,也是自研实现中最易遗漏的一类。
上下文压缩:本地实现与远端实现
上下文压缩在 core 中不是单个函数,而是一组文件,本地压缩之外另有一整套远端压缩:compact.rs、compact_token_budget.rs、compact_model_fallback.rs、compact_remote.rs、compact_remote_v2.rs、compact_remote_history.rs、compact_remote_request.rs、compact_remote_v2_attempt.rs,以及专门处理图片额度的 compact_remote_v2_images.rs。
远端压缩由 [features] 中的 remote_compaction 开关控制,默认开启,但仅在 ChatGPT 鉴权下可用。其含义是将压缩交由服务端完成,而非在本地另发一次模型请求进行摘要。
压缩路径中连图片占用多少额度都单列为一个文件,据此可以推断,该路径已经过相当多轮的生产验证。compact_model_fallback.rs 同样值得注意,它表明压缩所用的模型可以与主模型不同,且在失败时具备降级路径。
值得注意的是压缩的代价。压缩是一次有损操作,必然带来信息丢失与摘要偏差。企业场景中通常需额外配置一套关键事实保护机制,用于保留订单号、金额、变更单号一类不可被摘要的字段,并保存一份可追溯的压缩前后对照日志。
工具子系统
core/src/tools/ 自成一个子系统,其文件划分已经把职责界定清楚:
| 文件 | 职责 |
|---|---|
registry.rs |
工具注册表,记录哪些工具被注册 |
router.rs |
路由,把模型发出的函数调用解析到具体处理器 |
orchestrator.rs |
编排,一批调用如何组织 |
parallel.rs |
并发,哪些调用可以并行 |
lifecycle.rs |
生命周期,开始、结束与清理 |
approvals.rs |
审批,何时必须中止并征询用户 |
network_approval.rs |
网络审批,出站访问单独成一条路径 |
sandboxing.rs |
沙箱,技术边界如何施加于本次执行 |
tool_dispatch_trace.rs |
调度轨迹,一次调用经过哪些环节 |
hosted_spec.rs |
托管侧的工具声明 |

值得注意的是,审批与沙箱在此处是两个独立文件,而非一个。这与后文将展开的那条分工一致,沙箱定义的是技术上做得到什么,审批定义的是当前被允许做什么,二者不可相互替代。
handlers/ 子目录构成实际的功能清单,是 Codex 暴露给模型的能力边界:
- 文件与补丁 :
apply_patch.rs,配套一个apply_patch_spec.rs描述工具 schema。 - 命令执行 :
shell_spec.rs是默认的 shell 工具,unified_exec/下的exec_command.rs与write_stdin.rs是常驻 PTY 会话版本,对应[features]中的unified_exec开关。 - 上下文自省 :
get_context_remaining.rs供模型查询自身剩余的窗口量,new_context_window.rs主动开启一个新窗口。 - 计划 :
plan.rs。 - 等待 :
sleep.rs与wait_for_environment.rs。 - 工具发现 :
tool_search.rs,工具数量增多之后按需检索,而非全量前置。 - 图片 :
view_image.rs。 - 时间 :
current_time.rs。 - 人机交互 :
request_user_input.rs与request_user_input_async.rs,以及request_permissions.rs。 - 插件 :
list_available_plugins_to_install.rs与request_plugin_install.rs,即模型可以主动要求安装一个插件。 - MCP :
mcp.rs,加上mcp_resource/下的list_mcp_resources.rs、list_mcp_resource_templates.rs、read_mcp_resource.rs。 - 动态注册 :
dynamic.rs与extension_tools.rs,扩展带来的工具走这条路。
任务类型
core/src/tasks/ 将一次 turn 所要执行的工作分为四种类型,各有独立文件,另有一个 lifecycle.rs 负责生命周期:
-
regular.rs:普通的用户任务。 -
compact.rs:压缩任务,即压缩在 Codex 中被建模为一种任务,而非主循环中的一个分支。 -
review.rs:评审任务,对应它那套 Agent 互审的工作流。 -
user_shell.rs:用户自行执行的 shell 命令,与模型发起的执行分开建模。
将压缩与评审升格为任务类型,其收益在于二者自动继承了任务应有的全部性质,包括持久化、可取消与可观测。这一做法优于在主循环中增加一个 if is_compacting 分支,后者会使每一项通用能力都需为压缩再补一次特例。
multi-Agent
多 Agent 的运行时部分位于 core/src/agent/,包括 agent_resolver.rs(决定本次由哪个 Agent 承担)、registry.rs(注册表)、role.rs(角色)、control.rs(控制)、status.rs(状态),以及一个存放内置 Agent 的 builtins/ 目录。角色被单列为一个概念,据此可以推断,Multi-Agent 在 Codex 中并非简单地派生子进程,而是带有角色定义的实体。
Multi-Agent 工具在这份清单中需单独讨论,因为仓库内两代实现并存。
- 第一代位于
multi_agents/之下,包含spawn、wait、send_input、close_agent、resume_agent五个动作; - 第二代位于
multi_agents_v2/之下,改为spawn、wait、send_message、message_tool、list_agents、interrupt_agent、followup_task,另附一个analytics.rs。
两代对照可以看出多 Agent 在实践中被修改了哪些方面:
| 维度 | 第一代 | 第二代 |
|---|---|---|
| 通信 | send_input,语义近于写入标准输入 |
send_message,改成消息语义 |
| 可见性 | 不具备列举能力 | 增加 list_agents,主 Agent 能看到有哪些子 Agent 在跑 |
| 中断 | close_agent,只能关闭 |
增加 interrupt_agent,能打断但不销毁 |
| 续跑 | resume_agent |
改为 followup_task,语义由 "恢复一个 Agent" 转为 "向其追加一个任务" |
| 观测 | 无 | 单列 analytics.rs |
可见第二代的改动方向是一致的,即将 Sub-Agent 由 "一个被输入数据的进程" 转变为 "一个可被列举、可被打断、可被追加任务的协作对象"。这条演化路径对自研 Multi-Agent 具有直接的参考价值,即:先实现出来的往往是进程管理语义,而投入使用之后真正需要的是协作语义。
Guardian 安全复核
core/src/guardian/ 是一个独立的安全复核子系统,ext/ 之下另有一个 guardian-v2 crate。
其文件构成已将机制交代清楚:
policy.md与policy_template.md:策略以 Markdown 书写,即该环节是一次带提示词的复核,而非纯规则匹配。prompt.rs:提示词的组装。review.rs与review_session.rs:复核动作与复核会话,复核本身是有状态的。approval_request.rs:复核结论转成审批请求。metrics.rs:复核的指标。
配套的上下文片段亦可对应,context/ 之下有 guardian_policy.rs、guardian_approved_action.rs、guardian_review_evidence.rs、guardian_followup_review_reminder.rs、guardian_node_repl_policy.rs 五个。
值得注意的是,这一子系统在 OpenAI 的公开叙述中几乎不被提及,而目录结构与文件职责是可核验的证据。它与 Claude Code 自动模式中的记录分类器属于同一类机制,即:以一次额外的模型调用,在危险动作执行之前替人类审批者先行判断。两家独立地收敛到同一解法,可作为该环节必要性的一项旁证。
前端层:TUI、exec 与 app-server
三个前端并列,共用同一个 Core,其各自定位如下。
tui:交互式终端界面,基于 ratatui。[features]中另有一个tui2开关,对应实验中的新实现。exec:非交互式入口,即codex exec,面向脚本、CI 任务与一次性后台任务,执行一段有界的工作流并返回结构化输出。app-server:长驻服务进程,供富客户端使用,VS Code 扩展即其参考实现。

app-server 的协议原语
app-server 以 codex app-server 启动,与客户端之间做双向的 JSON-RPC 2.0 通信。传输有三种,stdio(默认,按行分隔的 JSON)、WebSocket,以及 Unix 套接字上的 WebSocket^3^。
该协议仅有三个原语:
- Thread:一段会话,内含多个 turn。
- Turn:一次用户请求加上随后 Agent 的工作。
- Item:消息、命令执行、文件变更与工具调用一类的条目。
会话操作覆盖得相当完整,包括启动、续跑、分叉(可指定自哪一个 turn 分叉,亦可为临时分叉)、读取、列举、压缩、归档与删除。
用户插话与中断
前文已述,用户的中途输入在 Codex 中是协议的一等公民,在 app-server 这一层它有确切的方法名:
turn/steer:向一个进行中的 turn 追加输入。要求匹配预期的 turn 标识,不接受参数覆盖,也不会重新发出 turn 开始事件。turn/interrupt:取消并将该 turn 终止为 "已中断"。
这两个方法的约束条件比方法本身更值得分析。要求匹配预期的 turn 标识,其目的是防止用户意图追加至 turn A 的输入被注入到已经开始的 turn B;不重新发出 turn 开始事件,其目的是使客户端的状态机无须为追加输入设置特例。
审批是服务端反向发起的请求
这是 app-server 协议中最典型的双向调用。服务端会反向向客户端发起请求,包括命令执行审批与文件变更审批,客户端的可选答复为 "同意"、"本会话内同意"、"拒绝" 与 "取消"。
其中一处细节值得单独记录,网络类审批按主机、协议与端口归组,因此一次答复即可放行多个排队中的同类请求,即 Agent 连续访问同一域名下的十个路径,用户只需批准一次。
文档对自身局限的交代同样值得记录。其中明确要求将 item/completed 视为权威状态,因为某些聚合事件会先行发出空数组;最后一个订阅者断开之后,线程仍会驻留一个 30 分钟的无活动窗口方才卸载;过载时返回 JSON-RPC 的 -32001,并要求客户端采用指数退避加抖动。官方同时标注,app-server 命令与 WebSocket 传输仍属实验性,不支持生产负载。

执行与沙箱
沙箱定义能做到什么,审批定义当前允许什么
Codex 将沙箱与审批视为两个协同但相互独立的控制。
- 沙箱由操作系统原生机制强制,分三档;
- 审批策略决定何时必须中止并征询用户,有四种取值^4^。
沙箱档位(sandbox_mode) |
含义 |
|---|---|
read-only |
只读 |
workspace-write |
可写工作区,官方称之为低摩擦默认档 |
danger-full-access |
无限制,仅在环境本身已经隔离时使用 |
审批策略(approval_policy) |
含义 |
|---|---|
untrusted |
最严,几乎每一步都需征询 |
on-request |
默认,仅越界时征询 |
on-failure |
仅在失败之后征询 |
never |
从不征询 |
二者组合构成一些常用的权限选项,例如: --full-auto 就是 "可写工作区+按需审批"。
workspace-write 档另有一组独立的配置项:
toml
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
exclude_tmpdir_env_var = false # 是否排除 $TMPDIR
exclude_slash_tmp = false # 是否排除 /tmp
writable_roots = ["/Users/YOU/.pyenv/shims"]
network_access = false # 出站网络默认关闭,需显式打开

这个二维控制设计的分工需要表述准确。沙箱定义的是技术上做得到什么,审批定义的是当前被允许做什么。二者不可相互替代,因为前者是能力边界,后者是授权判断。正因如此,Codex 的取向是能力最小化,默认即处于受限环境之中,低风险动作直接放行,越界请求交由策略判断,必要时才升级为人工审批。这与另一种常见但脆弱的思路正相反,后者先将权限全部授出,再于提示词中要求模型谨慎使用。安全约束越靠近执行层越可靠,而写在提示词中的约束,其性质只是建议。
就沙箱的实现而言,Codex 沙箱后端支持:
- macOS 的 Seatbelt
- Linux 的 bubblewrap(以 landlock 作兜底)
- Windows 的受限令牌沙箱
由 [features] 中的 experimental_windows_sandbox 与 elevated_windows_sandbox 两个开关控制。
高层的权限配置会被转换为各操作系统各自的策略格式再行下发,这种高层声明、按平台翻译的做法,使同一套权限意图能够落到不同的操作系统机制之上。
.rules:命令白名单
这一处是 Codex 权限体系中最具体、也最易迁移的设计。
一个显著的问题是:沙箱是粗粒度控制,而实践中存在一类命令,它们本身安全,却又必须在沙箱之外执行,例如:只读的远端查询 gh pr view。若因其需要联网便放开整个沙箱,则安全边界形同虚设;若逐次弹窗询问,则用户很快对提示产生疲劳,转为习惯性批准,安全提示同样失效。
Codex 的处理方式是设置一个独立的策略文件层。启动时它会加载 ~/.codex/rules 下的每一个 *.rules 文件,语法为 Starlark,一种语法接近 Python、但被设计为可安全嵌入且无副作用的语言。其核心构造是 prefix_rule():
python
# 允许以 `gh pr view` 开头的命令在沙箱外运行
prefix_rule(
pattern = ["gh", "pr", "view"],
decision = "allow",
match = [
"gh pr view 7888",
"gh pr view --repo openai/codex",
"gh pr view 7888 --json title,body,comments",
],
not_match = [
"gh pr --repo openai/codex view 7888",
],
)
其四个参数各有明确语义:
pattern(必填) :一个非空列表,每个元素是一个字面量,或一组字面量的并集。其匹配对象是命令的前缀,即execvp(3)参数列表的开头。写作["gh", "pr", ["view", "list"]]则同时放行gh pr view与gh pr list。decision(默认allow) :取值有allow(自动在沙箱外运行,不询问用户)、prompt(每次调用询问一次)、forbidden(直接拒绝,且不通知用户)。多条规则同时命中时取最严的一条,优先级为forbidden高于prompt,prompt高于allow。match与not_match(默认空) :二者是内联的单元测试,分别给出应当命中与不应当命中的用例。测试不通过,则该.rules文件加载失败。
本文认为 match 与 not_match 是全篇单点收益最高的一处设计。它正面处理了 "安全规则本身也可能写错" 这一问题,用例与规则同处一个文件,加载即校验,规则写错在启动阶段即会报出,而非等到某日放行了一条不应放行的命令。
与之配套的是一个离线校验工具,codex execpolicy check 可在不修改规则文件的前提下,查询某条命令将被如何判定:
bash
$ codex execpolicy check --pretty --rules ~/.codex/rules/default.rules \
-- gh pr view 7888 --json title,body,comments
{
"matchedRules": [
{
"prefixRuleMatch": {
"matchedPrefix": ["gh", "pr", "view"],
"decision": "prompt"
}
}
],
"decision": "prompt"
}
另有一处形成闭环的细节。用户在 TUI 中将某条命令加入白名单时,Codex 会向 ~/.codex/rules/default.rules 追加一条规则,此后不再询问。换言之,交互式的授权决策最终沉淀为声明式的策略文件,而不保存在某个不可见的内部状态之中。
就自研 Agent 而言,这一层设计的参考意义在于四点:
- 命令白名单应实现为独立的策略文件,而非散落于配置项或代码中的一个数组。
- 匹配单位应为参数列表的前缀,而非字符串。若按字符串前缀匹配,
npm test ; curl attacker.com一类的命令拼接即可绕过检查。 - 多条规则命中时取最严,该语义必须固定,不得由规则的书写顺序决定结果。
- 规则文件应自带测试并在加载时校验,同时提供一个离线的判定查询命令,用于回答某条命令将被如何处理。
沙箱之外的两个文档操作通道
官方文档明确标出了两处运行在沙箱之外的通道:
- 是一个专门的 shell 命令方法,它在沙箱外以完整权限运行,且不继承该线程的沙箱策略;
- 是一组仍属实验性的进程操作方法。
这一点尤须注意。它表明即便设计相当完整的沙箱,也会为可用性保留有文档记录的例外通道。对使用者而言,启用沙箱不等于所有动作均在沙箱之内,真正需要核对的是哪些接口位于边界之外,以及自身的集成是否无意间将其暴露。
会话持久化
除两级冻结之外,这是本文认为 Codex 最值得细读的一个部分,因为它给出了 "只追加日志 + 可重建索引" 这一模式的完整样本。
rollout 文件名的编码规则
会话轨迹称为 rollout,落盘于 ~/.codex 下的 sessions 子目录,已归档者位于 archived_sessions 子目录。文件名由 rollout/src/rollout_file_name.rs 生成,格式如下:
xml
rollout-<YYYY-MM-DDTHH-MM-SS>-<threadId>.jsonl
时间戳固定占 19 个字符,其后为一个连字符,再后为 ID。而经过 revert 的线程,其文件名包含两个 ID:
xml
rollout-<YYYY-MM-DDTHH-MM-SS>-<threadId>_<rolloutId>.jsonl
源码注释对此有明确说明,普通 rollout 的线程 ID 与 rollout ID 为同一取值,而 thread/revert 会在保持线程 ID 稳定的前提下,切换到一个新的、不可变的 rollout 文件。
换言之,Codex 并不回溯改写已有日志,而是新建一个文件,并在文件名中记录其所属线程。由此,日志始终保持只追加,历史版本仍完整保留于磁盘,而 "这个任务" 在用户视角中的身份未发生变化。
十一种记录类型
JSONL 的每一行是一个 RolloutItem,其枚举变体共 11 种,可从 rollout/src/metadata.rs 中的穷举匹配读出:
| 记录类型 | 承载什么 |
|---|---|
SessionMeta |
会话头,整个文件的第一类记录 |
ResponseItem |
模型响应 |
TurnContext |
turn 级的配置快照 |
WorldState |
世界状态 |
Compacted |
一次压缩事件 |
EventMsg |
事件消息 |
TokenUsageRecord |
token 用量 |
SecurityRiskScore |
安全风险评分 |
RealtimeItem |
实时会话条目 |
InterAgentCommunication |
Agent 之间的通信 |
InterAgentCommunicationMetadata |
上一项的元数据 |
这份清单本身构成一份架构说明。据此可以确认,落盘的不只是对话记录,而是将 turn 级配置、世界状态、压缩事件、token 用量、安全评分与 Agent 间通信一并写入同一条只追加的流。恢复时所面对的不是一个最终答案,而是一段可逐步检查的执行历史。
SessionMeta 的字段可由 builder_from_session_meta 反推,包括 id、timestamp、source、history_mode、model_provider、agent_nickname、agent_role、agent_path、cwd、cli_version、memory_mode,加一个可选的 git 段落,其中带有 commit_hash、branch、repository_url,以及分叉相关的 forked_from_id、forked_from_ordinal_exclusive 与 history_base。
值得注意的是 WorldState 这一类记录的存在。与之配套的是一套增量机制,世界状态采用 RFC 7386 的 merge patch,仅重新发出发生变化的那部分配置,因此 rollout 中保存的是增量,而非每次的全量快照。这一点直接决定了长会话的日志体积。
JSONL 是真相源,SQLite 是可重建的索引
只追加日志有一处固有短板,即查询困难。若要列出 "最近修改的十个会话" 或 "某个仓库下的所有会话",依靠扫描 JSONL 并不现实。Codex 的处理方式是在其旁挂一个 SQLite 索引库,并将该索引明确定义为可从日志重建的派生物。
重建逻辑位于 rollout/src/metadata.rs 的 backfill worker 之中,其机制包含以下几项:
- 租约 :
BACKFILL_LEASE_SECONDS为 900 秒。多个进程同时启动时,由try_claim_backfill竞争租约,未取得租约的进程直接跳过,并在日志中记录已有 worker 正在执行。 - 批处理 :
BACKFILL_BATCH_SIZE为 200,每批结束落一次检查点。 - 水位线 :每个 rollout 文件按相对
~/.codex的路径算出一个 watermark 字符串,排序后只处理大于上次水位线的那些,即中断之后可以续跑,无须从头重扫。 - 状态机 :backfill(追溯装填)具有
Complete与Running等状态,已完成者直接返回。 - 冲突策略 :重建时若 SQLite 中已存在该线程的记录,则保留既有的 git 信息与用户显式设置过的标题(
prefer_existing_git_info、prefer_existing_explicit_title),不为日志中的旧值所覆盖。

"日志 + 索引" 组合的价值在于它将两个诉求分离。日志负责正确性与可追溯,索引负责查询性能,且索引随时可删除重建。就自研 Agent 而言,其参考意义在于三点:
- 会话的真值来源应采用只追加的日志,落盘之后不再改写。若需变更语义,则新建文件,并在文件名或文件头部记录其来历。
- 需要查询的字段单独建立索引,并明示索引为派生物。索引损坏时的处置方式是重建,而非修补。
- 重建 worker 必须自带并发保护与断点续跑。租约与水位线二者构成最小配置,缺少任何一项,在多进程环境下均会出现问题。
revert 与 fork
分叉在 Codex 中有一个容易被忽略的精确定义。forked_from_ordinal_exclusive 记录的是逻辑上的分叉截断点,而 history_base 记录的是历史基线。源码注释专门说明了二者不可混用的原因,早期的 rollout 缺少显式截断点,仅当 history_base 直接指向逻辑父线程,或当前文件即为该线程最初的那个 rollout 时,以其作为截断点才是安全的。对于语义不明的早期 revert,宁可返回空值,也不报出另一个线程的边界。

这一类 "宁可不答,也不给出一个可能错误的答案" 的取舍,在状态恢复代码中是正确的。给出错误的分叉点,将使恢复出的会话表面正常而内容错位,其排查难度远高于直接失败。
扩展机制:ext/ 下的十四个 crate
Codex 并非单体实现,它具备明确的扩展机制。ext/ 目录下有 14 个独立 crate,其中 extension-api 为契约本身,其余十三个是依该契约挂载的能力:
| crate | 能力 |
|---|---|
extension-api |
扩展契约本身 |
agent |
多 Agent |
connectors |
外部连接器 |
goal |
目标驱动的循环 |
git-attribution |
git 归属标注 |
guardian-v2 |
第二代安全复核 |
history-notes |
历史笔记 |
image-generation |
图片生成 |
items |
条目模型 |
mcp |
MCP 集成 |
memories |
记忆 |
queue |
队列 |
skills |
技能 |
web-search |
网页搜索 |

值得注意的是这一机制的性质。Codex 采取的是内核加扩展的形态,core 具有特权,扩展挂载于其旁侧。这与另一种更为激进的做法不同,DeepSeek 的 dsh 不设特权内核,连主循环都是插件。二者体现的是不同的可替换性取舍,Codex 保留一个稳定的语义中心以换取演进速度,其代价是内核本身不可替换。
与之配套的还有 core/src/plugins/ 与 utils/plugins,以及上下文侧的 plugin_instructions.rs、available_plugins_instructions.rs、recommended_plugins_instructions.rs 三个片段。换言之,插件不只是被加载,它还会主动向上下文注入自身说明,并向模型推荐可安装的对象。前文工具清单中的 request_plugin_install 即这条路径的终点,模型可以主动提出安装某个插件。
配置文件
前述所有机制的对外配置项收在同一个文件之中,即 ~/.codex/config.toml,CLI 与 IDE 扩展共用同一份^5^。
config.toml 的主干
toml
# 模型与后端
model = "gpt-5"
model_provider = "ollama"
model_reasoning_effort = "high"
model_reasoning_summary = "none"
model_verbosity = "low"
model_context_window = 128000
# 安全边界
approval_policy = "on-request"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = ["/Users/YOU/.pyenv/shims"]
network_access = false
# 自定义模型后端
[model_providers.azure]
name = "Azure"
base_url = "https://YOUR_PROJECT.openai.azure.com/openai"
env_key = "AZURE_OPENAI_API_KEY"
query_params = { api-version = "2025-04-01-preview" }
wire_api = "responses"
[model_providers.openai]
request_max_retries = 4
stream_max_retries = 10
stream_idle_timeout_ms = 300000
# MCP server
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
# 子进程环境
[shell_environment_policy]
inherit = "none"
set = { PATH = "/usr/bin", MY_FLAG = "1" }
exclude = ["AWS_*", "AZURE_*"]
include_only = ["PATH", "HOME"]
# 遥测
[otel]
environment = "staging"
exporter = "none"
log_user_prompt = false
# 外部通知
notify = ["python3", "/path/to/notify.py"]
其中几处需要单独说明。model_providers 中可为每个后端单独配置重试与流空闲超时,据此可以推断,多模型接入并非简单更换一个 base_url,而是承认不同后端的稳定性特征存在差异。wire_api 区分 responses 与 chat 两种线路协议,而 model_verbosity 仅对 Responses API 生效,Chat Completions 后端会将其忽略。
notify 这一接口极为轻量,它仅在受支持的事件上调起一个外部程序,目前受支持的事件只有 agent-turn-complete 一个,参数为一段 JSON。以实现成本衡量,这是笔者所见成本最低的通知集成方案,可直接迁移。
[features]:十六个能力开关
Codex 把可选与实验能力全部收进 [features] 表,每个开关都有明确的默认值与成熟度分级:
| 开关 | 默认 | 阶段 | 作用 |
|---|---|---|---|
undo |
true | 稳定 | 以每轮的 git ghost 快照支持撤销 |
parallel |
true | 稳定 | 允许支持的模型并行调多个工具 |
shell_tool |
true | 稳定 | 启用默认的 shell 工具 |
warnings |
true | 稳定 | 将工具使用告警发送给模型 |
view_image_tool |
true | 稳定 | 启用 view_image 工具 |
web_search_request |
false | 稳定 | 允许模型发起网页搜索 |
skills |
true | 实验 | 启用技能的发现与注入 |
exec_policy |
true | 实验 | 对 shell 与 unified_exec 强制执行策略检查 |
remote_compaction |
true | 实验 | 远端压缩,仅 ChatGPT 鉴权可用 |
apply_patch_freeform |
false | 实验 | 启用自由格式的 apply_patch 工具 |
experimental_windows_sandbox |
false | 实验 | Windows 受限令牌沙箱 |
elevated_windows_sandbox |
false | 实验 | 提权版 Windows 沙箱流程 |
remote_models |
false | 实验 | 展示可用性前先刷新远端模型列表 |
unified_exec |
false | Beta | 以 PTY 支撑的统一 exec 工具 |
shell_snapshot |
false | Beta | 快照 shell 环境以加速重复命令 |
tui2 |
false | 开发中 | 实验性的 TUI v2 |
命令行上可一次性开启多项,写作 codex --enable feature_a --enable feature_b。
值得注意的是这张表的信息量。它同时给出三点,即:哪些能力尚未定型、哪些默认关闭因而线上大多无人使用,以及 OpenAI 自身对每一项能力的信心程度。此外,一批遗留的布尔配置项,例如 tools.web_search、experimental_use_unified_exec_tool 与 include_apply_patch_tool,已被标记为弃用,并要求迁移至对应的 [features] 键。
将实验开关收进一张带默认值与阶段标记的表,其可维护性显著优于散布在各处的布尔配置项。就自研 Agent 而言,这是一项可直接采用的做法,即:所有未定型的能力集中于一处声明,每一项写明默认值与成熟度,弃用的旧键保留一个版本周期并给出迁移提示。
profiles 与四级优先级
profiles 将一组配置打包,切换时无须修改 config.toml 的顶层条目:
toml
model = "gpt-5-codex"
approval_policy = "on-request"
[profiles.deep-review]
model = "gpt-5-pro"
model_reasoning_effort = "high"
approval_policy = "never"
[profiles.lightweight]
model = "gpt-4.1"
approval_policy = "untrusted"
以 codex --profile deep-review 启动即可,亦可在顶层写入 profile = "deep-review" 将其设为默认。
取值的优先级共四级,由高至低为命令行显式参数、profile 中的取值、config.toml 的顶层条目、CLI 内置默认值。

官方对这一优先级设计的用法说明得相当明确,即将共性设置置于顶层,各 profile 只覆盖其需要调整的字段。
shell_environment_policy
子进程的环境变量另有一层独立策略,用于控制 Codex 传递给其所启动的任何子进程的环境。该策略可从干净状态起步(inherit = "none"),亦可从一个精简集合起步(inherit = "core"),再叠加排除、包含与覆盖三类规则。
匹配采用大小写不敏感的 glob,支持 *、? 与 [A-Z]。其中有一处默认行为需予注意,ignore_default_excludes = false 会在用户自定义规则之前,先执行一遍自动的 KEY / SECRET / TOKEN 过滤。
换言之,默认情况下带敏感字样的环境变量不会泄漏进子进程,而这一层保护需显式关闭才会失效。对一个会执行任意命令的 Agent 而言,这是必须具备的一道防线,且其位置的选择颇为关键,它在 harness 将环境交给子进程的那一刻生效,不依赖模型是否配合。
小结:六处可迁移的设计决策
通读整个仓库之后,本文认为最值得迁移至自研 Agent 的是以下六处,按优先级排列:
- 每次采样前冻结一份决策视图 。历史、环境、工具定义与权限均取自同一个不可变快照,宣告给模型的与执行时所用的必须同源。这是
TurnContext与StepContext那一层的核心,也是避免 "模型以为在 A、副作用发生在 B" 这类错位的根本手段。 - 只追加日志加可重建索引。真值来源是不再改写的 JSONL,查询由其旁的 SQLite 承担,索引明示为派生物,损坏时重建而非修补。重建 worker 必须自带租约与水位线。
- 命令白名单做成带测试的策略文件 。匹配参数列表前缀而非字符串,多规则命中取最严,规则自带
match与not_match并在加载时校验,另配一个离线的判定查询命令。 - 沙箱与审批分成两层。沙箱约束技术上做得到什么,审批约束当前允许做什么,二者不可相互替代,且均须落在执行层,而非提示词之中。
- 上下文按具名片段装配。每一种注入实现为一个有归属、可单测的模块,其中包括将 harness 自身动作(中断、切换模型、压缩)告知模型的那几种。
- 实验能力收进一张带默认值与阶段的开关表。弃用的旧键保留一个版本周期并给出迁移提示。
最后是一条不属于设计条目、但值得记录的观察。Codex 对自身代码与对其所生成应用使用的是同一套方法论,即:先将边界划定,再在边界之内给足自由。它在应用层采用 Types 至 UI 的六层结构加 Providers 单一入口,在自身 harness 中采用 core 加三个并列前端加 ext/ 契约。二者形态不同,思路一致。
参考引用
github.com/openai/code... github.com/openai/code... github.com/openai/code... github.com/openai/code... github.com/openai/code... developers.openai.com/codex/local... developers.openai.com/codex/conce... developers.openai.com/blog/codex-... learn.chatgpt.com/docs/app-se... openai.com/index/harne...