配置系统 · 分层加载与托管要求

Codex 的配置由两摞“层”组成:config 层按优先级逐键合并出生效值,requirements 层合成管理员的约束,再把取值关进 Constrained 校验器。本篇沿 codex-config 的加载器走一遍来源与优先级、-c 覆盖、profile、项目信任、未知字段告警,以及改写 config.toml 的方式。

作者 David更新于 第 35 篇(共 57 篇)

配置系统 · 分层加载与托管要求

第 5 部分讲扩展机制。skills、hooks、插件、MCP 服务器最终都落在 config.toml 的某张表里,所以先把配置系统本身拆开:值从哪几层来、谁覆盖谁、管理员的约束怎么压住用户的取值。

怎么用

用户配置在 ~/.codex/config.toml,--profile work 再叠一份 ~/.codex/work.config.toml;仓库里的 .codex/config.toml 只在项目受信任时生效;-c key=value 单次覆盖,--enable / --disable 切换功能开关;/etc/codex/config.toml 提供默认值,requirements.toml 提供管理员的强制约束。TUI 里 /debug-config 列出每一层与约束来源,/warnings 查看启动警告。写法见手册配置文件 config.toml与配置项速查。

全景:两摞层,一条流水线

配置代码分在三个 crate:codex-config(codex-rs/config/)负责读文件、分层、合并、约束与诊断,不认识会话;codex-core 的 config/mod.rs 把合并结果做成运行时用的 Config,这个结构体有 140 多个公开字段(codex-rs/core/src/config/mod.rs:608);codex-features 维护功能开关表。入口是 ConfigBuilder::build(codex-rs/core/src/config/mod.rs:1475):

图表加载中…

load_config_layers_state(codex-rs/config/src/loader/mod.rs:145)一次产出这两摞:config 层是“可被覆盖的取值”,合并成一张 TOML 表后反序列化为 ConfigToml;requirements 层是“不可越过的约束”,合成 ConfigRequirements,在构造 Config 时逐项压上去。

config 层:一个整数决定谁覆盖谁

每一层都带一个 ConfigLayerSource 标明出处,优先级就是一个整数:

    pub fn precedence(&self) -> i16 {
        match self {
            ConfigLayerSource::PackagedDefaults { .. } => -10,
            ConfigLayerSource::Mdm { .. } => 0,
            ConfigLayerSource::System { .. } => 10,
            ConfigLayerSource::EnterpriseManaged { .. } => 15,
            ConfigLayerSource::User { profile, .. } => {
                if profile.is_some() {
                    21
                } else {
                    20
                }
            }
            ConfigLayerSource::Project { .. } => 25,
            ConfigLayerSource::SessionFlags => 30,
            ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40,
            ConfigLayerSource::LegacyManagedConfigTomlFromMdm => 50,
        }
    }

(codex-rs/config/src/config_layer_source.rs:33)

层内容优先级
PackagedDefaults编进二进制的 codex-rs/config/defaults.toml,如 project_root_markers = [".git"]、project_doc_max_bytes = 32768-10
System/etc/codex/config.toml,Windows 为 %ProgramData%\OpenAI\Codex\config.toml10
EnterpriseManaged企业工作区从云端下发的配置包15
User$CODEX_HOME/config.toml;选了 profile 再叠一层20、21
Project项目根到 cwd 沿途每个 .codex/config.toml25
SessionFlags-c 覆盖,以及 ThreadConfigLoader 提供的线程级配置30
LegacyManagedConfigTomlFrom*旧式 managed_config.toml,来自文件或 macOS MDM40、50

ConfigLayerStack::new 用 verify_layer_ordering 确认层按这个整数有序、项目层从根到 cwd 排列(codex-rs/config/src/state.rs:594);effective_config() 从低到高依次合并,带 disabled_reason 的层跳过。每层还存一个 version:把 TOML 转成键已排序的 JSON 再算 SHA-256,写回配置时要用。

枚举里优先级为 0 的 Mdm 变体,v0.158.0 的加载器并不产出:macOS MDM 下发的 config_toml_base64 被当作旧式托管配置,以 LegacyManagedConfigTomlFromMdm(50)压在最上面。load_config_layers_state 的文档注释把 MDM 下发的配置排在 system 之下,与代码不符(同目录的 README.md 则与代码一致),以代码为准。

合并与 -c 覆盖

