Skip to content

Codex源码阅读路线

以会话、工具、安全、服务、UI 和扩展六类工程问题组织 Codex 源码阅读顺序,给出入口、主链、验证点和停止条件。

基于rust-v0.150.0
CodexRustArchitectureSource Code

Codex源码阅读路线 ​

Codex 的 Rust workspace 有 142 个 package。按目录顺序逐个阅读,通常会在 Core、App Server、协议、 执行后端和扩展之间反复跳转,却仍然回答不了一个具体问题。源码阅读更有效的单位不是 crate,而是 一条可验证的行为链:谁拥有状态、谁发送消息、谁跨越边界、结果在哪里重新进入系统。

本文给出六条问题驱动路线:会话、工具、安全、服务、UI 和扩展。每条路线都从公开入口或权威类型 开始,穿过共同的运行时“腰部”,最后落到测试或持久化证据。读完的标准不是“文件看过了”,而是能 画出主链、解释失败分支,并知道改动后应在哪一层写测试。

本文是阅读路线,不替代任何专题实现。选择一条路线后,先读对应的相关文章,再回到源码完成文末 检查;当已经能回答该路线的 owner、消息、失败与证据四个问题时,应停止横向扩展,进入实际修改或下一 专题,而不是继续遍历 workspace。

1. 入口选择 ​

入口文件适合确认产品装配,不适合无限递归。CLI、TUI 和 App Server 的 main 很快会进入配置、 认证、线程创建、异步 transport 和平台后端;顺着每个函数跳转,阅读范围会指数扩张。

更稳定的阅读框架由四个问题组成:

问题要找的源码证据常见误区
谁拥有状态struct 字段、Arc、锁、manager map只看函数调用,不看对象生命周期
谁驱动执行task spawn、channel send/recv、trait dispatch把异步函数调用当成同一调用栈
谁跨越边界JSON-RPC、SSE、PTY、MCP、文件与网络把序列化类型当作内部对象本身
结果如何回流EventMsg、tool output、rollout、UI reducer只追请求,不追响应与失败

先为问题写出一条预期链,再用 rg 验证每个节点。下面这组搜索足以建立第一张索引:

bash
rg -n "pub enum (Op|EventMsg|ResponseItem|TurnItem|RolloutItem)" codex-rs
rg -n "struct (ThreadManager|CodexThread|Session|TurnContext|ActiveTurn)" codex-rs/core
rg -n "run_turn|run_sampling_request|build_tool_router" codex-rs/core/src

如果搜索结果超过当前问题的边界,先记录“后续需要解释”的节点,不要立即展开。阅读路线需要显式的 停止条件,否则任何基础类型都能把人带到整个 workspace。

2. 运行时主线 ​

无论请求来自 TUI、CLI 还是 App Server,创建并驱动 agent thread 的核心链路都收敛到:

text
配置与产品入口
  → ThreadManager
  → CodexThread / SessionIo
  → Session 的 submission loop
  → SessionTask
  → RegularTask::run
  → run_turn
  → run_sampling_request
  → ModelClientSession::stream

CodexThread 是外部调用方持有的双向消息 conduit,而不是执行 Turn 的大对象。它把 Op 交给 SessionIo:

源码位置:codex-rs/core/src/codex_thread.rs :: CodexThread::submit

rust
// 阅读主链先抓住 handle 到 SessionIo 的委托边界,不在入口展开全部 Op 分支。
pub async fn submit(&self, op: Op) -> CodexResult<String> {
    self.io.submit(op).await
}

SessionIo::submit() 为操作生成 submission ID,包装成 Submission 后送入 Session loop。 Session 再根据 Op 创建或控制 SessionTask;普通用户 Turn 由 RegularTask 调用 run_turn()。 这条腰线两侧的职责不同:入口侧负责协议、产品状态和用户交互;Turn 侧负责 context、模型采样、工具 执行、事件与持久化。

这里的箭头是阅读顺序,不表示一次 Turn 只执行一次工具。真实 RegularTask 会在同一 Turn context 下 反复调用 run_turn(),直到没有 pending input;一次 run_turn() 内部又会执行多轮模型采样和工具 回灌。

3. 五组同名对象 ​

开始六条路线前,需要建立一张最小类型表。否则阅读中最常见的错误是看到 Turn、Item 或 Session 就认为它们属于同一层。

类型组权威位置作用
Op / Submissioncodex-rs/protocol/src/protocol.rs客户端提交给 Core 的控制与输入消息
Event / EventMsgcodex-rs/protocol/src/protocol.rsCore 返回客户端的实时事件
ResponseItemcodex-rs/protocol/src/models.rs模型上下文和 Responses API item
TurnItemcodex-rs/protocol/src/items.rsCore 面向产品展示的结构化 item
RolloutItemcodex-rs/protocol/src/protocol.rsJSONL 持久化候选项

另外还有两组运行时对象:

  • TurnContext 是本次 Turn 的不可变配置与身份快照;
  • ActiveTurn、RunningTask 和可变 TurnState 表示当前正在运行的任务、取消令牌和等待者。

