Skip to content

Crate依赖边界

从 Cargo 依赖图解释 Codex crate 的真实分层、中心节点、测试闭环与正在演进的稳定接口。

基于rust-v0.150.0
CodexRustCargoArchitecture

Crate依赖边界 ​

Codex 的 142 个 workspace package 组成的是一张稠密有向图,不是一棵从 CLI 向下展开的树。 本文统一使用 A → B 表示 A 在 Cargo manifest 中依赖 B;箭头描述编译期依赖, 不等于一次请求的运行时调用方向。

全部 package 的路径、target 与职责清单见 Cargo 工作空间全景;本文只讨论它们之间的依赖关系。

当前 release 的直接内部依赖可以归纳为四个数字:

  • 896 条内部依赖记录,其中 822 条是 normal dependency,74 条是 dev dependency;
  • 按 package 与 dependency 名称去重后是 891 对有向边;
  • 没有 workspace 内部的 build dependency,也没有 optional dependency;
  • 只看 822 条 normal edge 时,142 个节点构成产品依赖图。

这张图揭示了两个同时成立的事实:codex-protocol、路径类型和 HTTP transport 已形成可复用 底座;codex-core 当前直接依赖 63 个内部 crate、被 27 个内部 crate 直接依赖,是巨型编排枢纽。 所谓“稳定边界”不是给目录贴层级标签,而是识别哪些接口能够阻止上层状态和实现细节继续向外扩散。

本文讨论编译期依赖和接口扩散,不替代运行时调用链。读者应先用图选择 owner/consumer;一旦问题进入 Thread、Turn、工具或协议行为,就转到 Core与Core-API边界 或对应专题,不从 Cargo 箭头推导异步执行顺序。

1. 从 manifest ​

cargo metadata --no-deps 的 resolve 字段为空,但 .packages[].dependencies 仍保留每个 workspace package 声明的依赖、kind 和 target 条件。因此,分析内部直接边不需要下载 1200 多个 第三方 package:

bash
cargo metadata \
  --manifest-path codex-rs/Cargo.toml \
  --no-deps \
  --format-version 1 \
  > /tmp/codex-metadata.json

jq '[
  .packages[] as $from
  | $from.dependencies[]
  | select(.path != null)
  | {from: $from.name, to: .name, kind, target}
]' /tmp/codex-metadata.json

分析时必须拆成两张图:

  1. 产品图只取 normal dependency,用于判断可交付代码的编译边界和拓扑层次;
  2. 测试图在产品图上叠加 dev dependency,用于定位 fixture、test support 和反向测试引用。

完整 resolve 图给出了相同的 843 条内部记录。也就是说,本文的直接边不是按路径名猜测的, 同时又不受外部依赖下载是否成功影响。后文所有数量都以 package name 去重;同一 package 在不同 target 或 dependency kind 中重复出现时另行说明。

manifest 能直接验证 facade 的依赖方向:core-api 依赖 core 和宿主装配所需的契约 crate,而不是 反过来让 Core 依赖 facade。

源码位置:codex-rs/core-api/Cargo.toml :: [dependencies]

toml
[dependencies]
# facade 直接依赖实现 crate,所以它只收窄导入面,不形成运行时隔离层。
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-analytics = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-extension-api = { workspace = true }
codex-home = { workspace = true }
codex-image-generation-extension = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-login = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-models-manager = { workspace = true }
codex-protocol = { workspace = true }
codex-state = { workspace = true }
codex-utils-absolute-path = { workspace = true }

这 16 条 normal edge 说明 core-api 是“可运行宿主的 composition prelude”,不是只有四个 trait 的 最小接口 crate。它位于 Core 上方,却仍把多个底层契约类型统一暴露给宿主。

codex-core 自身的 manifest 则显示编排中枢为什么处于拓扑高度 15:它同时依赖协议、模型、工具、 持久化、扩展与平台服务。

源码位置:codex-rs/core/Cargo.toml :: [dependencies](内部依赖节选)

