Overview

这篇文章想系统回答一个问题:Codex 到底是怎么工作的

很多人第一次接触 Codex 时,都会有一种很强的”违和感”:

  • 它不只是聊天,而是真的会读代码、改文件、跑命令;
  • 它不是每次都等命令跑完才说话,而是会一边看输出一边继续推进任务;
  • 它看上去像一个模型,但很多关键行为其实并不发生在模型内部;
  • 它既能在本地执行,也能在云端环境里跑,而且这两种模式的安全边界还不完全一样。

如果只把 Codex 看成”一个会写代码的 LLM”,很多现象会解释不清。更准确的理解方式是:

Codex 是一个由模型负责决策、由 harness 负责编排、由 runtime 负责执行、并由 sandbox 与 approvals 负责边界控制的 agent system。

这篇文章会把这个系统拆开来看。重点不是介绍某个 API 参数,而是建立一个稳定的心智模型:当用户发出一个请求后,究竟发生了什么,哪些职责属于模型,哪些职责属于 harness,哪些职责属于 runtime,沙盒和审批又分别控制什么。

在进入细节之前,先给出一幅 Codex 的模块全景图,帮助你在阅读过程中始终有一个整体视角:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
codex-rs/                          # Codex 主仓库(Rust 实现)
├── core/                          # 核心引擎
│   └── src/
│       ├── codex.rs               # 主入口:Session, TurnContext, agent loop
│       ├── agent/                 # Agent 生命周期管理、角色、状态追踪
│       ├── tools/                 # 工具系统
│       │   ├── registry.rs        # 工具注册表(name → handler 映射)
│       │   ├── router.rs          # 工具路由(分发模型发出的 tool call)
│       │   ├── orchestrator.rs    # 工具编排器(审批 → 沙盒 → 执行 → 重试)
│       │   └── handlers/          # 具体工具处理器
│       │           ShellHandler, ApplyPatchHandler,
│       │           McpHandler, SpawnAgentHandler, ...
│       ├── context_manager/       # 上下文管理(对话历史、token 追踪)
│       ├── compact.rs             # 自动上下文压缩
│       ├── sandboxing/            # 沙盒适配器类型
│       ├── exec.rs                # 底层命令执行(进程 spawn、stdout 捕获)
│       └── spawn.rs               # 子进程启动(PTY 支持)
├── sandboxing/                    # 沙盒平台实现(独立 crate)
│   ├── seatbelt.rs                # macOS Seatbelt (sandbox-exec)
│   ├── landlock.rs                # Linux Landlock LSM
│   └── windows.rs                 # Windows sandbox
├── tui/                           # 终端 UI(基于 ratatui)
├── app-server/                    # IDE 集成服务(JSON-RPC 2.0)
├── execpolicy/                    # 执行策略与审批逻辑
├── hooks/                         # 钩子系统(PreToolUse / PostToolUse / AfterAgent)
├── skills/                        # Skills 系统(SKILL.md 驱动)
└── protocol/                      # 共享协议类型

下文会逐一展开这些模块之间的协作关系。

Why Codex Feels Different

Codex 和普通对话模型最大的差别,不在于“更会写代码”,而在于它被放进了一个可执行的闭环里。

普通聊天模型的典型行为是:

  1. 读取上下文;
  2. 生成一段回复;
  3. 结束这一轮。

而 Codex 的典型行为更像:

  1. 读取上下文和工具定义;
  2. 决定下一步动作;
  3. 调用工具;
  4. 观察工具结果;
  5. 根据新观察继续推理;
  6. 重复这个循环,直到任务结束。

因此,Codex 的关键特征不是“回答得更聪明”,而是它能把用户目标拆成一系列可执行动作,并在执行结果的反馈下持续调整后续行为。

这也是为什么理解 Codex 时,只看模型往往不够。很多决定产品体验的关键能力,例如:

  • 能不能真正跑 shell 命令;
  • 命令失败后能不能恢复;
  • 写文件时有没有边界;
  • 什么时候需要用户审批;
  • 长命令的输出怎么流式展示;
  • 多个 agent 怎样协同;

这些都不只属于模型,而属于模型外面的整套执行系统。

Message Roles and Instruction Hierarchy

先看最上层的上下文结构。

过去很多人熟悉的 chat 格式里,角色通常只有:

  • system
  • user
  • assistant

但在现在的 Codex / OpenAI agent 语境里,developer 同样是核心角色。它往往承载的是宿主程序写给模型的工作规则,而不是普通对话内容。

可以粗略理解为:

  • system:平台级规则;
  • developer:应用级规则,也就是 Codex 这个产品要求 agent 如何工作;
  • user:当前任务;
  • assistant:模型之前已经给出的自然语言回复或工具调用结果。