App Server 协议中的 Thread、Turn、ThreadItem 又是客户端 schema。它们由实时事件或 rollout 重放归约得到,不是 Core 将内部 Session 直接序列化的结果。读到类型转换时,要在笔记中同时写下 “来源类型”和“目标类型”,不要只记转换函数名。

下面的类图给六条阅读路线建立共同的对象骨架。它不是完整运行时类图,而是用于判断下一步该进入 Core、工具、协议还是产品 reducer。

从 MessageProcessor 进入适合研究产品请求,从 SessionTask 进入适合研究 Turn 算法,从 ThreadHistoryBuilder 进入则适合研究历史和客户端状态。先选 owner,再展开调用树。

4. 会话路线 ​

这条路线回答:新会话如何创建,一条用户消息如何变成 Turn,模型为什么会被调用多次,中断和 pending input 又由谁处理。

4.1 会话入口 ​

  1. codex-rs/core/src/thread_manager.rs
    • 从 ThreadManager 的 active thread map、创建、resume、fork 与 remove API 开始;
    • 只追到它如何得到 CodexThread,暂时不要进入每个持久化查询。
  2. codex-rs/core/src/codex_thread.rs
    • 阅读 CodexThread 字段、submit()、事件接收、shutdown 和 rollout materialization;
    • 确认对外 handle 与内部 Session 的所有权关系。
  3. codex-rs/core/src/session/session.rs 与 codex-rs/core/src/session/mod.rs
    • 前者看 Session 初始化和 services/state 装配;
    • 后者看 SessionIo、submission channel 和主循环如何解释 Op。
  4. codex-rs/core/src/tasks/mod.rs 与 codex-rs/core/src/tasks/regular.rs
    • 区分 Regular、Review、Compact、UserShell 等 TaskKind;
    • 看取消、替换 active task 和 TurnStarted/terminal event 的职责归属。
  5. codex-rs/core/src/session/turn.rs
    • 先读 run_turn() 的外层阶段,再读 run_sampling_request();
    • 最后才进入 try_run_sampling_request() 的流事件细节。
  6. codex-rs/core/src/client_common.rs 与 codex-rs/core/src/client.rs
    • 从 Prompt、ResponseStream、ModelClient、ModelClientSession::stream() 理解 transport;
    • provider-specific auth 和 HTTP/WebSocket 细节留到模型专题。

先用 submission 封装确认 ID 与 trace 在哪一层生成,再进入庞大的 Op 分派:

源码位置:codex-rs/core/src/session/mod.rs :: SessionIo::submit_with_trace, submit_with_id

rust
pub(crate) async fn submit_with_trace(
    &self,
    op: Op,
    trace: Option<W3cTraceContext>,
    parent_turn_id: Option<String>,
    root_turn_id: Option<String>,
) -> CodexResult<String> {
    let id = new_submission_id();
    let sub = Submission {
        id: id.clone(),
        op,
        trace,
        parent_turn_id,
        root_turn_id,
    };
    // ID 只有在 submission 成功入队后才返回给调用方。
    self.submit_with_id(sub).await?;
    Ok(id)
}

pub(crate) async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> {
    if sub.trace.is_none() {
        // 跨 channel 前捕获当前 span,后续 task 才能延续同一 trace。
        sub.trace = current_span_w3c_trace_context();
    }
    self.tx_sub
        .send(sub)
        .await
        .map_err(|_| CodexErr::InternalAgentDied)?;
    Ok(())
}

若还不能解释 InternalAgentDied 为什么来自 receiver drop,就不应继续深入模型重试;那属于 Session 控制面已经终止,而不是 provider 返回了一次普通错误。

4.2 会话主链 ​

4.3 会话验证 ​

读完后应该能回答:

  • ThreadId、SessionId 与 submission/Turn ID 为什么不是同一个 ID;
  • CodexThread 为什么同时持有 Session 和 SessionIo;
  • 普通 Turn、review、compact 和 user shell 如何选择不同 task;
  • pending input 为什么可能让 RegularTask 再次进入 run_turn();
  • interrupt 取消的是哪个 token,terminal event 在哪一层发出;
  • 模型 item 何时进入 history,何时转成客户端事件。

验证时优先读 codex-rs/core/tests/suite/items.rs、codex-rs/core/tests/suite/turn_state.rs、 codex-rs/core/tests/suite/pending_input.rs、codex-rs/core/tests/suite/resume.rs 和 codex-rs/core/tests/suite/stream_error_allows_next_turn.rs。能用这些测试解释正常、取消、恢复和 stream error 四条路径,才算 完成会话主线。

5. 工具路线 ​

工具系统有两张表:模型看到的 ToolSpec,以及宿主真正持有的 runtime registry。二者名称相关但不 等价;tool search、MCP exposure、Code Mode 和动态工具都会改变可见集合。

