Skip to content

Core运行时能力开关

解释 Codex Core 如何组合编译平台、Feature、管理约束、配置、模型能力、运行资源和客户端协商,形成最终运行时能力。

基于rust-v0.150.0
CodexRustFeature FlagsRuntime

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 policyFeature、Features、ManagedFeatures能力是否允许进入运行时不注册工具、不启动服务
配置与约束Config、Constrained<T>、requirements参数和企业策略是否有效normalize、拒绝或 startup warning
Model/providerModelInfo、ProviderCapabilities模型和 transport 能做什么fallback、local implementation 或隐藏工具
Runtime availabilityshell、ConPTY、helper、environment、auth当前主机是否真能执行条件降级或 fail closed
Client protocolinitialize capabilities、experimental API客户端能否请求/接收某 wire surfaceJSON-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

rust
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

rust
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() 的顺序是确定的:

text
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 都执行:

  1. 把 pinned true/false 写入 candidate;
  2. normalize feature dependencies;
  3. 验证 pin 与来源约束;
  4. 只有通过后才替换 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]

rust
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() 归约:

  1. ShellTool 关闭:返回 Disabled;
  2. UnifiedExec 关闭:使用 model 建议的 shell command backend;
  3. model 返回 Default/Local:规范化为 ShellCommand;
  4. ShellZshFork 打开但 UnifiedExecZshFork 未打开:unified exec 仍禁用;
  5. feature mode 为 Direct/ZshFork:还要检查 conpty_supported();
  6. Windows ConPTY 不可用:降为 ShellCommand;
  7. 真正 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_fallbackhost 缺失时是否 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 且 RemoteCompactionV2 feature 启用:走 V2;
  • provider 支持 V1/V2 但 V2 gate 不满足:走 legacy remote;
  • provider 标记 Unsupported:走 local compaction;
  • TokenBudget feature 又可在更前面接管 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 loaddefaults/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 initializeexperimental 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_enabledstage 与默认值独立
shell tool 类型不符ShellTool/UnifiedExec/ZshFork → ModelInfo → ConPTY/path发生条件 fallback
Code Mode host 启动但模型无工具provider selection → CodeMode/CodeModeOnly → tool planhost 与 exposure 分离
remote compaction 没走 V2provider capability → V2 feature → TokenBudget回退 V1/local
Windows 声称启用但命令被拒绝sandbox mode → permission shape → setup/backendfail-closed 正常工作
App/Plugin 工具缺失auth → Apps/Plugins → MCP/filter/exposure安装不等于可见
改配置后旧 thread 不变化gate 的解析时机session-static,需要新 thread
RPC 报 experimentalApiinitialize capabilitywire 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

rust
// :: 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

rust
// :: 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

rust
// :: 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

rust
// :: 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

rust
// :: 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

rust
// :: 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/ 执行:

bash
# 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-msvc

cargo check 通过只说明该 target 的编译图、类型和 cfg match arm 自洽;它不会启动 ConPTY、Seatbelt、 bubblewrap、MCP server 或 App Server transport。运行时能力仍必须由对应平台 runner 的测试验证。若本机 没有目标标准库、SDK 或 linker,命令失败也不能直接判定 Codex 不支持该平台,应先区分工具链缺失与源码 编译错误。

15. 开关生效验证 ​

先用只读搜索复原三条 gate 链:

bash
# 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

完成搜索后,应能回答:

  1. 为什么 CodeModeOnly=true 能补开 CodeMode,而 UnifiedExecZshFork=true 不会补开 ZshFork?
  2. 为什么 enterprise pin 与 mutation 相反时,最终值可能被静默归一化而不是返回错误?
  3. 为什么 tool-config 单测通过后,还必须检查 spec_plan_tests 的 visible/registered surface?
  4. 为什么客户端协商 experimentalApi 不能替代 [features] 中的 Core gate?