ARTICLE · 1090634
Codex 源码-Config 配置系统与 Feature Flags
Codex 源码解析系列
第 4 讲:Config 配置系统与 Feature Flags
基于 OpenAI Codex 源码 · 2026-09-28
💡 本讲一句话:Codex 启动时不是读一个 config.toml,而是把 package、system、cloud、user、profile、项目目录、CLI 参数等十来个来源按优先级叠成一份"有效配置";企业还能用 requirements 层强制 pin 住某些 feature flag。读完这一讲,你就知道任何一行 TOML 最终为什么生效(或没生效)。
一、一次启动,十份配置:分层模型
第 3 讲结尾留了个问题:sandbox_mode、approval_policy 这些协议字段是怎么从 TOML 变成枚举的?答案在 codex-config crate(约 20K 行)+ core 的 config 模块里。先看 loader 源码里的"官方说明书"——它把整个分层顺序写成了文档注释:
📄 codex-rs/config/src/loader/mod.rs (第 98-135 行,节选)
/// To build up the set of admin-enforced constraints, requirements layers are // 管理端约束(requirements)也分层收集,顺序与配置层一致/// collected in ascending precedence order, matching config layers... // 低优先级在前、高优先级在后——两条栈共用同一套"后叠的赢"语义/// - system `/etc/codex/requirements.toml` (Unix) or // 系统级 requirements:管理员放在 /etc 下/// `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows) // Windows 走 ProgramData 目录/// - cloud: enterprise-managed cloud config bundle requirements // 企业云配置包里的 requirements 片段/// - legacy: `/etc/codex/managed_config.toml` (Unix) reinterpreted as // 旧版 managed_config.toml 被重新解释为 requirements——向后兼容/// Configuration is built up from multiple layers in the following order: // 下面是配置层(config)的完整优先级顺序,从低到高:/// - package: optional default configuration supplied with the Codex package // package:随安装包内置的默认值(defaults.toml),垫底/// - admin: managed preferences (*) // admin:macOS 托管设备偏好(MDM),企业管控入口/// - system `/etc/codex/config.toml` (Unix) or ... // system:系统级配置,管理员可全局改默认/// - cloud enterprise-managed cloud config bundle fragments // cloud:企业云下发的配置片段/// - user `${CODEX_HOME}/config.toml` // user:用户全局配置——大多数人只写这一层/// - profile `${CODEX_HOME}/<name>.config.toml`, when selected // profile:选中的命名档案,只需写差异项/// - cwd `${PWD}/config.toml` (loaded but disabled when the directory is untrusted) // 当前目录配置:目录不可信时加载但禁用——防恶意仓库/// - tree parent directories up to root looking for `./.codex/config.toml` ... // 向上逐级找 .codex/config.toml,同样受信任度约束/// - repo `$(git rev-parse --show-toplevel)/.codex/config.toml` ... // git 仓库根目录的 .codex 配置/// - runtime e.g., --config flags, model selector in UI // runtime:CLI --config / UI 选择器,优先级最高为什么这样设计:十来个来源不是"谁后写谁赢"的简单覆盖,而是两条平行的栈:config 层管"值是什么"(model、sandbox_mode),requirements 层管"哪些值被强制"(企业策略)。两者都按低→高叠放,但 requirements 有独立的合成规则(后面第五节展开)。分层的好处是职责分离:用户改自己的 $CODEX_HOME/config.toml,管理员改 /etc/codex/,项目仓库只能碰自己目录下的配置——互不越权。
但"项目层"是安全上最危险的一层:它来自仓库内容,等于不可信输入。Codex 直接给它上了黑名单:
📄 codex-rs/config/src/loader/mod.rs (第 75-92 行)
// Project-local config comes from repository contents, so it should not get to // 注释先亮出安全动机:项目配置来自仓库内容,本质不可信// choose where a user's credentials are sent or which local commands are run. // 所以它无权决定凭证发往何处、能触发哪些本地命令const PROJECT_LOCAL_CONFIG_DENYLIST: &[&str] = &[ // 黑名单常量:项目层配置命中这些键一律不生效 "openai_base_url", // API 基址——仓库改它就能把请求(含凭证)劫持到任意服务器 "chatgpt_base_url", // ChatGPT 后端地址同理,登录态会跟着走 "apps_mcp_product_sku", // MCP 产品 SKU:影响计费与路由归属 "responses_api_metadata", // Responses API 元数据:可被用来伪造请求特征 "model_provider", // provider 选择权必须留在用户/系统手里 "model_providers", // provider 定义表(含 base_url/env)同理禁改 "notify", // 通知命令——项目文件不该能指定"每轮结束跑什么程序" "profile", // profile 选择器:防止项目层劫持配置分层本身 "profiles", // profiles 表同理,一并拉黑 "experimental_realtime_webrtc_call_base_url", // Realtime WebRTC 地址:语音链路不能被仓库改向 "experimental_realtime_ws_base_url", // Realtime WebSocket 地址同理 "otel", // OTEL 遥测端点:防止项目文件把遥测数据导走];⚠️ 安全红线:黑名单里每一项都对应一类真实攻击面——改 base_url 是凭证劫持,改 notify 是任意命令执行,改 otel 是数据外泄。注意它只约束项目层:同样的键写在 user/system/managed 层完全合法。"同一份 TOML schema,不同信任级别,不同生效范围"——这是配置系统里最容易被忽略的安全设计。
十来个来源的完整优先级(低→高):
为什么这样设计:注意 profile 层的定位——它不是"另一份完整配置",而是叠在 user 之上的差异层。源码里甚至有专门检查:如果你同时用了 --profile 和旧式 [profiles.xxx] 表,直接报错让你迁移(loader/mod.rs 第 330-351 行)。新旧两套 profile 机制不允许混用,避免"到底哪份生效"的歧义。
二、层与层之间怎么合:递归 TOML merge
十来个来源叠成一份,靠的是 merge_toml_values——一个递归函数。核心规则一句话:表对表递归合并,标量/数组整体覆盖:
📄 codex-rs/config/src/merge.rs (第 75-152 行,节选)
fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &mut Vec<String>) { // 递归合并入口:overlay 优先;path 记录当前 TOML 路径,供特判识别位置 replace_shell_environment_policy_filter_representation(base, overlay, path); // 先处理互斥表示切换(filters vs exclude/include_only),防止两种写法混叠出幽灵字段 ... // 中间还有 structured feature 的 bool↔table 归一化、网络域名键规范化等特判,此处省略 if let TomlValue::Table(overlay_table) = overlay && let TomlValue::Table(base_table) = base { // 双方都是表 → 走递归合并分支(let-chains 一次判断两个条件) normalize_key_aliases(path, base_table); // 先把 base 里的旧别名键归一成规范名,防止同义不同名导致重复条目 ... // overlay 侧同样做别名/域名/大小写规范化(network domains、shell filters 特判) for (key, value) in overlay_table { // 逐键遍历 overlay:这是"高层覆盖低层"的执行点 path.push(key.clone()); // 路径下钻一层,子层的特判靠它识别自己在哪 if let Some(existing) = base_table.get_mut(&key) { // base 里已有同键 → 递归合并而不是直接替换 merge_toml_values_at_path(existing, &value, path); // 表对表继续深入;标量对表/表对标量落到 else 分支整体覆盖 } else { // base 里没有这个键 → overlay 的新键直接补进来 base_table.insert(key, normalized_with_key_aliases(&value, path)); // 插入前再过一遍别名归一化,保持全树命名一致 } path.pop(); // 路径回退一层,继续处理下一个键 } } else { // 任一方不是表(标量/数组)→ overlay 整体覆盖 base *base = normalized_with_key_aliases(overlay, path); // "高层优先"的最终落点:低层值被整个替换,不做元素级合并 }}为什么这样设计:"表递归、标量覆盖"是 TOML 分层的标准语义——user 层写 [mcp_servers.foo] 的 command,profile 层只改它的 env,合并后 command 保留、env 被替换。但注意 else 分支:数组不合并(project_root_markers = [".git"] 在高层出现就整体替换低层的)。这是刻意的——数组元素级合并会产生"谁删了哪个元素"的歧义,整体覆盖语义清晰、可预测。
这个 merge 函数还藏着三类特判(merge.rs 第 61-73、154-232 行):🔹 structured feature:multi_agent_v2/network_proxy/sleep_tool 允许写成 bool 或 table,合并时自动归一(bool 塞进 enabled 字段);🔹 网络域名键:合并前对 host 做 normalize(去端口/大小写),防止 api.openai.com 和 API.OPENAI.COM:443 变成两条规则;🔹 shell 环境过滤器:filters 表的键统一转小写,匹配时忽略大小写。
合并的调用方是 ConfigLayerStack::effective_config()——所有层的最终叠加视图:
📄 codex-rs/config/src/state.rs (第 461-479 行)
/// Returns the merged config-layer view. // 文档注释:返回合并后的配置视图——所有层的最终叠加结果pub fn effective_config(&self) -> TomlValue { // 对外只暴露一个"已合并"的 TOML,调用方无需关心层序细节 let mut merged = TomlValue::Table(toml::map::Map::new()); // 从空表开始累积 for layer in self.layers_low_to_high() { // 按低→高优先级逐层叠加:后叠的覆盖先叠的 merge_toml_values(&mut merged, &layer.config); // 复用同一套递归合并——配置层与需求层共用一个 merge 语义,行为一致 } if let Some(requirements) = &self.model_provider_requirements { // 管理端强制的 provider 定义优先级最高 crate::model_provider_requirements::apply(&mut merged, requirements); // 直接替换对应条目,不参与普通合并——企业策略不可被用户层稀释 } merged // 返回最终 TOML:ConfigToml 反序列化的唯一输入}为什么这样设计:最后两步的 model_provider_requirements::apply 是"合并之外的强制替换"——企业要求必须用某个 provider 时,直接覆盖用户写的条目。普通 merge 是"协商"(高层赢),这一步是"命令"(无条件生效)。两种机制并存,才能同时满足"用户自由配置"和"企业强管控"。
三、Feature 注册表:200+ 个 flag 的"户口"
[features] 表里的每个键都不是随便写的——它们全部登记在 codex-features crate(约 2.5K 行)的静态注册表里。先看每个 flag 的生命周期阶段:
📄 codex-rs/features/src/lib.rs (第 46-61、897-953 行,节选)
/// High-level lifecycle stage for a feature. // 功能的生命周期阶段——每个 flag 都带"户口"#[derive(Debug, Clone, Copy, PartialEq, Eq)] // 派生常用 trait:Copy 让 Stage 按值传递零开销,Eq 支持放进集合pub enum Stage { // 五阶段生命周期开始 /// Features that are still under development, not ready for external use // UnderDevelopment:内部开发中,不对外暴露 UnderDevelopment, // 默认关闭、不进 /experimental 菜单 /// Experimental features made available to users through the `/experimental` menu // Experimental:实验特性——用户可开,但带 UI 文案 Experimental { // 三个 &'static str 字段内嵌在变体里:元数据与枚举同生命周期,无堆分配 name: &'static str, // /experimental 菜单里的显示名 menu_description: &'static str, // 菜单描述文案 announcement: &'static str, // 开启时的公告语(空串视为无公告) }, /// Stable features. The feature flag is kept for ad-hoc enabling/disabling // Stable:已稳定——flag 保留只为临时开关,默认通常开启 Stable, // 生产可用 /// Deprecated feature that should not be used anymore. // Deprecated:弃用中,等待移除 Deprecated, // 仍解析但会提示迁移 /// The feature flag is useless but kept for backward compatibility reason. // Removed:功能已删,flag 只为兼容旧配置保留 Removed, // no-op:老 config.toml 里的键还能解析、不报错——升级不断链}pub struct FeatureSpec { // 注册表条目结构:id + key + stage + default_enabled 四元组 pub id: Feature, // 枚举变体(代码里引用它) pub key: &'static str, // TOML/CLI 里的键名(配置里写它)——两个名字一一对应 pub stage: Stage, // 生命周期阶段:决定 UI 展示与默认行为 pub default_enabled: bool, // 默认开关——with_defaults() 的唯一依据}pub const FEATURES: &[FeatureSpec] = &[ // 静态注册表:编译期常量,全 crate 共享同一份事实 FeatureSpec { // TranscriptV2 条目开始 id: Feature::TranscriptV2, key: "transcript_v2", stage: Stage::UnderDevelopment, default_enabled: false }, // 开发中特性示例:默认关、不对外 ... // GhostCommit(key="undo",Removed no-op)等条目省略——老配置写 undo 仍能解析 FeatureSpec { // ShellTool 条目开始 id: Feature::ShellTool, key: "shell_tool", stage: Stage::Stable, default_enabled: true }, // shell 工具:稳定且默认开——大多数用户无感 FeatureSpec { // SecretAuthStorage 条目开始 id: Feature::SecretAuthStorage, key: "secret_auth_storage", stage: Stage::Stable, default_enabled: cfg!(windows) }, // 平台相关默认值:Windows 上默认走加密 secrets 后端,其他平台默认关——cfg! 在编译期求值 FeatureSpec { // UnifiedExec 条目开始 id: Feature::UnifiedExec, key: "unified_exec", stage: Stage::Stable, default_enabled: true }, // 统一 exec 运行时已转正:稳定+默认开(第 16 讲的主角)];为什么这样设计:注册表把"代码里的枚举变体"和"配置里的字符串键名"用 FeatureSpec 绑死——feature_for_key("shell_tool") 查表即得 Feature::ShellTool,不存在两处维护、互相漂移的问题。而 Stage 把生命周期做进类型:Removed 的 flag(如 undo、js_repl)功能已删但键名保留,老用户的 config.toml 升级后不报错——这是"配置向后兼容"和"代码向前演进"解耦的关键。数一下 Feature 枚举:约 200 个变体(lib.rs 第 93-415 行),从 ShellTool 到 ResponsesWebsocketsV2,每个都带 doc comment 说明用途。
还有一个容易被忽略的细节:Features::emit_metrics()(lib.rs 第 549-565 行)只上报偏离默认值的 feature——codex.feature.state 计数器带 feature/value 两个标签。默认开启的几百个 flag 不产生遥测噪音,只有"这个用户特意改过什么"才进 OTEL——第 44 讲遥测系统会看到这条数据。
四、从 [features] 表到启用集:装配流水线
配置里的 [features] 表(key→bool)怎么变成代码里查询的 BTreeSet<Feature>?装配入口是 Features::from_sources,四步走:
📄 codex-rs/features/src/lib.rs (第 568-681 行,节选)
/// Apply a table of key -> bool toggles (e.g. from TOML). // 把 [features] 表(key→bool)应用到当前特性集pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) { // 入参是有序 BTreeMap:遍历顺序确定,行为可复现、可测试 for (k, v) in m { // 逐键处理配置里的每个 flag ... // 前面有一长串 legacy 别名特判(web_search_request、use_legacy_landlock 等)→ record_legacy_usage 留痕 match feature_for_key(k) { // 先查规范注册表,再回退 legacy 别名表——新旧键名都能解析 Some(feat) => { // 认识这个键 → 应用开关值 if k != feat.key() { // 用户写的是旧别名(如 web_search)而非规范键 self.record_legacy_usage(k.as_str(), feat); // 记一条 legacy usage:启动时提示迁移,遥测里也能看到谁还在用旧名 } if *v { self.enable(feat) } else { self.disable(feat) } // true→插入启用集;false→移除——BTreeSet 的 insert/remove } None => { // 完全不认识的键:不报错(兼容未知扩展),但必须留痕 tracing::warn!("unknown feature key in config: {k}"); // warn 日志提示拼写错误,避免"配了个寂寞"的静默失效 } } }}pub fn from_sources( // 特性集的最终装配入口:base + profile 两个来源 + 运行时覆盖 base: FeatureConfigSource<'_>, // base = 用户 config.toml 的 [features](低优先级) profile: FeatureConfigSource<'_>, // profile = <name>.config.toml 的 [features](高优先级,叠在 base 上) overrides: FeatureOverrides, // 运行时强制项(如 web_search_request 覆盖),优先级最高) -> Self { let mut features = Features::with_defaults(); // 从注册表默认值起步:default_enabled=true 的先全部打开 for source in [base, profile] { // base→profile 顺序叠加,后者覆盖前者——与配置层同序 ... // legacy 开关(experimental_use_unified_exec_tool)先应用 if let Some(feature_entries) = source.features { features.apply_toml(feature_entries); } // 再应用该层的 [features] 表 } overrides.apply(&mut features); // 运行时覆盖最后生效——优先级最高,且同样记录 legacy usage features.normalize_dependencies(); // 收尾:补齐依赖关系(见下) features}pub fn normalize_dependencies(&mut self) { // 依赖归一化:保证特性组合自洽,不出现"半残状态" if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) { // CodeModeOnly 是"只暴露 code mode 入口"的加强模式 self.enable(Feature::CodeMode); // 开了加强却没开基础 → 自动补开基础——依赖关系由代码保证,不靠用户自觉 }}为什么这样设计:三个值得注意的取舍。🔹 未知键不报错只 warn:feature flag 是扩展点,第三方/未来版本可能引入新键,fail-loud 会卡死老配置;但 warn + 日志保证拼写错误可被发现。🔹 legacy usage 留痕:旧别名(web_search_request)继续工作,同时启动警告告诉用户"该用规范名了"——迁移窗口期由产品控制,而不是硬切。🔹 依赖归一化放在装配末尾:CodeModeOnly⇒CodeMode 这类隐含依赖只在一处维护,任何入口(TOML、CLI、运行时覆盖)产出的特性集都经过同一道收尾。
查询侧则极简:features.enabled(Feature::UnifiedExec) 就是一次 BTreeSet 成员检查——O(log n)、无锁、零分配。整个 core(224K 行)里几百处 feature gate,全部走这一个入口。
五、管理端 pinning:企业策略压过用户配置
前面四节都是"用户侧"的故事。但企业场景里,管理员要在 requirements.toml(或 MDM/云配置包)里强制某些 feature 的开/关状态——用户的 config.toml 写了也不算数。这套机制叫 ManagedFeatures:
📄 codex-rs/core/src/config/managed_features.rs (第 65-87、151-193 行,节选)
fn from_configured_with_optional_warnings( // 构造入口:用户特性集 + 管理端 requirements → 合规的 ManagedFeatures configured_features: Features, feature_requirements: Option<Sourced<FeatureRequirementsToml>>, startup_warnings: Option<&mut Vec<String>>,) -> std::io::Result<Self> { // 返回 Result:pin 校验失败是硬错误,直接拒绝启动而不是静默降级 let (pinned_features, source) = match feature_requirements { // 解出管理端 pin 表及其来源(system/cloud/MDM) Some(Sourced { value: feature_requirements, source }) => (parse_feature_requirements(feature_requirements, &source, startup_warnings), Some(source)), // 有 requirements → 解析成 Feature→bool 的 pin 映射,并记住"谁要求的" None => (BTreeMap::new(), None), // 没有管理端来源(纯用户配置)→ 空 pin 表,后续校验自动跳过 }; let normalized_features = normalize_candidate(configured_features, &pinned_features); // 第一步:归一化——把 pin 值强写进候选集 validate_pinned_features(&normalized_features, &pinned_features, source.as_ref())?; // 第二步:校验——确认最终状态真的满足所有 pin,不满足则报错 Ok(Self { value: ConstrainedWithSource::new(Constrained::allow_any(normalized_features), source), pinned_features }) // 连同 pin 表一起存下:后续 set/enable/disable 每次变更都要重新过这道闸}fn normalize_candidate(mut candidate: Features, pinned_features: &BTreeMap<Feature, bool>) -> Features { // 候选特性集 → 合规特性集 if !pinned_features.contains_key(&Feature::UnifiedExec) { // UnifiedExec 是"唯一执行后端",用户层不允许关掉 candidate.enable(Feature::UnifiedExec); // 管理端没显式 pin 它时强制开启——旧 shell 后端已移除,这是兜底保险 } for (feature, enabled) in pinned_features { // 逐条应用企业 pin:值直接覆盖用户选择 candidate.set_enabled(*feature, *enabled); // pin=true 则开、pin=false 则关——管理端意图不可协商 } candidate.normalize_dependencies(); // 补依赖(CodeModeOnly⇒CodeMode),保证组合自洽 candidate}fn validate_pinned_features_constraint( // 校验:归一化后的特性集是否真的满足所有 pin normalized_features: &Features, pinned_features: &BTreeMap<Feature, bool>, source: Option<&RequirementSource>,) -> ConstraintResult<()> { // 返回 ConstraintResult:错误类型自带"字段/候选值/允许值/来源"四元组 let Some(source) = source else { return Ok(()); }; // 没有管理端来源 → 无需校验,直接放行(纯用户配置场景) for (feature, enabled) in pinned_features { // 逐条比对 pin 值与实际状态 if normalized_features.enabled(*feature) != *enabled { // 不一致 = 有路径试图绕过企业策略 return Err(ConstraintError::InvalidValue { // 构造带来源的错误:报错信息里能直接看到"谁要求的、要求什么、实际是什么" field_name: "features", candidate: format!("{}={}", feature.key(), normalized_features.enabled(*feature)), allowed: feature_requirements_display(pinned_features), requirement_source: source.clone(), }); // 错误四元组齐全——排障时不用翻代码就知道冲突双方 } } Ok(()) // 全部 pin 满足 → 校验通过为什么这样设计:"归一化 + 校验"两步看似重复(normalize 已经强写了 pin,validate 还会不一致吗?),但这是防御性编程:normalize 处理的是"当前这条路径",而 ManagedFeatures::set()/enable()/disable() 每次运行时变更都要重新走 normalize_and_validate(第 93-125 行)——任何调用方想绕过 pin 改 feature,都会在 set 时被同一道闸拦下。错误信息里带 requirement_source(system/cloud/MDM),用户看到报错就知道"这是公司策略,不是我的配置错了"。
requirements 层对普通配置字段的覆盖走另一条路——apply_to_config(core/src/config/requirements.rs):管理值直接替换用户值,冲突时生成带来源的启动警告:
📄 codex-rs/core/src/config/requirements.rs (第 87-112 行)
fn apply_exact_requirement<T>( // 通用"精确覆盖":管理端值直接替换用户配置值 field_name: &'static str, configured_value: &mut Option<T>, requirement: Option<&Sourced<T>>, startup_warnings: &mut Vec<String>,) where T: Clone + PartialEq + std::fmt::Debug { // 泛型约束:可克隆(写入)、可比对(判冲突)、可打印(进警告文案)——三个能力缺一不可 let Some(Sourced { value, source }) = requirement else { return; }; // 管理端没设这个字段 → 用户值原样保留,直接返回 if configured_value.as_ref().is_some_and(|configured| configured != value) { // 用户显式配了且与管理值不同 → 这是"冲突"场景 tracing::warn!(?source, ?value, "configured value is overridden by an exact requirement for {field_name}"); // 日志留痕:谁(source)覆盖了什么(value),排障可追溯 startup_warnings.push(format!("Configured value for `{field_name}` is overridden by the required value {value:?} from {source}.")); // 同时推入启动警告——TUI 开机就能看到,绝不静默改配置 } *configured_value = Some(value.clone()); // 最后无条件写入管理值:覆盖是确定性的,冲突只影响"是否提示用户"为什么这样设计:对比 feature pinning(硬错误、拒绝启动)和字段覆盖(软警告、继续运行),能看出 Codex 的分级策略:能力开关(feature)冲突时 fail-loud——企业说"不许用 code mode",用户偏要开,那就别启动;配置值(base_url、log_dir)冲突时覆盖+警告——管理员改遥测端点不该让用户整个客户端起不来。两种强度对应两类风险等级,而不是"一刀切报错"。
🔑 三层防线小结:用户配置(config.toml,自由)< 管理端 requirements(强制覆盖 + pin 校验)< 运行时 overrides(最高优先级)。feature flag 在这条链上被处理三次:装配时 from_sources、构造时 ManagedFeatures::from_configured、每次变更时 set()——同一道闸,三个入口。
六、数据流图:一次启动的配置装配流水线
配置装配流水线(ConfigBuilder.build_inner)
① 入口:ConfigBuilder.build_inner
解析 codex_home/cwd;Box::pin 把大 future 挪出小线程栈,防栈溢出。
▼
② 收集层:load_config_layers_state
package→admin→system→cloud→user→profile→项目层→runtime,低→高入 Vec;项目层受黑名单+信任度约束。
▼
③ 合成 requirements:compose_requirements
system/cloud/legacy/admin 四来源;普通字段低→高合并,deny_read/hooks/rules 高→低并集。
▼
④ 合并视图:effective_config()
递归 merge_toml_values(表递归、标量覆盖)+ 管理端 provider 强制替换。
▼
⑤ 反序列化:ConfigToml try_into
失败时定位"第一层出错的文件"报给用户——十来个来源里精确指出哪份坏了。
▼
⑥ 施加约束:apply_to_config + ManagedFeatures
管理值覆盖用户配置(冲突→启动警告);feature pin 归一化+校验,不满足直接报错。
▼
⑦ 最终产物:Config 对象
session、tools、sandboxing 全部从这里读配置——后面每一讲的主角都站在这一讲的肩膀上。
七、本讲小结:配置系统关键文件
带走三句话:🔹 Codex 的配置不是"一个文件"而是"两条栈"——config 层管值(十来个来源低→高叠),requirements 层管强制(企业策略独立合成);🔹 feature flag 是带户口的:Stage 五阶段 + FeatureSpec 注册表把代码枚举与配置键名绑死,Removed 的 flag 保留兼容、未知键 warn 不报错;🔹 管理端约束分两级强度——feature pin 冲突直接拒绝启动(fail-loud),普通字段冲突覆盖+警告(soft override),对应能力开关与配置值两类风险。
下一讲进入 Model Provider 抽象:config.toml 里的 [model_providers.xxx] 是怎么变成可插拔后端的——OpenAI、Bedrock、Ollama/LM Studio 共用同一套 provider 接口,以及 models-manager 如何发现可用模型。
📚 系列导航
← 第 3 讲:Protocol 协议层——SQ/EQ 双队列与权限模型
→ 第 5 讲:Model Provider 抽象与多后端
关注公众号「AI技术推荐官」获取更多源码解析内容