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 验证每个节点。下面这组搜索足以建立第一张索引:
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 的核心链路都收敛到:
配置与产品入口
→ ThreadManager
→ CodexThread / SessionIo
→ Session 的 submission loop
→ SessionTask
→ RegularTask::run
→ run_turn
→ run_sampling_request
→ ModelClientSession::streamCodexThread 是外部调用方持有的双向消息 conduit,而不是执行 Turn 的大对象。它把 Op 交给 SessionIo:
源码位置:codex-rs/core/src/codex_thread.rs :: CodexThread::submit
// 阅读主链先抓住 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 / Submission | codex-rs/protocol/src/protocol.rs | 客户端提交给 Core 的控制与输入消息 |
Event / EventMsg | codex-rs/protocol/src/protocol.rs | Core 返回客户端的实时事件 |
ResponseItem | codex-rs/protocol/src/models.rs | 模型上下文和 Responses API item |
TurnItem | codex-rs/protocol/src/items.rs | Core 面向产品展示的结构化 item |
RolloutItem | codex-rs/protocol/src/protocol.rs | JSONL 持久化候选项 |
另外还有两组运行时对象:
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 会话入口
codex-rs/core/src/thread_manager.rs- 从
ThreadManager的 active thread map、创建、resume、fork 与 remove API 开始; - 只追到它如何得到
CodexThread,暂时不要进入每个持久化查询。
- 从
codex-rs/core/src/codex_thread.rs- 阅读
CodexThread字段、submit()、事件接收、shutdown 和 rollout materialization; - 确认对外 handle 与内部
Session的所有权关系。
- 阅读
codex-rs/core/src/session/session.rs与codex-rs/core/src/session/mod.rs- 前者看 Session 初始化和 services/state 装配;
- 后者看
SessionIo、submission channel 和主循环如何解释Op。
codex-rs/core/src/tasks/mod.rs与codex-rs/core/src/tasks/regular.rs- 区分 Regular、Review、Compact、UserShell 等
TaskKind; - 看取消、替换 active task 和 TurnStarted/terminal event 的职责归属。
- 区分 Regular、Review、Compact、UserShell 等
codex-rs/core/src/session/turn.rs- 先读
run_turn()的外层阶段,再读run_sampling_request(); - 最后才进入
try_run_sampling_request()的流事件细节。
- 先读
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
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 工具入口
codex-rs/tools/src/tool_executor.rs与codex-rs/tools/src/lib.rs- 先看共享
ToolExecutor、ToolSpec、ToolName和 output contract; - 明确这是跨宿主的基础抽象,不是 Core 的完整路由器。
- 先看共享
codex-rs/core/src/tools/registry.rs- 阅读
CoreToolRuntime和ToolRegistry; - 注意 hook payload、并行能力、取消等待和 telemetry 都挂在 runtime 上。
- 阅读
codex-rs/core/src/tools/spec_plan.rs- 从
build_tool_router()看 Core tools、MCP、extension、dynamic 与 hosted specs 如何汇合; - 继续读 exposure policy 和
build_model_visible_specs()。
- 从
codex-rs/core/src/tools/router.rs与codex-rs/core/src/tools/parallel.rsbuild_tool_call()把ResponseItem解析成统一ToolCall;- router 查 runtime,parallel runtime 决定串并行和 terminal outcome。
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。
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
// 工具可见性按 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, ®istered_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 安全入口
codex-rs/protocol/src/models.rs与codex-rs/protocol/src/permissions.rsPermissionProfile、filesystem/network policy、entries 和 deny-read 是规范输入;- 先掌握
Managed、Disabled、External,再看 legacySandboxPolicy投影。
codex-rs/protocol/src/protocol.rs :: AskForApproval- 审批策略与 sandbox policy 是独立轴;
- 记录 never、on-request 等模式如何影响 prompt,而非是否存在 OS 隔离。
codex-rs/core/src/exec_policy.rs与codex-rs/core/src/tools/approvals.rs- 跟踪 allow、prompt、forbidden 以及 Hook/Guardian/用户的解析顺序。
codex-rs/core/src/tools/sandboxing.rs与codex-rs/sandboxing/src/manager.rs- 分开阅读
should_sandbox()、select_initial()与transform(); - 记录
sandbox_requested和具体SandboxType,不要合并成布尔值。
- 分开阅读
- 平台后端
- 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/。
- macOS:
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
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 服务入口
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。
- 从
codex-rs/app-server/src/lib.rs- 阅读 transport 建立、incoming message loop、connection state 与 outgoing sender;
- 到
MessageProcessor::process_request()为止。
codex-rs/app-server/src/message_processor.rs- 看 JSON-RPC 反序列化、initialize gate、typed request dispatch 和 processor 划分;
- 不要从巨大的 match 每个分支都跳进去。
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。
- 先读
codex-rs/app-server-protocol/src/protocol/thread_history.rsThreadHistoryBuilder如何从EventMsg或RolloutItem重建Vec<Turn>;- 特别检查 started/completed/aborted 和 item upsert。
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
#[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 界面入口
codex-rs/tui/src/lib.rs与codex-rs/tui/src/tui.rs- 看启动参数、terminal modes、event stream 和 app-server client 创建;
- 把终端输入、tick/draw 与服务事件视为不同事件源。
codex-rs/tui/src/app.rs- 先读
App的高层字段:client、active thread、chat widget、history、pending requests; - 不要逐个阅读所有 UI 状态字段。
- 先读
codex-rs/tui/src/app/event_dispatch.rsAppEvent如何驱动 thread/turn 操作、弹窗、配置和渲染状态;- 记录哪些事件要向 App Server 发请求,哪些只改变本地 UI。
codex-rs/tui/src/app/app_server_events.rs- 将 server notification 路由到 active thread 或 background thread;
- 检查 thread identity,避免把不同 thread 的 item 混入当前视图。
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。
- 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
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 扩展入口
- 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。
- 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。
- 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 路径。
- 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 提供具体实现。
- 汇合点
- 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
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. 持久化路线
遇到“实时正确但恢复错误”,需要从任一路线切到持久化层:
codex-rs/rollout/src/policy.rs:哪些ResponseItem/EventMsg会进入 rollout;codex-rs/rollout/src/recorder.rs:pending items、materialize、flush、shutdown 和失败重试;codex-rs/core/src/session/rollout_reconstruction.rs:resume 如何恢复模型 history;codex-rs/thread-store/与codex-rs/state/:SQLite metadata、列表、标题、memory 与检索;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
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 创建、预热、输入和handlers | Session依赖装配 → Session启动预热 → Session启动上下文 → Session输入队列 → Session运行时处理 |
| Prompt、模型循环和工具回灌 | Turn端到端链路 → Session启动上下文 |
| ToolSpec、路由、并行、Code Mode | Turn端到端链路 → Session运行时处理 |
| 审批、文件/网络权限、平台沙箱 | Codex信任边界 → 跨平台能力矩阵 → Core运行时能力开关 |
| JSON-RPC、schema、兼容性 | Codex 产品形态全景 → CodexThread公共API → CodexThread背压 |
| CLI、TUI、Exec和SDK进程 | Codex 项目概览 → Codex 产品形态全景 → 运行时任务拓扑 |
| shell 环境与命令恢复 | Session启动预热 → UserShellTask流程 → Shell快照与命令环境 |
| plugin、Skill、MCP、Hook、extension | Codex信任边界 → 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. 阅读产物
完成一个问题后,笔记至少应保留以下六项:
- 入口:公开命令、RPC、
Op或测试场景; - 所有者:持有核心状态的 struct 与生命周期;
- 消息:跨 task/process 的 channel、event 或 wire type;
- 主链:正常路径上的关键函数,控制在十个左右;
- 失败链:取消、拒绝、timeout、恢复或降级的明确分支;
- 验证材料:能失败于该行为的测试、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
// :: 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:
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
// :: 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:
cargo test -p codex-app-server --test all <test-name-filter>13.3 完成边界
| 路线 | 最小完成证据 | 失败时退回哪里 |
|---|---|---|
| Thread/Turn | 一个 TestCodex case同时断言事件顺序与终态 | 回到 owner、submission channel 和 task token |
| Tool | spec-plan/router近场测试+一个完整Turn tool output | 回到 visible specs 与runtime registry分界 |
| Security | policy unit test+目标平台/执行fixture | 回到approval与OS enforcement分界 |
| App Server | TestAppServer 从initialize走到目标RPC,并检查connection隔离 | 回到wire type、connection state和processor |
| TUI | reducer/component test+必要snapshot,Core行为用App Server fixture证明 | 回到AppEvent与ServerNotification分界 |
| Extension | owning crate测试+Session/tool-router汇合测试 | 回到发现、装配和运行生命周期 |
停止条件是结论已经被“入口、owner、消息、失败测试”四项共同支持。若只有源码正向路径,应在笔记中明确 登记测试缺口;若测试需要跨三个以上不相关专题才能成立,通常说明问题范围仍然过大。