toml
[dependencies]
anyhow = { workspace = true }
arc-swap = { workspace = true }
async-channel = { workspace = true }
# agent、API 与 App Server protocol 从不同方向进入 Core 编排。
codex-agent-graph-store = { workspace = true }
codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-code-mode = { workspace = true }
codex-connectors = { workspace = true }
codex-context-fragments = { workspace = true }
codex-config = { workspace = true }
codex-core-plugins = { workspace = true }
codex-core-skills = { workspace = true }
codex-exec-server = { workspace = true }
codex-extension-api = { workspace = true }
codex-extension-items = { workspace = true }
codex-features = { workspace = true }
codex-feedback = { workspace = true }
codex-file-system = { workspace = true }
codex-login = { workspace = true }
codex-mcp = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-models-manager = { workspace = true }
codex-shell-command = { workspace = true }
codex-execpolicy = { workspace = true }
codex-git-utils = { workspace = true }
codex-hooks = { workspace = true }
codex-http-client = { workspace = true }
codex-network-proxy = { workspace = true }
codex-otel = { workspace = true }
codex-plugin = { workspace = true }
codex-model-provider = { workspace = true }
codex-protocol = { workspace = true }
codex-rollout = { workspace = true }
codex-sandboxing = { workspace = true }
codex-state = { workspace = true }
codex-thread-store = { workspace = true }
codex-tools = { workspace = true }
# ...

外部库与内部 crate 同处 [dependencies],因此图分析必须按 metadata 的 path != null 区分 workspace edge;仅按依赖名是否以 codex- 开头会漏掉 anyhow 之外的内部非前缀 crate,也会误判别名。

2. 依赖拓扑分层 ​

normal 图无环,因此可以为每个 crate 计算严格的依赖高度:

text
height(crate) = 0                                      crate 没有内部 normal dependency
height(crate) = 1 + max(height(each direct dependency))  其他情况

结果不是四层,而是 0~22 共 23 个严格拓扑高度。每条 normal edge 都从更高高度指向 更低高度,但可以一次跨过多个高度。为了便于阅读,可以把 23 层压缩成六个分析带;带名是对 职责的解释,高度和 package 数才是机械结果。

分析带高度Package 数代表 cratenormal 出边其中跨带
基础与契约0~454absolute-path、http-client、protocol、file-system520
平台与能力5~932state、config、exec-server、login、app-server-protocol154118
服务框架10~1419model-provider、plugin、mcp、tools、extension-api141108
核心编排151core6060
组合与适配16~1824extensions、core-api、app-server、test support246224
产品入口19~225app-server-client、exec、tui、cloud-tasks、cli122116

表中 normal 出边合计 775。基础带内部仍有 52 条边,例如 codex-protocol 依赖 codex-http-client、codex-network-proxy 和若干路径类型;因此“基础”不等于“无依赖”。 只有高度 0 的 28 个 crate 完全没有 workspace 内部 normal dependency。

这张压缩图不能当成依赖白名单。入口层普遍直接引用底层类型,codex-cli 自己就声明了 44 个内部 normal dependency;分层保证的是方向无环,并没有保证所有调用都只能经过相邻层。

3. 依赖图承重节点 ​

直接依赖数反映一个 crate 自己的组合复杂度;直接使用者数反映修改公共接口时的第一圈影响。 传递闭包则估计变更可能扩散到多少 workspace package。

Crate直接依赖传递依赖直接使用者传递使用者边界含义
codex-utils-absolute-path0050101最底层跨平台路径值对象
codex-http-client112696网络 transport 底座
codex-protocol9126893Core 事件、操作和共享模型契约
codex-config10232552多入口共享的配置边界
codex-exec-server13232049执行环境实现枢纽
codex-app-server-protocol8211436App Server wire contract
codex-extension-api7521533扩展注册和工具执行接口
codex-tools1042936从 Core 抽出的 host-side 工具合约
codex-core60742529运行时编排枢纽,也是最大耦合点
codex-core-api168011面向 workspace 外部嵌入者的 facade

codex-core-api 的内部使用者只有示例 crate,不能据此判断它不重要。它的目标消费者位于 workspace 外部,而 core-api/src/lib.rs 明确将自己定义为建立在 codex-core 之上的 thread management public facade。中心性指标只能描述仓库内图,不能替代源码中的接口承诺。

下面是一条真实的编译依赖脊柱。它只是稠密图中的一条路径,并非一次 Turn 的调用链; codex-cli、codex-app-server 和 codex-core 还存在大量绕过中间节点的直接依赖。

4. codex-core 中枢 ​

codex-core 位于机械高度 15:向下直接依赖 60 个内部 crate,传递覆盖 74 个;向上被 25 个 crate 直接依赖、被 29 个 crate 传递依赖。它既组装模型、工具、执行、配置、扩展和 持久化能力,又向产品入口暴露 Thread 和 Turn 的运行对象。

