Core运行时能力开关
Codex 的一项能力很少由单个布尔值决定。[features].unified_exec = true 不保证最终出现 unified exec: ShellTool 也要启用,model tool preference 要允许,Windows 还要有 ConPTY;Code Mode 的工具表、host 进程与 fallback 又是三个独立开关;远端压缩除了 feature,还取决于 provider capability。
因此“feature enabled”只是输入之一。最终能力通常是以下条件的交集:源码是否编译、effective feature 是否启用、管理要求是否允许、配置参数是否有效、model/provider 是否支持、当前平台和外部 资源是否可用,以及客户端是否协商了相应协议。
阅读本文前,建议先理解 Core与Core-API边界 中 policy 与 wire type 的 区别,并结合 跨平台能力矩阵 阅读编译目标和运行时后端。 本文不要求记住全部 feature 名称,而是训练一种判断方法:沿着“配置输入→effective set→组合 gate→最终 consumer”追踪能力,并用反向测试确认某个 gate 缺失时能力确实消失或降级。
读完后,应能解释为什么 Stable 不等于默认开启、为什么 managed pin 不一定让相反 mutation 返回错误、 以及为什么 Core feature 与 App Server experimental capability 必须分别验证。
1. 六层 Gate 结果
| 层次 | 代表机制 | 决定什么 | 失败时常见结果 |
|---|---|---|---|
| 编译期 | #[cfg(...)]、cfg!(...) | 类型、模块或后端是否存在 | 入口不存在或选择其他实现 |
| Feature policy | Feature、Features、ManagedFeatures | 能力是否允许进入运行时 | 不注册工具、不启动服务 |
| 配置与约束 | Config、Constrained<T>、requirements | 参数和企业策略是否有效 | normalize、拒绝或 startup warning |
| Model/provider | ModelInfo、ProviderCapabilities | 模型和 transport 能做什么 | fallback、local implementation 或隐藏工具 |
| Runtime availability | shell、ConPTY、helper、environment、auth | 当前主机是否真能执行 | 条件降级或 fail closed |
| Client protocol | initialize capabilities、experimental API | 客户端能否请求/接收某 wire surface | JSON-RPC invalid request |
这不是所有能力必须逐层通过的固定 pipeline。例如 local-only feature 不需要客户端协商,纯协议实验 字段也不取决于 OS。图的作用是提醒:看到任意一个 true,只能证明一个 gate,不能证明最终能力。
2. Stage 与 Enabled
codex-features 为每项 feature 保存 FeatureSpec { id, key, stage, default_enabled }。Stage 有五类:
| Stage | 含义 | 是否进入 /experimental 菜单 |
|---|---|---|
UnderDevelopment | 开发中,不应作为普通用户能力承诺 | 否 |
Experimental | 可供用户试用,有名称、说明和公告 | 是 |
Stable | 稳定 feature,flag 仍可用于 rollout/禁用 | 否 |
Deprecated | 仍识别但应迁移 | 否 |
Removed | 仅保留兼容 key 或 no-op | 否 |
Stage 不决定布尔值。该基线上:
UnifiedExec是 Stable,当前FEATURES表中默认开启;Windows 仍需通过平台后端与 ConPTY 条件检查;MemoryTool是 Stable,却默认关闭;MultiAgentV2是 Stable,也默认关闭;CodeModeHost是 Stable 且默认开启,但真正的CodeMode仍是 UnderDevelopment 且默认关闭;- 一些 Removed feature 默认仍为 true,因为行为已经 always-on 或 key 只为兼容保留。
因此不能用 stage == Stable 推导 enabled,也不能用 enabled 推导产品已经稳定。UI 只枚举 Stage::Experimental;UnderDevelopment 被用户显式打开时,Core 会产生 unstable-features warning, 除非配置要求抑制该提示。
源码中 Stage 只携带生命周期元数据,而 Features 只保存真正启用的 ID 集合:
源码位置:codex-rs/features/src/lib.rs :: Stage, Features
pub enum Stage {
// Stage 表达发布生命周期,不参与 enabled 集合判断。
UnderDevelopment,
Experimental {
name: &'static str,
menu_description: &'static str,
announcement: &'static str,
},
Stable,
Deprecated,
Removed,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Features {
// 真正的运行时开关只有 enabled 集合,Stage 不存放在这里。
enabled: BTreeSet<Feature>,
legacy_usages: BTreeSet<LegacyFeatureUsage>,
}
impl Features {
pub fn with_defaults() -> Self {
let mut set = BTreeSet::new();
for spec in FEATURES {
// 默认值只读 default_enabled,证明 Stable 与 enabled 并非同一维度。
if spec.default_enabled {
set.insert(spec.id);
}
}
Self {
enabled: set,
legacy_usages: BTreeSet::new(),
}
}
pub fn enabled(&self, f: Feature) -> bool {
self.enabled.contains(&f)
}
pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self {
if enabled {
self.enable(f)
} else {
self.disable(f)
}
}
}with_defaults() 读取每项 FeatureSpec.default_enabled,完全没有检查 Stage。这从实现上证明 “稳定性标签”和“本次 Session 是否启用”是正交信息。
registry 把这两个维度并排保存,平台默认值也在同一处显式计算:
源码位置:codex-rs/features/src/lib.rs :: FeatureSpec, FEATURES stable entries
pub struct FeatureSpec {
pub id: Feature,
pub key: &'static str,
pub stage: Stage,
pub default_enabled: bool,
}
pub const FEATURES: &[FeatureSpec] = &[
// Removed key 仍可被识别,但默认不重新启用旧行为。
FeatureSpec {
id: Feature::GhostCommit,
key: "undo",
stage: Stage::Removed,
default_enabled: false,
},
// Stable 只表示生命周期;ShellTool 的默认值由该字段单独给出。
FeatureSpec {
id: Feature::ShellTool,
key: "shell_tool",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ViewImage,
key: "view_image",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::SecretAuthStorage,
key: "secret_auth_storage",
stage: Stage::Stable,
// 同一个 Stable feature 可以因 target 不同而采用不同默认值。
default_enabled: cfg!(windows),
},
FeatureSpec {
id: Feature::UnifiedExec,
key: "unified_exec",
stage: Stage::Stable,
default_enabled: !cfg!(windows),
},
// ...
];因此新增 feature 时必须同时审查 key、stage 和 default;只把枚举 variant 加进 Feature 并不会让它 进入默认集合,也不会自动出现在实验菜单。
Stage 表达 feature 的发布生命周期。下面的状态图展示常见演进方向,但 runtime 的 enabled 布尔值 始终是另一条轴:Stable 可以关闭,UnderDevelopment 也可能被显式打开。
该状态图不能用来推导默认值。真正的开关仍由 FeatureSpec.default_enabled、配置覆盖和 managed requirements 共同计算。
3. Effective能力
Features::from_sources() 的顺序是确定的:
FEATURES 中的 default_enabled
→ base config 的 legacy toggle
→ base config 的 [features]
→ profile 的 legacy toggle
→ profile 的 [features]
→ CLI/调用方 FeatureOverrides
→ normalize_dependencies()
→ ManagedFeatures 应用 requirements pin 并再次 normalize/validate后应用的配置覆盖先应用的配置。legacy key 会映射到 canonical Feature 并记录 usage,以便 doctor、 warning 和迁移工具提示。例如旧 connectors 映射 Apps, experimental_use_unified_exec_tool 映射 UnifiedExec。
未知 key 会 warning;一组 Removed/no-op key 被有意忽略,避免旧配置重新激活已删除行为。
解析顺序按来源展开后,后层覆盖前层,managed requirements 在最后拥有强制权。下面的时序图展示 effective set 如何在 Session 创建前收敛。
Session 得到的是约束后的最终集合,不会在每个 Turn 重放整条解析链。运行中配置刷新若只更新特定服务, 也不能据此推断 self.features 已整体替换。
3.1 Feature归一化
当前 normalize_dependencies() 的明确规则是:启用 CodeModeOnly 时自动启用 CodeMode。反方向不 成立,其他 composition gate 也不会偷偷打开基础 feature。
例如 UnifiedExecZshFork 只允许 UnifiedExec 与 ShellZshFork 组合;它本身不会启用任何一方。这样 enterprise policy 可以分别 pin 三个开关,不会因为打开组合开关意外获得新 shell backend。
3.2 ManagedFeatures
ManagedFeatures 包装 ConstrainedWithSource<Features>,从 FeatureRequirementsToml 解析 pinned feature。构造和每次 mutation 都执行:
- 把 pinned true/false 写入 candidate;
- normalize feature dependencies;
- 验证 pin 与来源约束;
- 只有通过后才替换 effective set。
因此 config.features.enable() 返回 ConstraintResult,调用方不能丢弃结果,但“请求与 pin 相反”也不 等于必然报错:normalize_candidate() 会先把 pinned value 写回 candidate,普通相反 mutation 通常成功 并保持管理值。只有后续 dependency normalization 又把某个 pinned feature 改回冲突值时,validation 才 返回 ConstraintError。管理要求既可以强制启用,也可以强制禁用;warning/error 会保留 requirement source,便于解释最终值来自哪里。
4. Feature
Session 单独保存 features: ManagedFeatures,源码注释要求 enabled set 在 Session 生命周期内保持 不变。Session 创建时从 config.features.clone() 捕获;Session::enabled() 和一些控制路径读取这份 固定快照。
用户配置 reload 不会重新裁剪整个运行时。reload_user_config_layer() 明确说明 feature gates 和 legacy notify 等 derived fields 保持 session-static;它主要刷新 plugin、skill、Hook、MCP server 和少量配置。 refresh_runtime_config() 目前只同步 SecretAuthStorage 等特定运行值,不替换 Session 的 self.features。
这解释了为什么部分实验菜单提示“重启 Codex 后生效”:ThreadManager/Session 构造时已经选择了 provider、background worker、shell snapshot、deferred executor 和其他服务。运行中修改 TOML 不能在 不破坏不变量的情况下补建所有组件。
Turn 仍会从 Session configuration 构造新的 TurnContext,所以 model、permission、mode 等允许的 thread settings 可以逐 Turn 更新;它们不是 feature registry 的替代品。
5. 编译期 gate
#[cfg(target_os = ...)] 与 cfg!(...) 的影响不同:
#[cfg]直接从当前 target 移除模块、函数、字段或 match arm;cfg!保留两侧代码的类型检查,再在常量条件下选择运行分支。
平台 sandbox 是最清楚的例子:Seatbelt 模块只在 macOS 编译,Linux helper/WSL 检查只在 Linux 编译, Windows override 和 spawn preparation 只在 Windows 编译。get_platform_sandbox() 再从当前 target 和 Windows enable state 选择具体 SandboxType。
有些 feature 默认本身也由平台决定:
源码位置:codex-rs/features/src/lib.rs :: FEATURES[UnifiedExec]
FeatureSpec {
id: Feature::UnifiedExec,
key: "unified_exec",
stage: Stage::Stable,
// 稳定 feature 的默认值由 registry 明确给出;平台后端可在后续 gate 拒绝执行。
default_enabled: true,
}编译期存在不等于默认启用,默认启用也不等于运行环境满足。Windows 可以显式启用 UnifiedExec,但 仍要通过 ConPTY build 探测;macOS/Linux 即使默认启用,也可能被用户或 requirements 关闭。
6. Shell Gate
最终 shell tool type 由 shell_type_for_model_and_features() 归约:
ShellTool关闭:返回Disabled;UnifiedExec关闭:使用 model 建议的 shell command backend;- model 返回
Default/Local:规范化为 ShellCommand; ShellZshFork打开但UnifiedExecZshFork未打开:unified exec 仍禁用;- feature mode 为 Direct/ZshFork:还要检查
conpty_supported(); - Windows ConPTY 不可用:降为 ShellCommand;
- 真正 ZshFork session 还要求 Unix、用户 shell 为 zsh,并能解析 zsh/main-execve-wrapper 路径。
这条链说明“没有 unified exec tool”可能是 policy、model preference、平台版本或路径装配中的任一项, 不能只检查 [features]。
7. Code Mode Gate
Code Mode 相关 feature 至少包括:
| Gate | 作用 |
|---|---|
CodeMode | 是否启用 JavaScript code execution 能力 |
CodeModeHost | 是否使用 standalone host process |
CodeModeOnly | 是否把 model-visible tools 收窄到 code-mode entrypoints |
CodeModeBufferedExec | 是否改变 exec 默认 yield 行为 |
config disable_in_process_fallback | host 缺失时是否 fail closed |
ThreadManager 创建时,只要 CodeModeHost 启用或明确禁止 in-process fallback,就选择 ProcessOwnedCodeModeSessionProvider;否则使用 disabled provider。Session 再把 provider 装入 CodeModeService。
但 provider 存在不代表模型能看到 Code Mode。build_tool_router() 仍按 Turn feature、model/tool surface 和 CodeModeOnly 规则注册 exec/wait,并决定哪些普通 tools 只进入 nested surface、哪些保持 direct。将 host process 选择与 tool exposure 合并成一个 bool,会破坏 fail-closed 和渐进 rollout。
8. Model Gate
Feature 只能表达“允许尝试”,不能让 provider 获得不存在的能力。
8.1 Compaction
manual 与 inline compaction 都先检查 ProviderCapabilities.remote_compaction:
- provider 支持 V2 且
RemoteCompactionV2feature 启用:走 V2; - provider 支持 V1/V2 但 V2 gate 不满足:走 legacy remote;
- provider 标记 Unsupported:走 local compaction;
TokenBudgetfeature 又可在更前面接管 compaction,创建新的 context window。
所以关闭 V2 不等于关闭远端压缩;provider V2 可以兼容回退 V1。真正禁用所有 remote 路径需要改变 provider capability 或更高层策略。
8.2 Model request
ModelInfo 控制 parallel tool calls、search tool、shell type、reasoning summary 参数、image detail 等 能力。Core 构造 prompt/request 时把 feature policy 与 ModelInfo 组合,而不是向所有模型发送同一组 参数。
Responses WebSocket 则由 provider info 的 supports_websockets 与 session 内的 fallback disable_websockets 共同决定。连接失败可以让后续请求退回 HTTP/SSE;feature flag 不能强迫一个 provider 建立不支持的 transport。
9. Platform Gate
Windows sandbox 有 canonical [windows].sandbox 配置和 requirements constraint,同时保留旧 feature 兼容映射。解析顺序会区分“用户明确选择”与“requirements 强制结果”,最终转换为 WindowsSandboxLevel::{Disabled, RestrictedToken, Elevated}。
即使 Windows sandbox level 已启用,具体 permission profile 仍要被 backend 表达。restricted-token 无法强制 deny-read 或 split-read 时拒绝运行,而不是静默退成无沙箱。受管 network proxy 又可能要求 elevated identity backend。
Linux 的 UseLegacyLandlock 是 Deprecated feature,只在显式开启时改走 legacy path;默认仍是 bubblewrap + seccomp。WSL1、helper/bwrap 缺失和 policy shape 是后续 runtime gate。
这里体现了安全能力与普通 UI feature 的差异:缺失可视化 guidance 可以隐藏,缺失强制后端则应报错 或保持受限,不能用“feature 已开启”掩盖 enforcement 不存在。
10. Tool组合Gate
Apps 的有效性不仅由 Feature::Apps 决定。Features::apps_enabled_for_auth() 还要求 ChatGPT auth; connector discovery 在 feature 关闭时直接返回空。Plugins、ToolSuggest、Apps 三者又共同控制推荐/可 安装工具是否进入 model-visible context。
build_tool_router() 对 MCP 工具继续应用:server enable/required、tool filter、plugin provenance、 Apps enable、search/defer policy、model namespace capability 和本 step MCP binding。结果可能是:server 已经连接,但工具被过滤;工具在 registry 中,但 deferred;namespace 可见,但具体 tool 尚未加载。
因此排查“插件安装了但模型看不到工具”要依次检查 authentication、Apps/Plugins feature、plugin enabled state、MCP startup、tool filter、exposure 和 ToolSpec,而不是只查 marketplace 安装目录。
11. Session-static
| 解析时机 | 典型 gate | 改变后何时生效 |
|---|---|---|
| 编译 | target OS、Unix/Windows implementation | 重新构建 |
| Config load | defaults/profile/requirements/legacy mapping | 创建新 Config |
| ThreadManager 创建 | Code Mode provider、shared managers | 新 manager/进程 |
| Session 创建 | feature snapshot、shell snapshot、deferred executor、network proxy | 新 Session |
| Turn 创建 | model、permission、mode、dynamic tools | 下一 Turn |
| Step 捕获 | environment readiness、MCP binding、tool router、capability roots | 下一次 sampling |
| Request 发送 | provider auth、WebSocket fallback、model parameters | 下一请求 |
| App Server initialize | experimental API/client capabilities | 当前 connection |
这个时间轴决定了配置 reload 的正确预期。修改 MCP server 可以通过 refresh worker 影响下一 step;修改 model/permission 可以用 thread settings 影响下一 Turn;修改 session-static feature 通常需要创建新 Session;修改 #[cfg] 条件则必须换二进制。
12. Protocol实验能力
App Server protocol 的 #[experimental(...)] 与 Core Feature registry 是不同系统。客户端在 initialize 中声明 experimentalApi 后,MessageProcessor 才允许包含 experimental method/field 的 request;未协商时返回“requires experimentalApi capability”。
这只控制 wire surface,不自动启用 Core feature。例如客户端可以获得某实验 RPC 的调用资格,但 后端 tool/feature 仍可能被 config requirements 禁用;反过来,Core feature 已启用,旧客户端也不应 收到无法解析的实验字段。
因此新增产品能力常需要两层 gate:Core feature 控制行为,protocol experimental marker 控制暴露。 二者必须分别测试,不能共用一个布尔值替代。
13. 能力诊断顺序
| 现象 | 依次检查 | 常见结论 |
|---|---|---|
| 配置 true 但功能不见 | canonical key → effective Features → requirements pin | 被 profile/管理策略覆盖 |
| Stable feature 默认关闭 | FeatureSpec.default_enabled | stage 与默认值独立 |
| shell tool 类型不符 | ShellTool/UnifiedExec/ZshFork → ModelInfo → ConPTY/path | 发生条件 fallback |
| Code Mode host 启动但模型无工具 | provider selection → CodeMode/CodeModeOnly → tool plan | host 与 exposure 分离 |
| remote compaction 没走 V2 | provider capability → V2 feature → TokenBudget | 回退 V1/local |
| Windows 声称启用但命令被拒绝 | sandbox mode → permission shape → setup/backend | fail-closed 正常工作 |
| App/Plugin 工具缺失 | auth → Apps/Plugins → MCP/filter/exposure | 安装不等于可见 |
| 改配置后旧 thread 不变化 | gate 的解析时机 | session-static,需要新 thread |
| RPC 报 experimentalApi | initialize capability | wire gate 未协商 |
测试能力开关时,应覆盖“feature × capability × platform/resource”矩阵,而不是只写 feature true/false 两例。features crate 测解析、legacy 与 requirements;tool_config/spec_plan_tests 测最终工具表;Core integration tests 测 Turn 行为;平台 tests 测后端;App Server protocol tests 测 experimental gating。
最终判断公式可以写成:effective capability = policy 允许的能力 ∩ 当前实现真正提供的能力 ∩ 客户端 能够消费的能力。Feature flag 只属于第一项;把三项分开,才能解释 Codex 运行时为什么选择某个 组件、为何发生降级,以及什么时候必须拒绝。
14. 测试与编译实验
静态枚举只能证明开关存在。下面的测试从依赖归一化、管理约束、工具组合和协议协商四层反向验证最终 结果;最后的 target check 则只验证编译图,不冒充平台后端运行测试。
14.1 CodeModeOnly测试
code_mode_only_requires_code_mode 从默认集合开始,只启用 CodeModeOnly,调用依赖归一化后要求 CodeMode 同时启用。测试没有反向断言,源码也没有 CodeMode → CodeModeOnly 规则,因此这是单向依赖。
源码位置:codex-rs/features/src/tests.rs
// :: code_mode_only_requires_code_mode(完整测试)
#[test]
fn code_mode_only_requires_code_mode() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeModeOnly);
// dependency closure发生在配置来源合并之后,而不是enable调用内部。
features.normalize_dependencies();
assert_eq!(features.enabled(Feature::CodeModeOnly), true);
assert_eq!(features.enabled(Feature::CodeMode), true);
}这解释了一个常见误区:直接构造 Features 并调用 enable() 的中间状态可以暂时不满足依赖;只有经过 from_sources() 或 ManagedFeatures 的 normalize 后,才能把集合视为 effective features。
14.2 Managed pin
feature_requirements_normalize_runtime_feature_mutations 先由 enterprise requirement 固定 personality=true、shell_tool=false,再故意提交完全相反的 candidate。can_set() 和 set() 都成功, 最终值仍回到 pin;这直接修正了“相反 mutation 必然报错”的错误理解。
源码位置:codex-rs/core/src/config/config_tests.rs
// :: feature_requirements_normalize_runtime_feature_mutations(关键路径)
let mut requested = config.features.get().clone();
requested
.disable(Feature::Personality)
.enable(Feature::ShellTool);
// candidate先被pin归一化,所以普通相反请求可以通过validation。
assert!(config.features.can_set(&requested).is_ok());
config
.features
.set(requested)
.expect("managed feature mutations should normalize successfully");
assert!(config.features.enabled(Feature::Personality));
assert!(!config.features.enabled(Feature::ShellTool));真正可能失败的是“pin 与 dependency closure 自相矛盾”:例如 requirement 固定 CodeMode=false,candidate 又启用 CodeModeOnly;pin 先关闭 CodeMode,随后依赖归一化会重新开启它,validation 才发现 pinned value 被破坏。当前测试没有直接覆盖这一冲突组合;错误分支可以从源码控制流定位,但仍缺一条专门的回归测试。
14.3 Shell组合矩阵
unified_exec_feature_mode_follows_composition_dependencies 按顺序打开四个开关。关键反例是:只开 UnifiedExecZshFork 仍保持 Direct;只开 ShellZshFork 且关闭 composition gate,反而回到 Disabled; 四项齐全才得到 ZshFork。
源码位置:codex-rs/tools/src/tool_config_tests.rs
// :: unified_exec_feature_mode_follows_composition_dependencies(完整测试)
#[test]
fn unified_exec_feature_mode_follows_composition_dependencies() {
let mut features = shell_features();
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
);
features.enable(Feature::UnifiedExec);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Direct
);
// composition gate本身不会偷偷启用ShellZshFork。
features.enable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Direct
);
features.enable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
);
features.enable(Feature::UnifiedExecZshFork);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::ZshFork
);
// 最上层ShellTool关闭后,底层组合值不再产生可执行能力。
features.disable(Feature::ShellTool);
assert_eq!(
unified_exec_feature_mode_for_features(&features),
UnifiedExecFeatureMode::Disabled
);
}工具层的异步测试再验证最终 model-visible surface,而不只检查中间 enum。未打开 composition gate 时只 暴露 shell_command;四项齐全且 PTY 可用时,暴露 exec_command/write_stdin,同时把 legacy shell 保留为 hidden registered tool。
源码位置:codex-rs/core/src/tools/spec_plan_tests.rs
// :: shell_zsh_fork_stays_standalone_until_unified_exec_composition_is_enabled(关键断言)
standalone.assert_visible_contains(&["shell_command"]);
standalone.assert_visible_lacks(&["exec_command", "write_stdin"]);
if codex_utils_pty::conpty_supported() {
// feature组合通过后还要经过runtime PTY gate。
composed.assert_visible_contains(&["exec_command", "write_stdin"]);
composed.assert_visible_lacks(&["shell_command"]);
composed.assert_registered_contains(&["exec_command", "write_stdin", "shell_command"]);
assert_eq!(composed.exposure("shell_command"), ToolExposure::Hidden);
} else {
composed.assert_visible_contains(&["shell_command"]);
composed.assert_visible_lacks(&["exec_command", "write_stdin"]);
}14.4 Featuregate
Apps 的最小单测证明 feature 与 auth 是交集:默认或缺少 ChatGPT auth 都为 false,只有两者同时满足才 为 true。
源码位置:codex-rs/features/src/tests.rs
// :: apps_require_feature_flag_and_chatgpt_auth(完整测试)
#[test]
fn apps_require_feature_flag_and_chatgpt_auth() {
let mut features = Features::with_defaults();
assert!(!features.apps_enabled_for_auth(/*has_chatgpt_auth*/ false));
features.enable(Feature::Apps);
// Apps=true不能创造缺失的ChatGPT身份。
assert!(!features.apps_enabled_for_auth(/*has_chatgpt_auth*/ false));
assert!(features.apps_enabled_for_auth(/*has_chatgpt_auth*/ true));
}App Server 的实验方法则不读取这个 feature 集合。集成测试以 experimental_api=false 完成 initialize, 随后调用实验 RPC,必须得到 -32600 和可定位的 capability 错误。
源码位置:codex-rs/app-server/tests/suite/v2/experimental_api.rs
// :: mock_experimental_method_requires_experimental_api_capability(关键路径)
let init = mcp
.initialize_with_capabilities(
default_client_info(),
Some(InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_server_openai_form_elicitation: false,
extensions: None,
}),
)
.await?;
let JSONRPCMessage::Response(_) = init else {
anyhow::bail!("expected initialize response, got {init:?}");
};
let request_id = mcp
.send_mock_experimental_method_request(MockExperimentalMethodParams::default())
.await?;
let error = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
// wire capability在MessageProcessor边界拒绝请求,不依赖Core Feature值。
assert_experimental_capability_error(error, "mock/experimentalMethod");14.5 Target编译实验
在已安装相应 Rust target、linker 和平台依赖的环境中,可以从 codex-rs/ 执行:
# Linux target:验证cfg移除Windows/macOS专属项后,codex-cli仍能完成类型检查。
cargo check -p codex-cli --target x86_64-unknown-linux-musl
# macOS与Windows target应在对应SDK/toolchain runner上执行。
cargo check -p codex-cli --target aarch64-apple-darwin
cargo check -p codex-cli --target x86_64-pc-windows-msvccargo check 通过只说明该 target 的编译图、类型和 cfg match arm 自洽;它不会启动 ConPTY、Seatbelt、 bubblewrap、MCP server 或 App Server transport。运行时能力仍必须由对应平台 runner 的测试验证。若本机 没有目标标准库、SDK 或 linker,命令失败也不能直接判定 Codex 不支持该平台,应先区分工具链缺失与源码 编译错误。
15. 开关生效验证
先用只读搜索复原三条 gate 链:
# 1. 哪些依赖会由normalize自动补齐?
rg -n "normalize_dependencies|CodeModeOnly" codex-rs/features/src
# 2. 哪些开关只是Shell组合gate,哪些会改变最终tool exposure?
rg -n "UnifiedExecZshFork|unified_exec_feature_mode|ToolExposure::Hidden" \
codex-rs/tools/src codex-rs/core/src/tools
# 3. Core feature和experimentalApi分别在哪一层拒绝请求?
rg -n "apps_enabled_for_auth|experimental_api_enabled|requires experimentalApi" \
codex-rs/features codex-rs/app-server完成搜索后,应能回答:
- 为什么
CodeModeOnly=true能补开CodeMode,而UnifiedExecZshFork=true不会补开 ZshFork? - 为什么 enterprise pin 与 mutation 相反时,最终值可能被静默归一化而不是返回错误?
- 为什么 tool-config 单测通过后,还必须检查
spec_plan_tests的 visible/registered surface? - 为什么客户端协商
experimentalApi不能替代[features]中的 Core gate?
