OpenAI CodeX 软件架构解析

OpenAI Codex 架构解析

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

架构分析

如下图所示,Codex 的顶层结构可归纳为七个部分。

  1. Core 引擎corecore-apicore-pluginsstatetoolsprotocol):Agent 主循环、会话与轮次状态、工具注册与执行,以及客户端与引擎之间的线路类型。
  2. 前端层tuiexecapp-server):分别是交互式终端、非交互式批处理、长驻服务进程。三者并列依赖同一个 Core。
  3. 执行与沙箱sandboxinglinux-sandboxbwrapexecpolicyexec-servershell-commandshell-escalationprocess-hardening):技术边界层,模型能否访问某个文件或某个端口,由这一层裁定。
  4. 会话持久化rolloutrollout-tracethread-storehistorymemories/readmemories/write):执行轨迹、线程索引、记忆的读写分离。
  5. 模型接入model-providermodel-provider-infomodels-managerollamalmstudioresponses-api-proxy):多后端接入,支持本地模型。
  6. 上下文装配context-fragmentspromptsskillshooksconfigfeatures):决定每一次模型请求往上下文里放什么。
  7. 扩展机制ext/ 下 14 个 crate):extension-api 为契约本身,其余十三个是依该契约挂载的能力。

此外还有另外的四组横切能力:

  1. 双向 MCP(mcp-server 将 Codex 暴露为 MCP server,rmcp-client 使其作为 MCP client 连接他方)
  2. Code Mode(code-mode 四个 crate,令模型以编写代码的方式编排工具,而非逐个发出函数调用)
  3. 云与协同(cloud-tasksagent-rolesagent-graph-store
  4. 可观测(otelanalyticsdiagnosticsresponse-debug-context)。

Core 引擎

core 是唯一的特权内核。其内部的子目录划分本身即构成一份架构说明,本文按职责将其归为七类,以下逐一展开。

会话、轮次与步

一个长时间运行的 Agent 会话,若只具备 "一次对话" 这一种粒度,在工程上将立即面临三个问题,即:

  1. 进程崩溃之后从何处恢复?
  2. 用户中途追加的输入在哪个边界被领取?
  3. 模型所见的工具集与实际可执行的工具集如何保证一致?

Codex 的处理方式是将粒度划分为三层。

  • Thread(线程) :一段完整的会话,具有稳定的 ThreadId,对应用户视角中 "这个任务" 的单位。
  • Turn(轮):一次用户请求及其后 Agent 的全部工作,是持久化的单位。
  • Step(步):一个 turn 内部的一次采样请求,及该次采样所产出的工具调用与执行。一个 turn 可包含多个 step。

这三层在代码中均有对应实体,位于 core/src/session/ 之下:session.rsturn.rsstep_context.rsturn_context.rsturn_input.rsturn_suspension.rsinput_queue.rscontext_window.rstoken_budget.rsworld_state.rsrollout_reconstruction.rsmulti_agents.rsthread_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 而言,其参考意义在于三点

  1. 每次采样之前应有一个明确的 "决策视图" 对象,历史、环境、工具定义与权限均由其提供,而非各自读取全局状态。
  2. 该视图对象必须是不可变快照,中途的变更只能作用于下一步,不得回溯改写已经宣告出去的那一份。
  3. 宣告与执行必须共用同一来源。若宣告工具集的代码与执行工具的代码各自读取一份配置,则错位只是时间问题。

上下文装配:四十多个具名片段

core/src/context/ 目录下有四十余个文件(按前述计数方式,下同),每个文件负责一种要注入上下文的片段,例如 base_instructions.rsdeveloper_instructions.rsuser_instructions.rsenvironment_context.rscompaction_summary.rspermissions_instructions.rshook_additional_context.rstoken_budget_context.rsturn_aborted.rsplugin_instructions.rsmulti_agent_role_instructions.rscurrent_time_reminder.rsmodel_switch_instructions.rsguardian_policy.rsinter_agent_message.rs,此外还有一个 world_state/ 子目录。

笔者认为这是整个代码仓库中最能体现 Codex 上下文工程成熟度的一处。作为对照,多数项目的做法是在单个函数内做字符串拼接,条件分支逐次累加,最终无法回答某一句提示词来自何处、在何种条件下出现。Codex 的做法是将每一种待注入的内容都实现为具名、有归属、可单独测试的模块。

这一做法带来三点收益:

  1. 上下文的构成可枚举,出现问题时能够定位到具体是哪一个片段污染了模型的判断。
  2. 片段可独立演进,新增一种注入无须改动其他片段的代码。
  3. 也是最具实际价值的一点,token 预算可以核算,因为每一份消耗都有明确归属。

值得注意的是 turn_aborted.rsmodel_switch_instructions.rs 这两个片段的存在,据此可以确认,在中断与模型切换之后,系统会主动向上下文补入一段说明,告知模型此前发生了什么。这一类 "将 harness 自身动作告知模型" 的片段,是长会话不发生偏移的关键,也是自研实现中最易遗漏的一类。

上下文压缩:本地实现与远端实现

上下文压缩在 core 中不是单个函数,而是一组文件,本地压缩之外另有一整套远端压缩:compact.rscompact_token_budget.rscompact_model_fallback.rscompact_remote.rscompact_remote_v2.rscompact_remote_history.rscompact_remote_request.rscompact_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.rswrite_stdin.rs 是常驻 PTY 会话版本,对应 [features] 中的 unified_exec 开关。
  • 上下文自省get_context_remaining.rs 供模型查询自身剩余的窗口量,new_context_window.rs 主动开启一个新窗口。
  • 计划plan.rs
  • 等待sleep.rswait_for_environment.rs
  • 工具发现tool_search.rs,工具数量增多之后按需检索,而非全量前置。
  • 图片view_image.rs
  • 时间current_time.rs
  • 人机交互request_user_input.rsrequest_user_input_async.rs,以及 request_permissions.rs
  • 插件list_available_plugins_to_install.rsrequest_plugin_install.rs,即模型可以主动要求安装一个插件。
  • MCPmcp.rs,加上 mcp_resource/ 下的 list_mcp_resources.rslist_mcp_resource_templates.rsread_mcp_resource.rs
  • 动态注册dynamic.rsextension_tools.rs,扩展带来的工具走这条路。

任务类型

core/src/tasks/ 将一次 turn 所要执行的工作分为四种类型,各有独立文件,另有一个 lifecycle.rs 负责生命周期:

  1. regular.rs:普通的用户任务。

  2. compact.rs:压缩任务,即压缩在 Codex 中被建模为一种任务,而非主循环中的一个分支。

  3. review.rs:评审任务,对应它那套 Agent 互审的工作流。

  4. 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/ 之下,包含 spawnwaitsend_inputclose_agentresume_agent 五个动作;
  • 第二代位于 multi_agents_v2/ 之下,改为 spawnwaitsend_messagemessage_toollist_agentsinterrupt_agentfollowup_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.mdpolicy_template.md:策略以 Markdown 书写,即该环节是一次带提示词的复核,而非纯规则匹配。
  • prompt.rs:提示词的组装。
  • review.rsreview_session.rs:复核动作与复核会话,复核本身是有状态的。
  • approval_request.rs:复核结论转成审批请求。
  • metrics.rs:复核的指标。

配套的上下文片段亦可对应,context/ 之下有 guardian_policy.rsguardian_approved_action.rsguardian_review_evidence.rsguardian_followup_review_reminder.rsguardian_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 沙箱后端支持:

  1. macOS 的 Seatbelt
  2. Linux 的 bubblewrap(以 landlock 作兜底)
  3. Windows 的受限令牌沙箱

[features] 中的 experimental_windows_sandboxelevated_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 viewgh pr list
  • decision(默认 allow :取值有 allow(自动在沙箱外运行,不询问用户)、prompt(每次调用询问一次)、forbidden(直接拒绝,且不通知用户)。多条规则同时命中时取最严的一条,优先级为 forbidden 高于 promptprompt 高于 allow
  • matchnot_match(默认空) :二者是内联的单元测试,分别给出应当命中与不应当命中的用例。测试不通过,则该 .rules 文件加载失败。

本文认为 matchnot_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 而言,这一层设计的参考意义在于四点:

  1. 命令白名单应实现为独立的策略文件,而非散落于配置项或代码中的一个数组。
  2. 匹配单位应为参数列表的前缀,而非字符串。若按字符串前缀匹配,npm test ; curl attacker.com 一类的命令拼接即可绕过检查。
  3. 多条规则命中时取最严,该语义必须固定,不得由规则的书写顺序决定结果。
  4. 规则文件应自带测试并在加载时校验,同时提供一个离线的判定查询命令,用于回答某条命令将被如何处理。

沙箱之外的两个文档操作通道

官方文档明确标出了两处运行在沙箱之外的通道:

  1. 是一个专门的 shell 命令方法,它在沙箱外以完整权限运行,且不继承该线程的沙箱策略;
  2. 是一组仍属实验性的进程操作方法。

这一点尤须注意。它表明即便设计相当完整的沙箱,也会为可用性保留有文档记录的例外通道。对使用者而言,启用沙箱不等于所有动作均在沙箱之内,真正需要核对的是哪些接口位于边界之外,以及自身的集成是否无意间将其暴露。

会话持久化

除两级冻结之外,这是本文认为 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 反推,包括 idtimestampsourcehistory_modemodel_provideragent_nicknameagent_roleagent_pathcwdcli_versionmemory_mode,加一个可选的 git 段落,其中带有 commit_hashbranchrepository_url,以及分叉相关的 forked_from_idforked_from_ordinal_exclusivehistory_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(追溯装填)具有 CompleteRunning 等状态,已完成者直接返回。
  • 冲突策略 :重建时若 SQLite 中已存在该线程的记录,则保留既有的 git 信息与用户显式设置过的标题(prefer_existing_git_infoprefer_existing_explicit_title),不为日志中的旧值所覆盖。

"日志 + 索引" 组合的价值在于它将两个诉求分离。日志负责正确性与可追溯,索引负责查询性能,且索引随时可删除重建。就自研 Agent 而言,其参考意义在于三点:

  1. 会话的真值来源应采用只追加的日志,落盘之后不再改写。若需变更语义,则新建文件,并在文件名或文件头部记录其来历。
  2. 需要查询的字段单独建立索引,并明示索引为派生物。索引损坏时的处置方式是重建,而非修补。
  3. 重建 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.rsavailable_plugins_instructions.rsrecommended_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 区分 responseschat 两种线路协议,而 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_searchexperimental_use_unified_exec_toolinclude_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 的是以下六处,按优先级排列:

  1. 每次采样前冻结一份决策视图 。历史、环境、工具定义与权限均取自同一个不可变快照,宣告给模型的与执行时所用的必须同源。这是 TurnContextStepContext 那一层的核心,也是避免 "模型以为在 A、副作用发生在 B" 这类错位的根本手段。
  2. 只追加日志加可重建索引。真值来源是不再改写的 JSONL,查询由其旁的 SQLite 承担,索引明示为派生物,损坏时重建而非修补。重建 worker 必须自带租约与水位线。
  3. 命令白名单做成带测试的策略文件 。匹配参数列表前缀而非字符串,多规则命中取最严,规则自带 matchnot_match 并在加载时校验,另配一个离线的判定查询命令。
  4. 沙箱与审批分成两层。沙箱约束技术上做得到什么,审批约束当前允许做什么,二者不可相互替代,且均须落在执行层,而非提示词之中。
  5. 上下文按具名片段装配。每一种注入实现为一个有归属、可单测的模块,其中包括将 harness 自身动作(中断、切换模型、压缩)告知模型的那几种。
  6. 实验能力收进一张带默认值与阶段的开关表。弃用的旧键保留一个版本周期并给出迁移提示。

最后是一条不属于设计条目、但值得记录的观察。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...

Footnotes

  1. developers.openai.com/blog/codex-...

  2. github.com/openai/code...

  3. learn.chatgpt.com/docs/app-se...

  4. developers.openai.com/codex/conce...

  5. developers.openai.com/codex/local...

相关推荐
kyriewen16 分钟前
别再只给人写页面了:AI 已经开始自己点你的按钮、填你的表单
前端·javascript·人工智能
Rauser Mack37 分钟前
桌面AI的范式转变:AI Agent 安全架构与多工作空间隔离实践
人工智能·安全·安全架构
衡石科技40 分钟前
HENGSHI BOX|全域智控,私域安全的ChatBI一体机
大数据·人工智能·安全
月华路42 分钟前
《模型不玄学》小白的模型算法实战手册 - 前言
人工智能·机器学习
Capricorn198844 分钟前
防编造架构实战:对比 Gemini Notebook 解析知芽 Notebook Skill 的工程实现
人工智能·笔记·elasticsearch·架构·知识图谱·论文笔记
财迅通Ai1 小时前
光智科技深耕晶体生长技术 构建多领域同源技术布局
人工智能·科技·光智科技
roamingcode1 小时前
4.5 小时,从一句话需求到可安装的 Chrome 插件:一次 AI 结对开发的完整复盘
前端·人工智能·chrome·claude·codex
2601_967659911 小时前
2026选购科普:做静音床垫的品牌都有哪些
人工智能
飞Link1 小时前
动作方法中的分割与过度分割方法全解析(含代码实战与踩坑案例)
人工智能·python·算法·计算机视觉