5.1 工具入口 ​

  1. codex-rs/tools/src/tool_executor.rs 与 codex-rs/tools/src/lib.rs
    • 先看共享 ToolExecutor、ToolSpec、ToolName 和 output contract;
    • 明确这是跨宿主的基础抽象,不是 Core 的完整路由器。
  2. codex-rs/core/src/tools/registry.rs
    • 阅读 CoreToolRuntime 和 ToolRegistry;
    • 注意 hook payload、并行能力、取消等待和 telemetry 都挂在 runtime 上。
  3. codex-rs/core/src/tools/spec_plan.rs
    • 从 build_tool_router() 看 Core tools、MCP、extension、dynamic 与 hosted specs 如何汇合;
    • 继续读 exposure policy 和 build_model_visible_specs()。
  4. codex-rs/core/src/tools/router.rs 与 codex-rs/core/src/tools/parallel.rs
    • build_tool_call() 把 ResponseItem 解析成统一 ToolCall;
    • router 查 runtime,parallel runtime 决定串并行和 terminal outcome。
  5. codex-rs/core/src/tools/handlers/
    • 选择一个具体工具纵向读完 spec、handler 和 runtime;
    • 推荐先读 codex-rs/core/src/tools/handlers/unified_exec.rs 与 codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs,再读 codex-rs/core/src/tools/handlers/apply_patch.rs,最后读 MCP 或 multi-agent。
  6. codex-rs/core/src/tools/orchestrator.rs
    • 对 shell 类工具阅读审批、首次执行、sandbox denied 与条件重试;
    • 不要把该 orchestrator 错套到所有宿主扩展路径。

build_tool_router() 的装配顺序揭示了工具来源:

源码位置:codex-rs/core/src/tools/spec_plan.rs :: build_tool_router

rust
// 工具可见性按 core、MCP、extension、dynamic 四类来源依次汇入同一 registry。
let mut registry = ToolRegistry::default();
add_core_tool_sources(&context, &mut registry);
let registered_mcp_tools = session.services.mcp_handler_cache.append_mcp_tools(
    mcp,
    &turn_context.config,
    apps_enabled,
    &mcp.config().mcp_server_catalog,
    search_tool_enabled(turn_context),
    &mut registry,
);
apply_mcp_tool_exposure_policy(turn_context, mcp, &registered_mcp_tools, &mut registry);
let standalone_web_search_tool = append_extension_tool_executors(
    turn_context,
    extension_tool_executors(session, step_store),
    &mut registry,
);
append_dynamic_tool_runtimes(&turn_context.dynamic_tools, &mut registry);

模型只收到 router.model_visible_specs();registry 中可以存在 hidden 或 deferred runtime。因此排查 “工具为什么没有出现在 prompt”时,应先比较 registry、exposure 和 visible specs,而不是直接进入 handler。

5.2 工具验证 ​

读完后应能从一个 ResponseItem::FunctionCall 追到:解析 call ID 和 arguments、查找 runtime、运行 PreToolUse Hook、审批、执行、PostToolUse Hook、转换 output,并把配对结果写回模型 history。

测试入口按层次选择:codex-rs/core/src/tools/spec_plan_tests.rs 验证可见集合, codex-rs/core/src/tools/router_tests.rs 验证解析与命名空间, codex-rs/core/tests/suite/tool_parallelism.rs 验证并发,具体 handler tests 验证参数和结果, Core suite 的 codex-rs/core/tests/suite/tools.rs、codex-rs/core/tests/suite/unified_exec.rs 和 codex-rs/core/tests/suite/apply_patch_cli.rs 验证完整 Turn。

6. 安全路线 ​

安全路线必须从 policy type 向执行后端读,不能从某个平台的 sandbox 文件反推全局安全语义。

6.1 安全入口 ​

  1. codex-rs/protocol/src/models.rs 与 codex-rs/protocol/src/permissions.rs
    • PermissionProfile、filesystem/network policy、entries 和 deny-read 是规范输入;
    • 先掌握 Managed、Disabled、External,再看 legacy SandboxPolicy 投影。
  2. codex-rs/protocol/src/protocol.rs :: AskForApproval
    • 审批策略与 sandbox policy 是独立轴;
    • 记录 never、on-request 等模式如何影响 prompt,而非是否存在 OS 隔离。
  3. codex-rs/core/src/exec_policy.rs 与 codex-rs/core/src/tools/approvals.rs
    • 跟踪 allow、prompt、forbidden 以及 Hook/Guardian/用户的解析顺序。
  4. codex-rs/core/src/tools/sandboxing.rs 与 codex-rs/sandboxing/src/manager.rs
    • 分开阅读 should_sandbox()、select_initial() 与 transform();
    • 记录 sandbox_requested 和具体 SandboxType,不要合并成布尔值。
  5. 平台后端
    • macOS:codex-rs/sandboxing/src/seatbelt.rs;
    • Linux:codex-rs/linux-sandbox/src/linux_run_main.rs、codex-rs/linux-sandbox/src/bwrap.rs、codex-rs/linux-sandbox/src/landlock.rs;
    • Windows:codex-rs/sandboxing/src/windows.rs 与 codex-rs/windows-sandbox-rs/。
  6. codex-rs/network-proxy/ 与凭据/环境
    • 网络强制要同时检查 sandbox 是否封住直连和 proxy 如何决策;
    • secret propagation 还要读 auth storage 与 shell environment policy。