在 Codex 里,developer 往往会规定这类事情:

  • 你是一个 coding agent;
  • 先读代码再改代码;
  • 优先使用哪些工具;
  • 输出分成 commentaryfinal
  • 不要做哪些危险 git 操作;
  • 什么时候要更新用户进度;
  • 什么情况下必须遵守沙盒和审批策略。

所以在架构上,developer 更像一层应用运行规则,而不是普通聊天历史。Codex 之所以表现得像“有工作流程”的 agent,很大程度上就是因为它始终处在一套强约束的指令体系之下。

Tools: Spec vs Implementation

再往下看工具。

模型并不会直接看到工具的源码实现。它首先看到的是一份 tool spec,也就是结构化接口描述。这个 spec 通常会告诉模型:

  • 工具名是什么;
  • 这个工具做什么;
  • 参数有哪些;
  • 参数的类型和约束是什么;
  • 结果大概会返回什么结构。

在 Codex 的实际源码中,模型面对的工具类型(ToolPayload 枚举)主要有以下几种:

类型 用途
LocalShell 执行 shell 命令(如 npm testgit status
Function 标准函数调用(如 apply_patch 应用文件补丁)
Mcp 调用 MCP server 提供的外部工具
ToolSearch 搜索当前可用的工具列表
Custom 客户端动态注册的自定义工具

LocalShell 为例,模型看到的 spec 大致包含:

  • name = local_shell
  • 描述:在 PTY 中执行一条 shell 命令
  • 参数可能包括:要执行的命令文本、工作目录、超时设置等

Function 类型的 apply_patch 为例:

  • name = apply_patch
  • 描述:应用一个文件补丁(diff)
  • 参数:补丁内容(标准 unified diff 格式)

模型真正会做的,是基于这些 spec 决定:

  1. 现在需不需要调用工具;
  2. 如果需要,应该调用哪个工具;
  3. 参数该怎么填。

所以从抽象上说:

  • tool spec 是暴露给模型看的接口定义;
  • tool implementation 是宿主程序里真正干活的代码。

在 Codex 源码中,这个对应关系由 ToolRegistry 管理:它维护一张 name → ToolHandler 的映射表。模型发出 tool call 后,ToolRouter 根据 tool name 从 registry 中查到对应的 handler,再由 handler 实际执行。这和传统软件里的”API 文档”和”后端实现”关系非常像——模型看到的不是实现细节,而是能力边界。

除了内建工具外,Codex 还支持通过 MCP(Model Context Protocol) 集成外部工具。McpHandlerMcpResourceHandler 负责与外部 MCP server 通信,使 Codex 能够调用第三方提供的工具能力(例如数据库查询、API 调用等),从而扩展工具生态。

Harness: The Control Plane

接下来进入最容易混淆、但也最重要的一层:harness

如果用一句话定义:

Harness 是包在模型外面、把用户任务组织成持续 agent loop 的控制系统。

它关注的不是”某条命令怎么 spawn”,而是更上层的问题:

  • 这一轮该不该调用工具;
  • 工具调用前要不要审批;
  • 失败后要不要重试;
  • 当前任务是否已经完成;
  • 输出该展示给用户什么;
  • 多 agent 之间如何分工;
  • 哪些规则需要始终保留在上下文里。

因此,harness 更像 control plane。它在意的是任务过程的组织、约束与调度。

在 Codex 语境里,harness 一般会包含这些职责:

  1. Agent loop orchestration 把”读上下文 -> 选动作 -> 调工具 -> 看结果 -> 再决策”组织成一个可持续运行的闭环。源码中这体现在 codex.rs 的主循环:Session 持有全局状态,每次用户输入触发一个新的 TurnContext,turn 内部不断循环直到任务完成或用户中断。

  2. Instruction layeringsystemdeveloperuser 等不同来源的指令组织成上下文,并维护优先级。ContextManager 负责管理对话历史(Vec<ResponseItem>),在发送给模型前做标准化处理(剥离无效项、过滤模态)。

  3. Tool routing 接住模型发出的 tool call,通过 ToolRouter 把它路由给 ToolRegistry 中对应的 ToolHandler。路由过程不仅匹配工具名,还要区分内建工具、MCP 工具和动态注册工具。

  4. Tool orchestration (审批 → 沙盒 → 执行 → 重试) ToolOrchestrator 是工具执行的核心管线。每个 tool call 都要经过:

    • 检查 ExecApprovalRequirement(是否需要审批 / 是否禁止)
    • 选择沙盒策略(首次尝试用哪个沙盒)
    • 在沙盒中执行
    • 如果沙盒拒绝,决定是否升级沙盒(如降级到 SandboxType::None)并重试
  5. State and session management 维护会话状态、长进程 session、子 agent 状态、工具调用历史等。Session 是最顶层的生命周期容器,持有 ContextManager、配置、事件通道、MCP 连接、钩子运行时等。

  6. User interaction 决定什么时候发中间进度、什么时候等待审批、什么时候给最终结果。

  7. Hook dispatch 通过 HookRuntime 分发 PreToolUsePostToolUseAfterAgent 等钩子事件,允许用户自定义脚本在工具执行前后介入。

因此,harness 的关键词不是”执行”,而是编排。它是一个围绕模型的调度壳,确保模型的行为始终在可控、可观测、可恢复的框架内运行。

Runtime: The Execution Plane

与 harness 相对,runtime 更像 execution plane

如果 harness 负责回答”该不该做、怎样组织做”,那么 runtime 负责回答:

既然决定要做了,那这件事怎样在真实环境里安全地执行起来?

runtime 更靠近操作系统,典型职责包括:

  • 把工具请求转成真实的进程执行;
  • 设置工作目录、环境变量、stdio 策略;
  • 应用沙盒策略;
  • 应用网络限制;
  • 处理超时、取消、输出截断、流式输出;
  • 收集 stdout / stderr 并回传给 harness。

所以更准确的关系不是”harness 和 runtime 并列夹在模型与 OS 中间”,而是:

  • harness 是更外层的控制系统;
  • runtime 往往是 harness 里的执行子系统。

在 Codex 的开源实现里,这个边界是能看到的:

  • execpolicy/ 负责策略定义与审批逻辑,偏向 harness 层的决策;
  • core/src/tools/orchestrator.rsToolOrchestrator,编排”审批 → 选沙盒 → 执行 → 重试”的完整管线,是 harness 与 runtime 的交界;
  • core/src/exec.rs 把工具执行请求构造成真正的操作系统进程;
  • core/src/spawn.rs 负责最终的子进程启动(支持 PTY);
  • sandboxing/ 目录(独立 crate)包含各平台的具体沙盒后端实现:seatbelt.rs(macOS)、landlock.rs(Linux)、windows.rs(Windows);
  • core/src/sandboxing/ 包含沙盒适配器类型,定义了上层与各平台实现之间的接口。

从源码中可以清晰地看到,每个具体工具(如 ShellHandlerApplyPatchHandler)都实现了 ToolRuntime trait,这个 trait 定义了工具与编排器之间的契约:

1
2
3
4
5
6
7
8
pub trait ToolRuntime<Rq, Out> {
    fn exec_approval_requirement(&self, req: &Rq) -> Option<ExecApprovalRequirement>;
    fn sandbox_mode_for_first_attempt(&self, req: &Rq) -> SandboxOverride;
    fn sandbox_preference(&self) -> SandboxablePreference;
    fn escalate_on_failure(&self) -> bool;
    async fn run(&self, req: &Rq, attempt: &SandboxAttempt, ctx: &ToolCtx) -> Result<Out, ToolError>;
    async fn start_approval_async(&self, req: &Rq, ctx: ApprovalCtx) -> ReviewDecision;
}

这意味着 runtime 不是一块模糊的”执行层”,而是每个工具都明确声明自己的审批需求、沙盒偏好和失败升级策略。

因此,一个更贴切的说法是:

Harness 是管”过程”的,runtime 是管”落地执行”的。而两者之间的契约由 ToolRuntime trait 和 ToolOrchestrator 共同定义。

Sandboxing, Approvals, and Trust Boundaries

理解 Codex 时,最容易混淆的另一个点,是把 sandboxapproval 当成一回事。实际上它们是两层不同的控制。

可以这样记:

  • sandbox:技术边界,决定“做不做得到”
  • approval policy:交互边界,决定“越界前要不要先问人”

这两层结合起来,才构成 Codex 的真实信任边界。

Sandbox

沙盒的作用是限制 agent 能碰到什么。

本地 CLI / IDE 场景下,OpenAI 官方文档明确说明 Codex 使用的是 OS-level sandboxing

  • macOS:Seatbelt,底层通过 sandbox-exec 系统调用实现文件系统和网络限制;
  • Linux:Landlock(Linux Landlock LSM),通过 Linux 内核的 Landlock 模块实现细粒度的文件系统访问控制;
  • Windows:独立实现,通过受限 token 和权限控制实现沙盒。

在默认模式下,Codex 一般具有这样的边界:

  • 只能在当前 workspace 内读写;
  • .git.codex.agents 这类路径可以被进一步保护;
  • 网络默认关闭;
  • 通过工具启动的命令同样继承沙盒,而不是只有“内建文件编辑”才受限制。

官方文档里常见的几种模式是:

  • read-only
  • workspace-write
  • danger-full-access

这些模式回答的都是同一个问题:agent 技术上能碰到哪些文件和哪些系统能力

Approval Policy

审批层关心的不是”能不能做到”,而是”遇到越界或高风险动作时,要不要停下来问用户”。

例如:

  • 访问网络;
  • 写工作区之外的目录;
  • 执行不在允许规则内的命令;
  • 执行明显具有破坏性的操作。

源码中的审批机制通过 ExecApprovalRequirement 枚举来表达,主要有三种状态:

  • Skip:不需要审批,直接执行;
  • Forbidden:禁止执行;
  • NeedsApproval:需要审批后才能执行。

面向用户的可配置策略模式通常包括:

  • untrusted:所有操作都需要审批;
  • on-request:仅在越界时请求审批;
  • on-failure:失败时再请求升级;
  • never:不请求审批(全部自动执行)。

此外,Codex 还支持 Guardian System(自动审批审查器)。Guardian 可以在没有用户介入的情况下,根据规则自动决定是否允许某个工具调用。这对于无人值守的长时间任务(如 CI/CD 集成、云端 agent)至关重要。Guardian 相当于一个自动化的”安全审查员”,在 harness 和用户之间增加了一层缓冲。

所以,一个动作可能在技术上可执行,但策略上仍要求先审批;反过来,一个动作即使用户愿意批准,如果底层沙盒根本不允许,也不能直接执行。Guardian 则在这两者之间提供了一个中间地带:不需要等用户在线,但仍然有规则约束。

Local vs Cloud

这里顺便补上本地与云端的区别,因为这和 sandbox 的实现方式直接相关。

在本地模式下:

  • 模型推理仍在云上;
  • 但命令执行和文件修改发生在你的机器上;
  • 沙盒依赖你的操作系统机制来约束本地进程。

在云端模式下:

  • agent 跑在 OpenAI 管理的隔离容器里;
  • 官方文档采用两阶段模型:setup 阶段可装依赖,agent 阶段默认离线;
  • secrets 只在 setup 阶段可用,进入 agent 阶段前会被移除。

这说明“本地 / 云端”的差异,不只是文件放在哪,而是整个执行边界和安全模型都不同

End-to-End Lifecycle: From Query to Response

把前面的概念串起来,Codex 的一次完整生命周期可以画成这样:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
User Input (CLI / IDE / API)
   |
   v
+------------------------------------------------------+
| 0. Turn Initialization                               |
| - Session receives turn/start event                  |
| - Creates TurnContext (sandbox policy, approval     |
|   policy, CWD, config snapshot, model info)        |
| - Opens event channels (stdout, stderr, item stream)|
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 1. Context Assembly (ContextManager::for_prompt)     |
| - Retrieve conversation history (Vec<ResponseItem>)  |
| - Apply normalization (strip ghosts, modality filter)|
| - Token budgeting & truncation if near limit        |
| - Check if compaction needed → trigger summarization|
| - Inject system / developer / user instructions     |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 2. Model Inference (OpenAI Responses API)            |
| - Stream SSE events to harness                      |
| - Model reads context + tool specs                  |
| - Model emits: natural language OR tool call(s)     |
| - Streaming events propagate to UI in real time     |
+------------------------------------------------------+
   |
   +---> If plain text response + task done → final answer
   |
   v
+------------------------------------------------------+
| 3. Response Parsing (Streaming)                     |
| - As SSE events arrive, parse output type:           |
|   - text output      → stream to UI                |
|   - function_call    → queue tool call             |
|   - local_shell      → queue shell execution       |
|   - mcp_tool_call    → queue MCP call             |
|   - tool_result      → (model output, not runtime) |
| - May accumulate multiple tool calls in one turn   |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 4. Tool Call Routing (ToolRouter + ToolRegistry)    |
| - For each tool call:                               |
|   - Extract tool name and namespace                |
|   - Look up ToolHandler from registry             |
|   - Dispatch to handler                           |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 5. ToolOrchestrator Pipeline                       |
| ┌────────────────────────────────────────────────┐  |
| │ 5a. Approval Gate                              │  |
| │     - Check ExecApprovalRequirement             │  |
| │       Skip / Forbidden / NeedsApproval          │  |
| │     - If NeedsApproval:                         │  |
| │       → Guardian (auto-approve/deny) OR        │  |
| │       → Ask user (pause turn, wait response)   │  |
| │     - Hook: PreToolUse fires here              │  |
| └────────────────────────────────────────────────┘  |
|   | (approved)                                     |
|   v                                                  |
| ┌────────────────────────────────────────────────┐  |
| │ 5b. Sandbox Selection                           │  |
| │     - ToolRuntime declares sandbox preference   │  |
| │     - SandboxManager.select_initial() picks     │  |
| │       platform-specific sandbox (Seatbelt /     │  |
| │       Landlock / Windows)                       │  |
| │     - Transform command to fit sandbox policy   │  |
| └────────────────────────────────────────────────┘  |
|   | (execute)                                      |
|   v                                                  |
| ┌────────────────────────────────────────────────┐  |
| │ 5c. Execution (ToolRuntime::run)                │  |
| │     - exec.rs: spawn child process              │  |
| │     - spawn.rs: PTY setup, cwd/env config       │  |
| │     - Streaming: collect stdout/stderr chunks   │  |
| │     - Long-running: yield_time_ms → return early │  |
| │     - Collect exit code, errors                 │  |
| └────────────────────────────────────────────────┘  |
|   | (sandbox denies or tool fails)                 |
|   +---> Escalation: retry with SandboxType::None   |
|   +---> Hook: PostToolUse fires here              |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 6. Result Formatting                                 |
| - ToolOutput → ResponseInputItem (for model context) |
| - Streaming stdout/stderr → UI update              |
| - Format: tool name, call_id, success/error, output |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 7. Loop Back to Model                               |
| - Append ResponseInputItem to ContextManager        |
| - Check if tool called "done" / "task complete"    |
|   - YES → end turn, stream final response          |
|   - NO  → Go to Step 2 (next model inference)      |
+------------------------------------------------------+
   |
   v
+------------------------------------------------------+
| 8. Turn Completion                                 |
| - ContextManager records final ResponseItem        |
| - Hook: AfterAgent fires                           |
| - Event: turn/complete notification                |
| - Session returns to idle, ready for next turn     |
+------------------------------------------------------+

这张图里有几个关键点值得再强调。

第一,模型并不直接操作操作系统。模型只能生成”下一步动作意图”(tool call),真正把它落成系统调用的是 harness 中的 ToolOrchestrator 和 runtime 中的 ToolRuntime::run()

第二,工具调用不是一次性的插曲,而是 agent loop 的一部分。工具结果会作为 ResponseInputItem 重新进入 ContextManager,继续驱动模型的下一轮决策。

第三,每一步都有明确的边界控制ToolOrchestrator 的管线设计确保了审批、沙盒、钩子这三个安全层在每次工具执行时都被正确穿过。

第四,上下文管理贯穿始终。从 for_prompt() 的标准化,到 compaction 的自动触发,再到 token 使用量的持续追踪,ContextManager 保证了模型始终能看到正确的、不超过窗口大小的上下文。

因此,Codex 不是”模型 + 插件”的松耦合结构,而是一个把观察、动作、验证、压缩和回复连接起来的闭环系统。

Long-running Commands and Streaming

Codex 的长命令体验之所以看起来“像有人在后台盯着终端看”,就是因为这里存在两条并行通道。

第一条通道是 原始进程输出流

命令启动后,runtime 会持续读取 stdout / stderr,并把新增内容流式推给前端或宿主程序。这部分并不一定要求模型重新推理,所以你看到的很多实时日志,其实只是进程本身在打印。

第二条通道是 工具结果驱动的新推理

典型流程通常像这样:

  1. 启动命令;
  2. 等一个 yield_time_ms
  3. 收集当前输出;
  4. 把这部分输出作为 tool result 返回;
  5. 如果进程还在运行,则保留 session_id
  6. 后续再用 write_stdin(session_id, chars="") 轮询新输出。

因此更准确的理解方式是:

1
2
3
4
5
6
启动进程
-> 收到一部分输出
-> 模型读到当前输出并解释进度
-> 再轮询进程
-> 收到更多输出
-> 模型继续解释或决定下一步

所以看起来像”边跑边汇报”,本质上是三件事叠加:

  • runtime 负责维持进程和流式输出;
  • tool result 负责把阶段性观察送回模型;
  • 模型负责把这些观察翻译成更易读的自然语言或下一步行动。

Context Management and Auto-Compaction

前面的章节讲了工具执行和沙盒,但还有一个很容易被忽略、却直接影响 agent 能不能”长跑”的问题:上下文管理

在 Codex 源码中,ContextManager(位于 core/src/context_manager/)是上下文管理的核心。它维护的是一个有序的对话历史列表(Vec<ResponseItem>),从最早到最新排列,并在每次调用模型前做标准化处理。

但问题是:对话历史会不断增长。每轮工具调用都会产生新的消息项(工具调用本身、工具结果、模型的中间推理),长任务下来,历史很容易超出模型的上下文窗口。

Codex 的解决方案是 自动上下文压缩(Auto-Compaction),实现在 core/src/compact.rs 中。当 ContextManager 检测到 token 使用量接近上限时(通过 TokenUsageInfo 追踪),会触发以下流程:

  1. 暂停当前的 agent loop;
  2. 启动一次专门的 summarization 任务,让模型把旧的对话历史压缩成一段摘要;
  3. 用摘要替换掉被压缩的历史;
  4. 重新注入初始上下文(system / developer 指令);
  5. 继续后续的 agent loop。

这保证了 Codex 可以在理论上无限执行下去,而不会因为上下文溢出而崩溃。代价是旧细节会丢失,但关键指令和最新状态会被保留。

ContextManager 的几个关键方法:

  • record_items() — 向历史中追加新的消息项;
  • for_prompt() — 准备发送给模型的历史(标准化 + 模态过滤);
  • set_token_usage_full() — 标记上下文已满,触发 compaction。

这个机制的存在说明,agent 系统的可持续性不是免费的。它需要 harness 层主动管理上下文生命周期,而模型本身并不知道自己正在被”压缩”。

Hooks: Extending the Agent Loop

Codex 的 harness 不仅是一个封闭的调度系统,它还提供了钩子(Hooks)机制,允许用户在 agent loop 的关键节点插入自定义逻辑。

钩子系统位于 codex-rs/hooks/ 中,支持三种类型的钩子:

钩子 触发时机 用途
PreToolUse 工具执行之前 可以阻止命令执行、修改参数、记录审计日志
PostToolUse 工具执行之后 可以修改返回结果、触发后续动作、记录结果
AfterAgent 一个 agent turn 完成后 可以执行收尾操作、触发通知、更新外部状态

钩子的配置写在 config.toml 中,通过 HookRuntime 分发。一个典型的用例是:

  • PreToolUse 中检查 shell 命令是否包含危险操作(如 rm -rf /),并自动阻止;
  • PostToolUse 中把每次文件修改同步到备份系统;
  • AfterAgent 中发送任务完成通知。

钩子系统使得 Codex 的行为可以被用户定制,而不需要修改 Codex 本身的源码。这是 harness 作为”编排层”的另一个体现:它不仅编排内部组件,还允许外部逻辑介入编排过程。

Multi-agent as a Tree of Threads

再往上看 multi-agent。

Codex 的 multi-agent 更像一棵线程树,而不是多个 agent 共用一份上下文的共享池。

通常会有:

  • 一个 root agent;
  • 若干 child agent;
  • 每个 child agent 都有自己的消息历史、任务输入、工具状态和执行循环。

因此默认情况下,子 agent 并不会天然共享父 agent 的完整上下文。常见的信息传递方式有三种:

  1. spawn_agent(..., fork_context = true) 显式复制父线程的对话上下文给子 agent;

  2. send_input 给目标 agent 发送结构化输入(用户指令或任务描述);

  3. wait_agent / list_agents 查看子 agent 的执行状态,等待其完成后汇总结果。

从架构上说,multi-agent 并没有改变 harness / runtime 的基本关系。它只是把“单个 agent loop”扩展成了“多个相互隔离、但可由上层协调的 agent loop”。

这意味着 multi-agent 的核心不是“共享脑子”,而是:

  • 上下文默认隔离;
  • 需要时再显式复制或显式传递;
  • 上层 harness 负责协调并汇总结果。

The Architecture in One Picture

如果把 Codex 的主要模块放到一张图里,可以得到这样一个更稳定的架构视图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
+------------------------------------------------------------------+
| User / IDE / CLI / App                                           |
| - submit task                                                    |
| - review output                                                  |
| - approve or deny sensitive actions                              |
+------------------------------------------------------------------+
                              |
                              v
+------------------------------------------------------------------+
| Harness (control plane)                                          |
| ┌──────────────────────────────────────────────────────────────┐ |
| │ Session                                                      | |
| │ ┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐ | |
| │ │ContextManager│ │ Agent Loop        │ │ ToolOrchestrator │ | |
| │ │ - history    │ │ - turn cycle      │ │ - approval gate  │ | |
| │ │ - compaction │ │ - streaming       │ │ - sandbox select │ | |
| │ │ - token mgmt │ │ - event dispatch  │ │ - retry/escalate │ | |
| │ └──────────────┘ └───────────────────┘ └──────────────────┘ | |
| │                                                              | |
| │ ┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐ | |
| │ │ ToolRouter   │ │ HookRuntime       │ │ Guardian         │ | |
| │ │ - registry   │ │ - PreToolUse      │ │ - auto-approval  │ | |
| │ │ - dispatch   │ │ - PostToolUse     │ │ - policy check   │ | |
| │ │ - MCP bridge │ │ - AfterAgent      │ │                  │ | |
| │ └──────────────┘ └───────────────────┘ └──────────────────┘ | |
| └──────────────────────────────────────────────────────────────┘ |
| - assemble system/developer/user context                         |
| - expose tool specs to model                                     |
| - route tool calls to handlers                                   |
| - enforce policy / approvals / rules                             |
| - manage sub-agents, retries, compaction, progress updates       |
+------------------------------------------------------------------+
                              |
                              v
+------------------------------------------------------------------+
| Model                                                            |
| - interpret request and tool results                             |
| - reason over context, select next action                        |
| - emit natural language or structured tool calls                 |
+------------------------------------------------------------------+
                              |
                              v
+------------------------------------------------------------------+
| Runtime (execution plane)                                        |
| ┌──────────────────────────────────────────────────────────────┐ |
| │ ToolRuntime implementations                                  | |
| │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ | |
| │ │ ShellHandler │ │ApplyPatch    │ │ McpHandler            │ | |
| │ │ - PTY spawn  │ │Handler       │ │ - MCP tool calls      │ | |
| │ │ - streaming  │ │ - diff apply │ │ - resource reads      │ | |
| │ └──────────────┘ └──────────────┘ └───────────────────────┘ | |
| │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ | |
| │ │SpawnAgent    │ │ViewImage     │ │ Other handlers...     │ | |
| │ │Handler       │ │Handler       │ │ (ListDir, JsRepl,     │ | |
| │ │ - sub-agent  │ │ - image read │ │  Plan, ToolSearch...) │ | |
| │ └──────────────┘ └──────────────┘ └───────────────────────┘ | |
| └──────────────────────────────────────────────────────────────┘ |
| - turn tool calls into executable requests                       |
| - set cwd / env / stdio / timeout                                |
| - stream stdout / stderr                                         |
| - manage long-running sessions                                   |
| - collect outputs and return structured results                  |
+------------------------------------------------------------------+
                              |
                              v
+------------------------------------------------------------------+
| Sandbox / Approval Boundary                                      |
| ┌──────────────────────────────────────────────────────────────┐ |
| │ Platform Sandboxes (codex-rs/sandboxing/)                    | |
| │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ | |
| │ │ macOS        │ │ Linux        │ │ Windows               │ | |
| │ │ Seatbelt     │ │ Landlock     │ │ Restricted Token      │ | |
| │ │ (sandbox-exec│ │ (LSM)        │ │ Sandbox               │ | |
| │ └──────────────┘ └──────────────┘ └───────────────────────┘ | |
| └──────────────────────────────────────────────────────────────┘ |
| - filesystem restrictions (read-only / workspace-write / full)   |
| - network restrictions                                           |
| - approval gates for sensitive actions                           |
| - platform-specific enforcement                                  |
+------------------------------------------------------------------+
                              |
                              v
+------------------------------------------------------------------+
| OS / Container / Cloud Environment                               |
| - real process spawn                                             |
| - actual file edits                                              |
| - actual network requests                                        |
| - actual command execution                                       |
+------------------------------------------------------------------+

这张图里有几个关键判断:

Model

模型负责推理,但不直接触碰操作系统。它的输出是”意图”(自然语言或 tool call),不是最终系统调用。

Harness

harness 是总调度台。它包含 Session(全局状态)、ContextManager(上下文与压缩)、Agent Loop(循环驱动)、ToolOrchestrator(审批与沙盒编排)、ToolRouter(工具路由)、HookRuntime(钩子分发)和 Guardian(自动审批)。它关心上下文怎么组织、工具怎么暴露、审批怎么插入、上下文何时压缩、任务何时结束。

Runtime

runtime 是执行引擎。每个 ToolRuntime 实现了与编排器之间的契约——声明自己的审批需求、沙盒偏好和失败升级策略,然后通过 exec.rsspawn.rs → OS 的路径真正执行。它关心工具请求怎样变成真实命令、如何拿到输出、如何处理长进程和超时。

Sandbox

sandbox 是技术边界。它不是”建议模型克制一点”,而是底层真正限制命令能做什么。三个平台各有独立实现(Seatbelt / Landlock / Windows Sandbox),由 SandboxManager 统一调度。

Approval

approval 是人机协作边界。它决定哪些事情必须停下来问用户,而不是自动继续。Guardian 在此提供了自动审批能力,使得在无人值守场景下仍然有安全规则约束。

OS / Container

OS 或云容器才是真正执行系统调用的地方。文件到底被谁改了、进程到底在哪里被 spawn,发生在这一层。

因此,把这些模块串起来后,一个常见误解就可以被纠正:

Codex 不是模型直接“会用电脑”,而是模型在 harness 的调度下,通过 runtime 和 sandbox 受限地使用执行环境。

A Practical Mental Model

如果要把整篇文章压缩成一个简化心智模型,我觉得最有用的是下面这组对应关系:

  • 模型:负责想下一步做什么(基于上下文推理,输出自然语言或 tool call);
  • tool spec:告诉模型有哪些动作可以选(能力边界,不是实现细节);
  • ToolRouter + ToolRegistry:根据模型发出的 tool call,找到对应的处理器;
  • ToolOrchestrator:负责"审批 → 选沙盒 → 执行 → 重试"的编排管线,是 harness 和 runtime 的交界;
  • ToolRuntime(每个工具各自实现):声明自己的审批需求和沙盒偏好,然后真正执行;
  • ContextManager:管理对话历史,在 token 接近上限时触发压缩;
  • HookRuntime:允许用户自定义脚本在 PreToolUse / PostToolUse / AfterAgent 三个节点介入;
  • Guardian:在无人值守场景下自动决定是否批准某个工具调用;
  • sandbox:负责限制它能碰什么(技术边界,平台级强制执行);
  • approval:负责决定何时必须先问用户(交互边界);
  • OS / cloud env:负责真正执行命令和文件操作。

换成一句更口语的话:

LLM 负责下工单,ToolRouter 负责找对人,ToolOrchestrator 负责把关放行,ToolRuntime 负责施工,ContextManager 负责整理记忆,HookRuntime 负责外挂钩子,Guardian 负责替用户守夜,sandbox 和 approvals 负责围栏,OS 或容器才是真正动手干活的地方。

其中最值得记住的一句话是:Codex 的能力来自整个系统,而不只是模型。 很多表面上看像"模型能力"的东西,其实属于模型外部的系统组件。

Final Notes

理解 Codex 的关键,不是记住几个术语,而是分清不同层的职责边界。

很多表面上看像”模型能力”的东西,其实属于模型外部系统:

  • 可不可以本地改文件 — 由 sandbox 策略决定,不是模型决定;
  • 长命令为什么能流式展示 — 由 runtime 的流式输出机制 + tool result 回传机制共同实现;
  • 为什么有时候会被要求审批 — 由 ToolOrchestrator 根据 ExecApprovalRequirement 决定;
  • 长任务为什么不会因为上下文溢出而崩掉 — 由 ContextManager 的 auto-compaction 保证;
  • 为什么子 agent 不自动共享全部上下文 — multi-agent 默认隔离,需要显式 fork_context
  • 为什么本地和云端的安全模型不同 — 本地依赖 OS-level sandbox,云端依赖容器隔离 + 两阶段模型;
  • 外部工具怎么接入 — 通过 MCP(Model Context Protocol)集成,由 McpHandler 桥接;
  • 用户怎么能不写源码就定制行为 — 通过 hooks 系统在关键节点注入自定义脚本。

把这些边界理清之后,Codex 就不会再像一个神秘的”会写代码的黑盒”。它更像一套分层明确的 agent architecture:

  • 上层是指令和目标(system / developer / user);
  • 中层是 harness 编排(Session, Agent Loop, ToolOrchestrator, ContextManager, Hooks)和模型决策;
  • 下层是 runtime 执行(各 ToolRuntime 实现, exec.rs, spawn.rs)与沙盒限制;
  • 最底层才是真实的操作系统或云容器环境。

也正因为如此,Codex 的很多产品体验,其实不只是模型问题,而是 harness engineering 问题。模型决定上限,harness 和 runtime 决定它能不能稳定、可控、可恢复地把事情做完。

References

[1] OpenAI Developers, Sandboxing, accessed March 28, 2026.

[2] OpenAI Developers, Agent approvals & security, accessed March 28, 2026.

[3] OpenAI Developers, Agent internet access, accessed March 28, 2026.

[4] OpenAI, Introducing the Codex app, accessed March 28, 2026.

[5] OpenAI Cookbook, GPT-5-Codex Prompting Guide, accessed March 28, 2026.

[6] OpenAI Codex GitHub repository, openai/codex, accessed March 28, 2026.

[7] OpenAI Codex source, codex-rs/core/src/exec.rs, accessed March 28, 2026.

[8] OpenAI Codex source, codex-rs/sandboxing/ (Seatbelt, Landlock, Windows sandbox implementations), accessed March 28, 2026.

[9] OpenAI Codex source, codex-rs/core/src/spawn.rs, accessed March 28, 2026.

[10] OpenAI Codex source, codex-rs/core/src/compact.rs (auto-compaction), accessed March 28, 2026.

[11] OpenAI Codex source, codex-rs/hooks/ (hooks system), accessed March 28, 2026.

[12] OpenAI Codex source, codex-rs/execpolicy, accessed March 28, 2026.

[13] OpenAI Codex source, codex-rs/core/src/tools/orchestrator.rs (ToolOrchestrator), accessed March 28, 2026.

[14] OpenAI Codex source, codex-rs/app-server/ (JSON-RPC app-server for IDE integrations), accessed March 28, 2026.