合并规则在 merge_toml_values(codex-rs/config/src/merge.rs:57):两边都是表就逐键递归,否则上层整个替换下层——数组也不例外,高层写的 writable_roots 会整体盖掉低层的,而不是追加。特例只有几处,例如 features.code_mode 这类既能写布尔又能写表的结构化开关,合并时在两种形态之间转换。合并之前,各层的相对路径已按该层文件所在目录解析成绝对路径(resolve_relative_paths_in_config_toml),所以项目配置里的 ./x 指向 .codex/ 下,而不是 cwd。

-c 由 CliConfigOverrides 按第一个 = 拆开,右半边拼成 _x_ = <值> 交给 TOML 解析器,再取回哨兵键 _x_;解析失败就去掉首尾引号当字符串,所以 -c model=o3 不必加引号(codex-rs/utils/cli/src/config_override.rs:95)。build_cli_overrides_layer 按点号把路径展开成嵌套表,成为 SessionFlags 层。子命令前后都能写 -c,根级的排在前面,后写的覆盖先写的。--enable x 校验 x 是已知开关后,翻译成 features.x=true 追加进同一个列表(codex-rs/cli/src/main.rs:959)。

profile 与项目层

--profile work 让用户层路径指向 $CODEX_HOME/work.config.toml,加载器先读基础的 config.toml,再把 profile 作为第二个 User 层(21)叠上去;名字只允许 ASCII 字母、数字、_ 与 -。基础配置里还留着旧写法 profile = "work" 或 [profiles.work] 时,加载直接报错,要求迁移。

项目层要先找项目根:从 cwd 往上找 project_root_markers,默认 .git,而且 .git 目录里必须有 HEAD 才算。找根之前,加载器先把 system、用户、profile、-c 与托管配置合并一遍,只用来读 markers 和 [projects] 信任表——哪里算项目、哪个项目受信任,由用户和管理员决定,仓库自己说了不算。然后从根到 cwd,每个存在 .codex/ 目录的层级生成一个 Project 层;没有 config.toml 也留一个空层,因为同目录下的 hooks、rules 还要靠它定位(codex-rs/config/src/loader/mod.rs:1638)。

信任只决定层的 disabled_reason:未受信任的项目层照样加载、照样出现在 /debug-config 里,只是合并时跳过;它的 TOML 语法错误也不致命,受信任的层才会报错退出。受信任也不等于全权:

// 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.
// These settings are still supported from user, system, managed, and runtime
// config layers.
const PROJECT_LOCAL_CONFIG_DENYLIST: &[&str] = &[
    "openai_base_url",
    "chatgpt_base_url",
    // ...
    "model_provider",
    "model_providers",
    "notify",
    // ...
    "otel",
];

(codex-rs/config/src/loader/mod.rs:84)

sanitize_project_config 删掉这些键并给出启动警告,还剔除 tui.keymap.chat 下切换权限模式的两个绑定——注释的说法是,仓库内容不能把一个普通按键变成权限提升(codex-rs/config/src/loader/mod.rs:1137)。

requirements:另一摞层

requirements 的来源同样从低到高收集:system requirements.toml、云端配置包里的 requirements、改读成要求的旧 managed_config.toml、macOS MDM 的 requirements_toml_base64(codex-rs/config/src/loader/managed_requirements.rs:69)。compose_requirements 对大多数字段沿用 config 的 TOML 合并,只有 rules.prefix_rules、hooks、permissions.filesystem.deny_read、auto_review.required_on_models 四类按“高优先级在前”追加或取并集(codex-rs/config/src/requirements_layers/stack.rs:1)。合成的 ConfigRequirementsToml 有四十来个键(codex-rs/config/src/config_requirements.rs:1029),落到 Config 上有两种压法:

  • 精确值:cli_auth_credentials_store、chatgpt_base_url、sqlite_home、log_dir、model_provider 等,由 apply_to_config 直接替换用户的值,不一致时记一条警告(codex-rs/core/src/config/requirements.rs:17)。
  • 允许列表:allowed_approval_policies、allowed_sandbox_modes、allowed_web_search_modes 等变成 Constrained<T>——一个值加一个校验闭包,set 时先规范化、再校验(codex-rs/config/src/constraint.rs:174)。列表第一项是初始值,也是回退值。

