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:
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分析时必须拆成两张图:
- 产品图只取 normal dependency,用于判断可交付代码的编译边界和拓扑层次;
- 测试图在产品图上叠加 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]
[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](内部依赖节选)
[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 计算严格的依赖高度:
height(crate) = 0 crate 没有内部 normal dependency
height(crate) = 1 + max(height(each direct dependency)) 其他情况结果不是四层,而是 0~22 共 23 个严格拓扑高度。每条 normal edge 都从更高高度指向 更低高度,但可以一次跨过多个高度。为了便于阅读,可以把 23 层压缩成六个分析带;带名是对 职责的解释,高度和 package 数才是机械结果。
| 分析带 | 高度 | Package 数 | 代表 crate | normal 出边 | 其中跨带 |
|---|---|---|---|---|---|
| 基础与契约 | 0~4 | 54 | absolute-path、http-client、protocol、file-system | 52 | 0 |
| 平台与能力 | 5~9 | 32 | state、config、exec-server、login、app-server-protocol | 154 | 118 |
| 服务框架 | 10~14 | 19 | model-provider、plugin、mcp、tools、extension-api | 141 | 108 |
| 核心编排 | 15 | 1 | core | 60 | 60 |
| 组合与适配 | 16~18 | 24 | extensions、core-api、app-server、test support | 246 | 224 |
| 产品入口 | 19~22 | 5 | app-server-client、exec、tui、cloud-tasks、cli | 122 | 116 |
表中 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-path | 0 | 0 | 50 | 101 | 最底层跨平台路径值对象 |
codex-http-client | 1 | 1 | 26 | 96 | 网络 transport 底座 |
codex-protocol | 9 | 12 | 68 | 93 | Core 事件、操作和共享模型契约 |
codex-config | 10 | 23 | 25 | 52 | 多入口共享的配置边界 |
codex-exec-server | 13 | 23 | 20 | 49 | 执行环境实现枢纽 |
codex-app-server-protocol | 8 | 21 | 14 | 36 | App Server wire contract |
codex-extension-api | 7 | 52 | 15 | 33 | 扩展注册和工具执行接口 |
codex-tools | 10 | 42 | 9 | 36 | 从 Core 抽出的 host-side 工具合约 |
codex-core | 60 | 74 | 25 | 29 | 运行时编排枢纽,也是最大耦合点 |
codex-core-api | 16 | 80 | 1 | 1 | 面向 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-sideToolSpec、ToolExecutor和适配器从 Core 抽出,并明确禁止 在共享接口尚未稳定前把 Session、TurnContext 和审批状态反向带入该 crate。
这解释了一个看似矛盾的现象:core-api 自己仍有 16 个内部直接依赖,并不是一个零成本 抽象层;但它把外部消费者需要追踪的入口收敛成一个 crate。稳定性的价值在于控制暴露面, 而不是让所有实现依赖瞬间消失。
5. 边界正在迁移
当前图中有多处源码注释直接记录了依赖方向的演进意图:
| 边界 | 当前依赖事实 | 源码表达的方向 |
|---|---|---|
| TUI → App Server | TUI 正常依赖 app-server-client,不直接依赖 codex-core | 新行为优先走 App Server protocol |
| App Server client → Core | client 仍通过 legacy_core re-export Core 配置 | 这是迁移旧启动/配置路径的过渡入口 |
| Core → Tools | Core 依赖 codex-tools,反向边不存在 | 逐步把可复用 host 工具能力移出 Core |
| Protocol → Extension items | extension-items 明确位于 protocol 下方 | Core 不拥有每种扩展显示 schema |
| Arg0 → Apply Patch | apply-patch 保存自调用常量且不依赖 Core | 避免让 arg0 为常量反向依赖 Core |
| Windows sandbox ↛ Exec Server | runner 复制一个同步常量 | 避免 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 个:
| 图 | 唯一边 | 多节点强连通分量 | 含义 |
|---|---|---|---|
| normal | 775 | 0 | 产品编译边界是 DAG |
| dev 增量 | 68 | 不单独判定 | 测试和 fixture 引用 |
| normal + dev | 838 | 4 | 测试构建允许回引被测产品 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 条件 | 典型边 |
|---|---|---|---|
| normal | 764 | 11 | Core → shell-escalation 仅 Unix;CLI/App Server → windows-sandbox 仅 Windows |
| dev | 66 | 2 | linux-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:
从当前源码可以提炼出五条审查准则:
- normal 图必须继续无环,dev 图中的测试闭环单独解释;
- 新的可复用能力优先进入职责明确的 crate,不继续扩大
codex-core; - 外部 Thread 嵌入能力经
codex-core-api暴露,不能要求消费者拼装内部 crate; - TUI 新行为优先走 App Server protocol,不扩大
legacy_core兼容面; - 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模型,不直接导入Core | Codex 产品形态全景 |
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(节选)
// 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(节选)
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 可以通过。临时删除:
// codex-rs/core-api/src/lib.rs
-pub use codex_core::ThreadManager;再执行:
cargo check -p codex-thread-manager-sample得到预期的唯一 consumer 错误:
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 增加:
// codex-rs/tui/Cargo.toml :: [dependencies]
+codex-core = { workspace = true }执行仓库正式检查:
python3 .github/scripts/verify_tui_core_boundary.py脚本返回 1,并输出:
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. 依赖边界验证
# 某条边有哪些实际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 通过为什么仍不足以证明架构边界健康。