安全路线的一个最小分叉点如下:

源码位置:codex-rs/core/src/tools/sandboxing.rs :: unsandboxed_execution_allowed, sandbox_permissions_preserving_denied_reads

rust
pub(crate) fn unsandboxed_execution_allowed(
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> bool {
    // deny-read 依赖沙箱强制,存在时不能允许无沙箱升级。
    !file_system_sandbox_policy.has_denied_read_restrictions()
}

pub(crate) fn sandbox_permissions_preserving_denied_reads(
    sandbox_permissions: SandboxPermissions,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> SandboxPermissions {
    if sandbox_permissions.requires_escalated_permissions()
        && !unsandboxed_execution_allowed(file_system_sandbox_policy)
    {
        // 批准 escalated 也不会覆盖拒读不变量。
        SandboxPermissions::UseDefault
    } else {
        sandbox_permissions
    }
}

这段代码适合作为停止条件:能解释为什么 approval 与 enforcement 分离,再进入各平台 transform;否则 直接读 Seatbelt 或 Windows token 只会得到局部实现。

6.2 安全验证 ​

给定一条 shell 调用,应能分别写出:权限配置、exec policy 结果、是否请求批准、首次 SandboxAttempt、OS backend、network path、失败是否允许重试。若答案只有“在沙箱里”或“用户批准 了”,说明还没有读到强制边界。

优先使用 codex-rs/core/tests/suite/approvals.rs、codex-rs/core/tests/suite/exec_policy.rs、 codex-rs/core/tests/suite/network_approval.rs、codex-rs/core/tests/suite/windows_sandbox.rs, 再配合 codex-rs/linux-sandbox/tests 和 sandboxing unit tests。测试跳过条件也属于平台 证据,不能把某平台未运行的 case 当成已验证。

7. 服务路线 ​

App Server 不是 Core 的 JSON wrapper。它拥有 connection session、初始化 gate、请求并发/串行策略、 产品级 processor、live thread cache,以及从 Core 事件到 v2 notification 的投影逻辑。

7.1 服务入口 ​

  1. codex-rs/app-server-protocol/src/rpc.rs 与 codex-rs/app-server-protocol/src/protocol/common.rs
    • 从 ClientRequest、ServerRequest、notification envelope 和 request ID 开始;
    • 再进入 codex-rs/app-server-protocol/src/protocol/v2/ 的具体 Params/Response/Notification。
  2. codex-rs/app-server/src/lib.rs
    • 阅读 transport 建立、incoming message loop、connection state 与 outgoing sender;
    • 到 MessageProcessor::process_request() 为止。
  3. codex-rs/app-server/src/message_processor.rs
    • 看 JSON-RPC 反序列化、initialize gate、typed request dispatch 和 processor 划分;
    • 不要从巨大的 match 每个分支都跳进去。
  4. codex-rs/app-server/src/request_processors/thread_processor.rs、codex-rs/app-server/src/request_processors/turn_processor.rs 与 codex-rs/app-server/src/request_processors/thread_queue_processor.rs
    • 先读 thread_start、thread_read、turn_start、interrupt;
    • 再看 ThreadQueueRequestProcessor 如何把排队输入写入 QueuedItemService,以及项目/队列通知如何回到客户端;
    • 看它们如何调用 ThreadManager/CodexThread 并注册 event listener。
  5. codex-rs/app-server-protocol/src/protocol/thread_history.rs
    • ThreadHistoryBuilder 如何从 EventMsg 或 RolloutItem 重建 Vec<Turn>;
    • 特别检查 started/completed/aborted 和 item upsert。
  6. codex-rs/app-server-protocol/src/export.rs 与 codex-rs/app-server-protocol/src/schema_fixtures.rs
    • 最后核对 Rust wire type 如何变成 TypeScript/JSON schema;
    • stable 与 experimental filter 属于协议设计的一部分。

先确认 transport 枚举没有改变业务协议,再分别追连接实现:

源码位置:codex-rs/app-server-transport/src/transport/mod.rs :: AppServerTransport

rust
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AppServerTransport {
    // 三种在线 transport 共享 typed request/notification,上层不按连接方式复制 processor。
    Stdio,
    UnixSocket { socket_path: AbsolutePathBuf },
    WebSocket { bind_address: SocketAddr },
    // Off 是显式关闭监听,不等于启动一个无法连接的 server。
    Off,
}

若某个问题只在 WebSocket 复现,先定位 transport framing/connection state;若三种 transport 都复现, 再进入 MessageProcessor 或 Core,避免跨错边界。

rust-v0.150.0 还增加了项目、队列和 Realtime 的独立路径:项目请求由 codex-rs/app-server/src/request_processors/projects.rs 访问 ThreadStore 并发出 ProjectChangedNotification;队列请求由 codex-rs/app-server/src/request_processors/thread_queue_processor.rs 维护排队输入和启动时机;Realtime 事件由 codex-rs/app-server/src/realtime_event_handling.rs 同时投影通知并持久化 RolloutItem::RealtimeItem。遇到 project、queue 或 realtime 问题时,应从这些 processor 进入,而不是强行套用普通 turn/start 路线。

7.2 服务验证 ​

选择 thread/start 或 turn/start,画出 JSON-RPC request → typed request → processor → ThreadManager/CodexThread → Core EventMsg → v2 notification → response 的双向链。还应能解释 thread/read(includeTurns) 为什么需要 store 与 history reducer,而不是只读 active Session。

测试从 codex-rs/app-server/tests/suite 选择与 RPC 同名的场景,并使用 TestAppServer 从公共协议进入。若测试 直接调用 processor 私有方法,它不能证明 wire rename、初始化 gate 和 notification ordering 正确。

8. 界面路线 ​

当前 TUI 通过 App Server client 工作,仓库还专门检查 TUI 不得直接依赖 codex-core。因此 UI 阅读 路线不应从 Core event handler 开始,而应先确认 App Server notification 如何转换为本地 AppEvent 和 ChatWidget 状态。

8.1 界面入口 ​

  1. codex-rs/tui/src/lib.rs 与 codex-rs/tui/src/tui.rs
    • 看启动参数、terminal modes、event stream 和 app-server client 创建;
    • 把终端输入、tick/draw 与服务事件视为不同事件源。
  2. codex-rs/tui/src/app.rs
    • 先读 App 的高层字段:client、active thread、chat widget、history、pending requests;
    • 不要逐个阅读所有 UI 状态字段。
  3. codex-rs/tui/src/app/event_dispatch.rs
    • AppEvent 如何驱动 thread/turn 操作、弹窗、配置和渲染状态;
    • 记录哪些事件要向 App Server 发请求,哪些只改变本地 UI。
  4. codex-rs/tui/src/app/app_server_events.rs
    • 将 server notification 路由到 active thread 或 background thread;
    • 检查 thread identity,避免把不同 thread 的 item 混入当前视图。
  5. codex-rs/tui/src/chatwidget.rs 与 codex-rs/tui/src/chatwidget/
    • 从 event handler 进入 streaming、exec flow、approval、MCP startup 和 agent 状态;
    • 再看 bottom pane、history cell 与 render module。
  6. snapshots
    • 从某个 assert_snapshot! 反向定位 event sequence 和最终 terminal buffer;
    • UI 可见行为以 snapshot 作为最后证据。

TUI 的第一个源码锚点应是 App Server target,而不是 ChatWidget 中某个视觉状态:

源码位置:codex-rs/tui/src/lib.rs :: AppServerTarget, app_server_target_for_launch

rust
pub(crate) enum AppServerTarget {
    Embedded,
    LocalDaemon { endpoint: RemoteAppServerEndpoint },
    Remote { endpoint: RemoteAppServerEndpoint },
}

impl AppServerTarget {
    pub(crate) fn uses_remote_workspace(&self) -> bool {
        // 只有显式 Remote 改变 workspace 语义;本机 daemon 仍使用本地 workspace。
        matches!(self, Self::Remote { .. })
    }

    fn auth_config_for_cloud_loader(&self, mut auth_config: AuthConfig) -> AuthConfig {
        if self.uses_remote_workspace() {
            // 远端 App Server 自行强制认证策略,TUI 不沿用本地 workspace 的限制。
            auth_config.forced_login_method = None;
            auth_config.forced_chatgpt_workspace_id = None;
            auth_config.managed_auth_policy = Default::default();
        }
        auth_config
    }

    fn thread_params_mode(&self) -> ThreadParamsMode {
        // 同一个 target 判断还决定 Thread Params 按本地还是远端路径语义编码。
        if self.uses_remote_workspace() {
            ThreadParamsMode::Remote
        } else {
            ThreadParamsMode::Embedded
        }
    }
}

fn app_server_target_for_launch(
    explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
    default_daemon_socket: Option<AbsolutePathBuf>,
    can_reuse_implicit_local_daemon: bool,
    workload_identity_selected: bool,
) -> std::io::Result<AppServerTarget> {
    if workload_identity_selected {
        if explicit_remote_endpoint.is_some() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "workload identity must be configured on the remote app-server host",
            ));
        }
        return Ok(AppServerTarget::Embedded);
    }
    Ok(match explicit_remote_endpoint {
        // 显式远端优先级最高,不被本地 daemon socket 覆盖。
        Some(endpoint) => AppServerTarget::Remote { endpoint },
        None if can_reuse_implicit_local_daemon => {
            default_daemon_socket.map_or(AppServerTarget::Embedded, |socket_path| {
                AppServerTarget::LocalDaemon {
                    endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
                }
            })
        }
        None => AppServerTarget::Embedded,
    })
}