仓库维护规则直接承认这个结构的代价:codex-core 已因“把新功能加进 Core 比抽取库更容易” 而变得臃肿,并要求新增概念优先寻找现有 crate,必要时建立新 crate,而不是继续扩大 Core。 因此,依赖 Core 是当前实现事实,不是鼓励所有消费者采用的稳定架构方向。

下面的类图把几个容易混淆的依赖角色放在一起:协议是底层契约,Core 是实现中枢,core-api 是宿主 facade,App Server 与 TUI 是不同层次的消费者。

core-api → core 说明 facade 没有反转实现依赖;app-server → core 则说明仓库内部产品仍可访问比 facade 更宽的能力。稳定性判断必须结合消费方和可见性,而不是只看 crate 名称。

源码已经给出三条收缩路线:

  • codex-core-api 通过显式 re-export 提供 ThreadManager 等嵌入接口,并启用 private_bounds、private_interfaces、unreachable_pub 拒绝规则;
  • codex-thread-manager-sample 被 manifest 注释限制为只依赖一个 Codex workspace crate, 新的嵌入能力必须先加入 codex-core-api;
  • codex-tools 把 host-side ToolSpec、ToolExecutor 和适配器从 Core 抽出,并明确禁止 在共享接口尚未稳定前把 Session、TurnContext 和审批状态反向带入该 crate。

这解释了一个看似矛盾的现象:core-api 自己仍有 16 个内部直接依赖,并不是一个零成本 抽象层;但它把外部消费者需要追踪的入口收敛成一个 crate。稳定性的价值在于控制暴露面, 而不是让所有实现依赖瞬间消失。

5. 边界正在迁移 ​

当前图中有多处源码注释直接记录了依赖方向的演进意图:

边界当前依赖事实源码表达的方向
TUI → App ServerTUI 正常依赖 app-server-client,不直接依赖 codex-core新行为优先走 App Server protocol
App Server client → Coreclient 仍通过 legacy_core re-export Core 配置这是迁移旧启动/配置路径的过渡入口
Core → ToolsCore 依赖 codex-tools,反向边不存在逐步把可复用 host 工具能力移出 Core
Protocol → Extension itemsextension-items 明确位于 protocol 下方Core 不拥有每种扩展显示 schema
Arg0 → Apply Patchapply-patch 保存自调用常量且不依赖 Core避免让 arg0 为常量反向依赖 Core
Windows sandbox ↛ Exec Serverrunner 复制一个同步常量避免 windows-sandbox → exec-server → sandboxing → windows-sandbox 环

codex-app-server-client::legacy_core 尤其值得注意:模块名和注释都说明它是兼容桥,不是希望 长期扩大的 API。看到它 re-export codex_core::config 时,正确结论是“迁移尚未结束”,而不是 “TUI 应继续通过这个模块增加新的 Core 耦合”。

此外,codex-exec-server-protocol 在源码中声明其最早兼容 Codex 版本为 0.145.0。 这种带最低兼容版本的 wire contract,比“被很多 crate 使用”更接近真正的稳定协议承诺; 修改它时必须同时考虑远端执行端与不同版本客户端,而不能只让当前 workspace 编译通过。

6. 测试环和平台边 ​

normal 图没有多节点强连通分量。加入 dev dependency 后出现 4 个:

图唯一边多节点强连通分量含义
normal7750产品编译边界是 DAG
dev 增量68不单独判定测试和 fixture 引用
normal + dev8384测试构建允许回引被测产品 crate

四个测试闭环的规模分别是 27、5、3、2。最直观的两个是 codex-cli ↔ codex-cloud-tasks ↔ codex-tui 和 codex-mcp-server ↔ mcp_test_support。最大的 27 节点分量串联 Core、Tools、MCP、扩展和 多个 test-support crate,说明集成测试覆盖广,并不说明发布二进制存在 Cargo 循环。

平台条件也必须保留。843 条记录中有 13 条带 target 条件:

Dependency kind无 target 条件有 target 条件典型边
normal76411Core → shell-escalation 仅 Unix;CLI/App Server → windows-sandbox 仅 Windows
dev662linux-sandbox → Core 仅 Linux 测试;network-proxy → windows-sandbox 仅 Windows 测试

不带 --filter-platform 的声明图适合比较版本架构;指定 target 的 resolve 图适合回答“某个 平台实际编译哪些边”。两者不能互相替代,否则 macOS 上生成的图会静默漏掉 Windows 与 Linux 安全路径。