用户配的值不在允许范围内时,并不直接失败:

    if let Err(err) = constrained_value.set(configured_value) {
        let fallback_value = constrained_value.get().clone();
        // ...
        let message = format!(
            "Configured value for `{field_name}` is disallowed by requirements; falling back to required value {fallback_value:?}. Details: {err}"
        );
        startup_warnings.push(message);

        constrained_value.set(fallback_value).map_err(|fallback_err| {
            // ...
        })?;
        return Ok(true);
    }

(codex-rs/core/src/config/mod.rs:2247)

回退之后还要检查组合是否自洽:用户要的完全访问被要求强制降成只读、而 approval_policy 又是 never 时,Codex 拒绝启动并说明原因,而不是悄悄变成“只读又不许问”(codex-rs/core/src/config/mod.rs:4109)。约束也不止于启动:Config.permissions.approval_policy、web_search_mode、mcp_servers 等字段本身就是 Constrained,会话中途切换同样要过校验;ManagedFeatures 把 requirements 的 [features] 固定的开关钉住(codex-rs/core/src/config/managed_features.rs:24);app-server 的写配置接口遇到被精确值管住的键,直接返回 ConfigRequirementReadonly。

未知字段与报错定位

ConfigToml 的 serde 派生没有开 deny_unknown_fields(它上面的 #[schemars(deny_unknown_fields)] 只影响生成的 JSON Schema),拼错的键会被静默丢掉。为了不静默,加载末尾的 ignored_config_warning 用 serde_ignored 把生效配置再反序列化一遍,收集被忽略的路径,再逐层找出这个键写在哪个文件里;[features] 里不在开关表中的键另算未知。警告最多列三条、每条注明来源层,其余只报数量(codex-rs/config/src/strict_config.rs:205)。加 --strict-config 时,同样的检查在读每个文件时就做,第一处未知字段直接报错。

类型错误走另一条路:合并后的表反序列化失败,first_layer_config_error 就逐层单独反序列化,找到第一个出错的文件,用 toml_edit 定位行列,渲染成编译器风格的报错(codex-rs/config/src/diagnostics.rs:170)。[tui.keymap] 是例外:它的结构体开了 serde(deny_unknown_fields),写错动作名是硬错误。

功能开关与写回

codex-features 用静态表 FEATURES 登记全部开关,每项是 FeatureSpec { id, key, stage, default_enabled }(codex-rs/features/src/lib.rs:916)。v0.158.0 共 151 项:稳定 47、开发中 57、实验 2(prevent_idle_sleep 在三大桌面系统上另算实验)、已弃用 4、已移除 40。Features::from_sources 从默认集合出发,套上 [features] 表与旧顶层开关,最后补依赖,例如开了 code_mode_only 就自动开 code_mode。

改配置文件的是 ConfigEditsBuilder:把改动收集成 ConfigEdit 列表,解析符号链接找到真正的目标文件,用 toml_edit::DocumentMut 读入以保留注释与排版,逐条修改,确有变化才原子写回(codex-rs/core/src/config/edit.rs:731)。用 ConfigEditsBuilder::for_config 构造时,写入目标是当前生效的用户层,选了 profile 就写那份 profile 文件。关闭一个默认就关的开关时,它删掉这个键而不是写 false,免得开关将来转为默认开启时被旧配置钉死(codex-rs/core/src/config/edit.rs:867)。app-server 的 config/value/write 只允许写用户配置,请求带上次读到的 expected_version,与用户层当前的 SHA-256 版本不符就返回 ConfigVersionConflict,多个前端同时改配置时靠它避免互相覆盖(codex-rs/app-server/src/config_manager_service.rs:218)。另有 codex-write-config-schema 从这些类型生成 codex-rs/core/config.schema.json 供编辑器补全,测试 config_schema_matches_fixture 保证它与代码同步。

和《从 LLM 到 Coding Agent》对照

那本书的权限系统把权限模式当成用户手里的总开关,从 plan 到 bypass 一拨就变。Codex 在用户之上又加了一层:requirements 在加载配置时就收窄可选的审批策略与沙箱模式,选了不允许的值会被拉回允许列表的第一项,会话里再切换也过不了 Constrained 的校验。OpenCode 的配置系统同样多层深合并,托管目录与 macOS MDM 排在最后、覆盖一切;Codex 则把“可覆盖的默认值”和“不可越过的约束”拆成两摞,互不混用。


上一篇:自动审查 · 让另一个模型替你审批 · 下一篇:Skills · 发现、选择与注入

本页目录