target 先决定工作区与连接边界,workload identity 还会禁止客户端同时指定远端 endpoint;后续 AppEvent 才是在该边界内归约 UI 状态。跳过这里直接读渲染, 很容易把远端 workspace 差异误判成 ChatWidget bug。

8.2 界面验证 ​

给定一次命令审批,应能解释键盘输入如何变成 AppEvent,请求如何发给 App Server,approval notification 如何创建 popup,用户选择如何回传,以及 exec begin/end 如何更新同一个 history cell。 同时要说明 resize、focus、clipboard 和 notification 哪些由 Tui 终端层负责,哪些由 ChatWidget 负责。

阅读测试时从 snapshot 名称选择一个具体视觉结果,再回到对应 codex-rs/tui/src/chatwidget/tests.rs 或 codex-rs/tui/src/app/tests.rs。 不要只读 snapshot 文本;没有 event 驱动路径,无法判断 UI 是正确归约还是碰巧渲染成同样字符串。

9. 扩展路线 ​

“扩展”在 Codex 中至少指四种不同机制:plugin bundle、Skill prompt、MCP server 和编译进宿主的 Rust extension。它们的加载时机、运行进程和信任边界不同,必须分线阅读后再看汇合点。

9.1 扩展入口 ​

  1. Plugin
    • codex-rs/core-plugins/src/manifest.rs 定义 bundle 声明;
    • codex-rs/core-plugins/src/loader.rs 从 config layer/manifest 加载 skills、MCP、hooks 与 apps;
    • codex-rs/core-plugins/src/manager.rs 管理 marketplace、安装状态、缓存刷新和 effective plugin change。
  2. Skill
    • codex-rs/ext/skills/src/catalog.rs 定义 Host、Executor、Orchestrator 与 Custom authority;
    • codex-rs/ext/skills/src/provider.rs 规定 list/read/search 必须由同一来源 provider 处理;
    • codex-rs/ext/skills/src/loader/ 分别处理 host 与 environment 中的发现、元数据和 namespace;
    • codex-rs/ext/skills/src/selection.rs 将显式 mention 解析为选中的 Skill;
    • codex-rs/ext/skills/src/extension.rs 把读取结果变成 Turn context fragment,而不是独立脚本 runtime。
  3. MCP
    • codex-rs/codex-mcp/src/connection_manager.rs 管理 required/optional server、连接复用、tool catalog 与资源;
    • transport client 位于 codex-rs/rmcp-client/;
    • codex-rs/core/src/tools/handlers/mcp.rs 才是模型工具调用进入 MCP 的 Core 路径。
  4. Rust Extension
    • codex-rs/ext/extension-api/src/contributors.rs 定义 context、tool、MCP 和 lifecycle contributor;
    • codex-rs/ext/extension-api/src/registry.rs::ExtensionRegistryBuilder 在宿主装配期注册 contributor,build() 后成为不可变 registry;
    • 各 codex-rs/ext/* crate 提供具体实现。
  5. 汇合点
    • Session 初始化装配 plugin、Skills、MCP 与 extension services;
    • build_tool_router() 决定 MCP 和 native extension tool 的可见性;
    • run_turn() 收集 context contributor、Skill mention 和 plugin injection。

Rust Extension 的 build 边界可以先从 registry ownership 入手,而不是从某个具体 extension 倒推:

源码位置:codex-rs/ext/extension-api/src/registry.rs :: ExtensionRegistryBuilder::build

rust
pub fn build(self) -> ExtensionRegistry<C> {
    // build 消费 builder;Session 看到的是装配完成后的不可变 contributor 集合。
    self.registry
}

pub struct ExtensionRegistry<C: Sync> {
    event_sink: Arc<dyn ExtensionEventSink>,
    thread_lifecycle_contributors: Vec<Arc<dyn ThreadLifecycleContributor<C>>>,
    turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
    config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
    token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
    skill_invocation_contributors: Vec<Arc<dyn SkillInvocationContributor>>,
    context_contributors: Vec<Arc<dyn ContextContributor>>,
    mcp_server_contributors: Vec<Arc<dyn McpServerContributor<C>>>,
    turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
    tool_contributors: Vec<Arc<dyn ToolContributor>>,
    tool_lifecycle_contributors: Vec<Arc<dyn ToolLifecycleContributor>>,
    turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
    approval_review_contributors: Vec<Arc<dyn ApprovalReviewContributor>>,
}

impl<C: Sync> ExtensionRegistry<C> {
    pub fn event_sink(&self) -> Arc<dyn ExtensionEventSink> {
        Arc::clone(&self.event_sink)
    }

    pub fn thread_lifecycle_contributors(&self) -> &[Arc<dyn ThreadLifecycleContributor<C>>] {
        &self.thread_lifecycle_contributors
    }

    pub fn turn_lifecycle_contributors(&self) -> &[Arc<dyn TurnLifecycleContributor>] {
        &self.turn_lifecycle_contributors
    }
}

编译进 registry 的 contributor 与运行时发现的 Plugin、Skill、MCP server 不是同一种生命周期。只有到 Session/tool-router 汇合点后,才比较它们如何共同影响上下文和工具表。

9.2 扩展验证 ​

给定一个扩展能力,应能回答它运行在哪个进程、何时发现、如何进入模型上下文或工具列表、是否经过 普通 ToolOrchestrator、配置变化能否热刷新,以及失败是阻止 Session 还是只产生 warning。

测试分别落在 codex-rs/core-plugins、codex-rs/ext/skills、codex-rs/codex-mcp、 codex-rs/ext/*/tests 和 Core suite。不要用“plugin 测试通过”代替 MCP transport 或 Skill authority/provider 的专门证据;执行环境 Skill 还应阅读 codex-rs/ext/skills/tests/executor_file_system_authority.rs。