7. 工具强制边界 ​

Codex 已有多层依赖卫生机制,但它们检查的问题不同:

  • Cargo 编译器拒绝 normal/build dependency cycle,这是产品图无环的硬约束;
  • cargo shear --deny-warnings 在常规和完整 Rust CI 中检查未使用依赖,少量无法静态识别的 平台或生成代码依赖通过 package.metadata.cargo-shear 精确豁免;
  • cargo-deny 根据完整依赖图检查 license、advisory、source 等供应链规则;
  • Rust 可见性和 core-api 的 deny lint 限制意外公共接口;
  • AGENTS.md、crate README 和源码注释记录 Core 收缩、兼容桥和防环方向。

这些工具中没有一个自动表达“protocol 永远不得依赖 TUI”或“新扩展不得直接依赖 Core”这类 完整架构策略。当前稳定边界一部分由 Cargo 和类型系统强制,另一部分仍依赖 code review 与 源码中的局部约定。升级时不能只看 CI 是否通过,还要检查是否新增了跨带反向意图。

复现完整 resolve 图时要注意工具链与 lockfile 的关系:如果 Cargo.toml 中的 workspace version 与 Cargo.lock 的 workspace package 条目不一致,cargo metadata --locked 会拒绝改写 lockfile; 去掉 --locked 后,Cargo 可能只更新这些版本条目,而不会改变依赖边。这个现象说明命令输出必须结合 Cargo.toml、Cargo.lock 和 Rust/Cargo 版本解释,不能把一次本地解析失败直接归因于依赖图本身。

比较不同版本时,应先复制或保存 lockfile,再运行 metadata 命令,最后比较 package、normal edge、dev edge 和 target condition。这样读者可以区分“工具链拒绝写锁文件”和“依赖声明无法解析”两类问题。

8. 版本依赖Diff ​

下一稳定 tag 到来时,依赖检查不应只比较 Cargo.toml 文本。可维护的最小流程是分别比较 package、normal edge、dev edge 和 target condition:

从当前源码可以提炼出五条审查准则:

  1. normal 图必须继续无环,dev 图中的测试闭环单独解释;
  2. 新的可复用能力优先进入职责明确的 crate,不继续扩大 codex-core;
  3. 外部 Thread 嵌入能力经 codex-core-api 暴露,不能要求消费者拼装内部 crate;
  4. TUI 新行为优先走 App Server protocol,不扩大 legacy_core 兼容面;
  5. protocol、extension item、路径类型和 wire contract 的反向依赖要作为高风险变更审查。

这五条并不是凭空设计的“理想架构”,而是由当前依赖图、维护规则、迁移注释和兼容常量共同 体现的演进方向。它们比一张静态四层图更适合判断下一次 crate 调整是在收敛边界,还是把耦合 重新推回中心。

9. Consumer定位 ​

中心性数字只能告诉我们“影响面可能很大”,consumer 源码才说明依赖被用来做什么。下面四条边覆盖 嵌入 facade、产品 UI、共享工具合约和协议校验四种不同边界。

依赖边具体consumer行为应转入的专题
thread-manager-sample → core-api构造 ThreadManager、启动 Thread、处理事件并负责关闭Core与Core-API边界
tui → app-server-client通过 embedded/daemon/remote client消费JSON-RPC模型,不直接导入CoreCodex 产品形态全景
core / extension-api / ext-* → tools共享 ToolSpec、ToolExecutor、名称、输出和Responses适配器Turn端到端链路
app-server → tools在接受dynamic tool schema前复用共享解析器,不复制Tool schema规则Session运行时处理

codex-tools 的 consumer 说明“抽出共享合约”已经产生实际效果。Core 继续拥有 runtime registry 和 Session 编排,extension-api 只重导出作者需要的通用 trait/type,App Server 只复用 schema parser。

源码位置:codex-rs/ext/extension-api/src/lib.rs :: codex_tools re-exports(节选)

rust
// extension作者通过extension-api获得共享工具合约,不需要反向依赖codex-core。
pub use codex_tools::ConversationHistory;
pub use codex_tools::ExtensionTurnItem;
pub use codex_tools::FunctionCallError;
pub use codex_tools::JsonToolOutput;
pub use codex_tools::ResponsesApiTool;
pub use codex_tools::ToolCall;
pub use codex_tools::ToolEnvironment;
pub use codex_tools::ToolExecutor;
pub use codex_tools::ToolExecutorFuture;
pub use codex_tools::ToolName;
pub use codex_tools::ToolOutput;
pub use codex_tools::ToolPayload;
pub use codex_tools::ToolSpec;
pub use codex_tools::TurnItemEmitter;
pub use codex_tools::parse_tool_input_schema;