10. 持久化路线 ​

遇到“实时正确但恢复错误”,需要从任一路线切到持久化层:

  1. codex-rs/rollout/src/policy.rs:哪些 ResponseItem/EventMsg 会进入 rollout;
  2. codex-rs/rollout/src/recorder.rs:pending items、materialize、flush、shutdown 和失败重试;
  3. codex-rs/core/src/session/rollout_reconstruction.rs:resume 如何恢复模型 history;
  4. codex-rs/thread-store/ 与 codex-rs/state/:SQLite metadata、列表、标题、memory 与检索;
  5. ThreadHistoryBuilder:客户端 Turn 如何从持久化 item 再投影。

遇到“行为偶发但无法解释”,则切到可观测性层:submission ID、thread/turn ID、tool call ID、 response ID 与 process ID 是串联日志的不同键。先从 SessionTelemetry、tool dispatch trace 和 App Server request span 找关联字段,再看 OTel exporter 或 SQLite logs;不要全文搜索一段 UI 文案 来代替因果链。

横切阅读的停止条件是能解释一个状态在 live memory、rollout JSONL、SQLite metadata 和客户端 projection 中分别是否存在。它们不是四份等价副本,缺失某一层可能是明确的持久化 policy。

Rollout writer 的命令类型揭示了“追加”和“确认落盘”不是同一个动作:

源码位置:codex-rs/rollout/src/recorder.rs :: RolloutCmd

rust
enum RolloutCmd {
    // AddItems 可批量排队,不为每次追加创建确认通道。
    AddItems(Vec<RolloutItem>),
    // durability、flush 与关闭需要把 I/O 结果通过 oneshot 返回调用方。
    Persist { ack: oneshot::Sender<std::io::Result<()>> },
    Flush { ack: oneshot::Sender<std::io::Result<()>> },
    Shutdown { ack: oneshot::Sender<std::io::Result<()>> },
}

看到实时 Event 不代表对应 item 已 durable;排查恢复问题时应继续确认何处发送 Persist/Flush,以及 terminal path 是否等待了 ack。

11. 专题选择 ​

总览路线只建立边界和主链。开始修改代码前,应按问题进入对应专题,而不是继续扩大当前调用树。

当前问题相关文章顺序
Thread/Turn、task、取消、并发Core运行时架构总览 → Thread与Turn概念模型 → ThreadManager依赖
Session 创建、预热、输入和handlersSession依赖装配 → Session启动预热 → Session启动上下文 → Session输入队列 → Session运行时处理
Prompt、模型循环和工具回灌Turn端到端链路 → Session启动上下文
ToolSpec、路由、并行、Code ModeTurn端到端链路 → Session运行时处理
审批、文件/网络权限、平台沙箱Codex信任边界 → 跨平台能力矩阵 → Core运行时能力开关
JSON-RPC、schema、兼容性Codex 产品形态全景 → CodexThread公共API → CodexThread背压
CLI、TUI、Exec和SDK进程Codex 项目概览 → Codex 产品形态全景 → 运行时任务拓扑
shell 环境与命令恢复Session启动预热 → UserShellTask流程 → Shell快照与命令环境
plugin、Skill、MCP、Hook、extensionCodex信任边界 → Session依赖装配 → Session启动预热
multi-agent、Goal、delegation核心数据对象关系 → Thread与Turn概念模型
rollout、resume、fork、关闭ThreadManager恢复 → ThreadManager分叉 → Session关闭流程
config layer、feature和设置约束Core运行时能力开关 → Session设置约束
Cargo/Bazel、schema和测试源码仓库目录地图 → Cargo 工作空间全景 → Crate依赖边界 → 源码测试与生成物地图

12. 阅读产物 ​

完成一个问题后,笔记至少应保留以下六项:

  1. 入口:公开命令、RPC、Op 或测试场景;
  2. 所有者:持有核心状态的 struct 与生命周期;
  3. 消息:跨 task/process 的 channel、event 或 wire type;
  4. 主链:正常路径上的关键函数,控制在十个左右;
  5. 失败链:取消、拒绝、timeout、恢复或降级的明确分支;
  6. 验证材料:能失败于该行为的测试、snapshot、schema 或 rollout 记录。

一次阅读任务应经过“提出问题—收紧边界—形成证据—验证结论”的闭环。下面的状态机把返工路径也 画出来,避免把写出一段解释误当成研究完成。

Insufficient 不是失败终点,而是回到边界重新定义问题。只有源码路径、运行行为与测试能够相互 印证,阅读任务才得到可复现的结论。

如果只能复述函数做了什么,却无法指出状态所有者和失败证据,阅读仍停留在局部实现。如果主链超过 十几个节点,通常说明问题范围没有收紧。Codex 的复杂性来自多种产品入口、异步任务和边界组合;用 “问题—所有者—消息—证据”组织阅读,才能在不遍历整个仓库的前提下得到可修改、可验证的理解。

13. 测试入口 ​

完成检查不是“把整个 workspace 跑一遍”。先根据边界选择 harness:Core 行为从真实 Op/EventMsg 验证, App Server 行为从公共 JSON-RPC 验证,纯类型或算法才留在近场 unit test。

13.1 Core harness ​

TestCodexBuilder 允许测试覆盖 config、auth、model、history mode、environment 和 extension,然后连接 wiremock model server,得到真实 TestCodex。这比直接构造 SessionState 更适合验证一条完整 Turn。

源码位置:codex-rs/core/tests/common/test_codex.rs

rust
// :: TestCodexBuilder::with_config, with_model, build
impl TestCodexBuilder {
    pub fn with_config<T>(mut self, mutator: T) -> Self
    where
        T: FnOnce(&mut Config) + Send + 'static,
    {
        // 测试只覆盖与场景有关的Config,公共builder仍负责其余Session装配。
        self.config_mutators.push(Box::new(mutator));
        self
    }

    pub fn with_model(self, model: &str) -> Self {
        let new_model = model.to_string();
        self.with_config(move |config| {
            config.model = Some(new_model);
        })
    }

    pub async fn build(&mut self, server: &wiremock::MockServer) -> anyhow::Result<TestCodex> {
        let home = match self.home.clone() {
            Some(home) => home,
            None => Arc::new(TempDir::new()?),
        };
        let base_url = format!("{}/v1", server.uri());
        let test_env = TestEnv::local().await?;
        Box::pin(self.build_with_home_and_base_url(
            base_url,
            home,
            /*resume_from*/ None,
            test_env,
            /*include_local_environment*/ false,
        ))
        .await
    }
}

Core suite 的入口文件是 codex-rs/core/tests/all.rs。可以先按测试名过滤,而不是无差别运行全部 suite:

bash
cargo test -p codex-core --test all <test-name-filter>

13.2 服务测试入口 ​

TestAppServerBuilder::build() 只启动进程,build_initialized() 还完成标准 initialize/initialized handshake。测试握手失败时应选择前者;测试 thread/turn API 时通常选择后者。

源码位置:codex-rs/app-server/tests/common/test_app_server.rs

rust
// :: TestAppServerBuilder::build_initialized, build_initialized_with_timeout
impl TestAppServerBuilder {
    /// Builds a server and completes its standard initialization handshake.
    pub async fn build_initialized(self) -> anyhow::Result<TestAppServer> {
        self.build_initialized_with_timeout(DEFAULT_REQUEST_TIMEOUT)
            .await
    }

    /// Builds and initializes a server while preserving a suite-specific timeout.
    pub async fn build_initialized_with_timeout(
        self,
        timeout: Duration,
    ) -> anyhow::Result<TestAppServer> {
        // build与initialize分开,使测试可以停在协议gate前后任一侧。
        let mut server = self.build().await?;
        tokio::time::timeout(timeout, server.initialize()).await??;
        Ok(server)
    }
}

App Server suite 的入口文件是 codex-rs/app-server/tests/all.rs:

bash
cargo test -p codex-app-server --test all <test-name-filter>

13.3 完成边界 ​

路线最小完成证据失败时退回哪里
Thread/Turn一个 TestCodex case同时断言事件顺序与终态回到 owner、submission channel 和 task token
Toolspec-plan/router近场测试+一个完整Turn tool output回到 visible specs 与runtime registry分界
Securitypolicy unit test+目标平台/执行fixture回到approval与OS enforcement分界
App ServerTestAppServer 从initialize走到目标RPC,并检查connection隔离回到wire type、connection state和processor
TUIreducer/component test+必要snapshot,Core行为用App Server fixture证明回到AppEvent与ServerNotification分界
Extensionowning crate测试+Session/tool-router汇合测试回到发现、装配和运行生命周期

停止条件是结论已经被“入口、owner、消息、失败测试”四项共同支持。若只有源码正向路径,应在笔记中明确 登记测试缺口;若测试需要跨三个以上不相关专题才能成立,通常说明问题范围仍然过大。