Core 则把这些稳定工具值与自己的 Session/handler 组合起来。下面的 import 不是“业务主链”,但明确了 依赖边的消费内容:tool planning 使用共享 spec/executor,运行状态仍来自 Core 模块。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: codex_tools imports(节选)

rust
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::registry::ToolRegistry;
use crate::tools::router::ToolRouter;

// 跨crate共享的是工具值对象与执行trait,不是Session或TurnContext。
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor;
use codex_tools::ToolExposures;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use codex_tools::UnifiedExecShellMode;
use codex_tools::shell_command_backend_for_features;
use codex_tools::shell_type_for_model_and_features;

如果未来 codex-tools 开始导入 Session 或 TurnContext,这条边就会从“共享合约”退化为“把 Core 状态 搬到另一个 crate”。仅看 package 数或边方向仍不足以发现这种语义变化,必须审查实际 import 和 public signature。

10. 边界破坏实验 ​

下面两项负向实验在隔离的工作树中执行,实验修改不属于发布源码。它们用于确认 consumer 和守卫真实存在,而不是把依赖图上的箭头当成运行时结论。

10.1 删除facade重导出 ​

基线命令 cargo check -p codex-core-api -p codex-thread-manager-sample 可以通过。临时删除:

diff
// codex-rs/core-api/src/lib.rs
-pub use codex_core::ThreadManager;

再执行:

bash
cargo check -p codex-thread-manager-sample

得到预期的唯一 consumer 错误:

text
error[E0432]: unresolved import `codex_core_api::ThreadManager`
  --> thread-manager-sample/src/main.rs:51:5
51 | use codex_core_api::ThreadManager;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `ThreadManager` in the root

这说明 core-api → core 不只是图上的 manifest edge:facade 负责把底层类型名变成 sample 的编译契约, 而 sample 没有用第二个 Codex dependency 绕过它。实验只验证源码 surface;ThreadManager 行为仍由 Core 测试结果。

10.2 TUI增加Core依赖 ​

第二项实验恢复 facade 后,只向临时 TUI manifest 增加:

diff
 // codex-rs/tui/Cargo.toml :: [dependencies]
+codex-core = { workspace = true }

执行仓库正式检查:

bash
python3 .github/scripts/verify_tui_core_boundary.py

脚本返回 1,并输出:

text
codex-tui must not depend on or import codex-core directly.
Use the app-server protocol/client boundary instead.
- codex-rs/tui/Cargo.toml declares `codex-core` in `[dependencies]`

这条边界比单纯的代码约定更强:即使 Cargo 图仍然无环、代码尚未引用 Core 类型,manifest 声明本身 已经失败。其他跨带方向没有同等的机器检查,必须回到 metadata diff 和实际 import 继续判断。

两个实验的覆盖范围不同:删除 ThreadManager 重导出后,cargo check 在 sample 的 import 处以 E0432 失败,证明 facade 是该 consumer 的编译契约;向 TUI manifest 增加 codex-core 后,边界脚本 在编译前返回非零,证明该产品边界由仓库检查强制。它们都没有证明 ThreadManager 的运行行为,也没有 证明所有跨层依赖都存在同等守卫;后者仍要结合实际 import、target 条件和完整 resolve 图判断。

11. 依赖边界验证 ​

bash
# 某条边有哪些实际import,而不只是manifest声明?
rg -n 'use codex_tools|codex_tools::' codex-rs/core codex-rs/ext codex-rs/app-server

# facade和TUI边界分别由哪个consumer/检查锁定?
rg -n 'codex_core_api::ThreadManager|FORBIDDEN_PACKAGE|codex_core::' \
  codex-rs/thread-manager-sample .github/scripts codex-rs/tui

# 需要运行时行为时,不再从Cargo图推断,转入对应正文。

读者应能解释:为什么 core-api → core 不代表运行时隔离;为什么 core → tools 是当前抽取方向;为什么 TUI 即使能形成无环依赖也不得直接依赖 Core;以及 normal DAG 通过为什么仍不足以证明架构边界健康。