配置、认证与 Managed Requirements
阅读契约: 把配置读成 authority boundary,而不是散落在进程里的 preferences。追踪哪一层拥有 provenance,哪一层拥有 requirements,哪一层拥有 permission profiles,哪一层拥有 auth refresh。读完本章后,你应该能解释为什么 Codex turn 只在一个 resolved envelope 存在后才开始。

源码边界: 本章的 direct source claims 固定到 OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3。ConfigBuilder、Config、Permissions、ManagedFeatures、permission profile compilation 和 AuthManager behavior 在链接处属于 verified source。“runtime envelope”、“constraint clamp”和“same answer before tools”等术语,是从这些 source shapes 和 public app-server permission schema 得出的 surrounding contract inference;它们不是关于 OpenAI 隐藏服务内部的断言。
第 2 章停在 Rust command router。Router 现在可以决定 invocation 是 TUI、exec、review、app-server、MCP、plugin 或另一个 surface。即便如此,agent turn 仍不能开始。任何 surface 运行 tools 或调用 model 前,Codex 必须先回答另一个问题:
What is allowed in this process, for this workspace, under this identity,
with these managed requirements?
把这个答案叫“configuration”会低估这个边界。Model choice、provider settings、approval mode、permission profile、filesystem roots、network policy、feature state、web search mode、managed hooks、MCP server requirements 和 auth state 都会塑造后续代码可以做什么。如果每个 subsystem 都自己重新读取 files 和 environment variables,一个 component 可能认为 workspace writable,另一个 component 认为 read-only,第三个 component 可能在 auth layer 已刷新后仍使用旧 token。
Codex 选择提前编译一个 envelope。后续 subsystem 仍然可以接收 scoped updates,但这些 updates 会穿过 constrained values 和 known owners。这就是一堆 settings 与 runtime contract 的区别。
一、Stack 被编译,而不是被抽样
核心源码路径从 ConfigBuilder::build_inner 开始。Builder 解析 Codex home directory,解析 effective working directory,把 CLI overrides 传给 loader,并接收一个 ConfigLayerStack。只有之后,它才把 merged TOML deserialize 成 ConfigToml。
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&cli_overrides,
loader_overrides,
cloud_requirements,
thread_config_loader
.as_deref()
.unwrap_or(&codex_config::NoopThreadConfigLoader),
)
.await?;
let merged_toml = config_layer_stack.effective_config();
// Each layer already resolved relative paths against its config file.
let config_toml: ConfigToml = match merged_toml.try_into() {
Ok(config_toml) => config_toml,
Err(err) => {
if let Some(config_error) = codex_config::first_layer_config_error::<ConfigToml>(
&config_layer_stack,
codex_config::CONFIG_TOML_FILE,
)
.await
{
return Err(codex_config::io_error_from_config_error(
std::io::ErrorKind::InvalidData,
config_error,
Some(err),
));
}
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err));
}
};
两个细节很重要。第一,stack 不只是最终 values 的 map。Config struct 会保留 config_layer_stack,作为 final config 如何被推导出来的 provenance。第二,relative path resolution 发生在 final deserialization 之前。来自 project layer 的 path 与来自 home layer 的 path 不应该被解释到错误目录下。
| Layer role | 能表达什么 | 为什么 provenance 重要 |
|---|---|---|
| Built-in defaults | 没有 user layer 时的 baseline behavior。 | Defaulted values 不应该被归咎于 user file。 |
| User/home config | 持久 personal defaults。 | Errors 可以指回 user-owned layer。 |
| Profile config | 命名 mode choices。 | Profile 可以被选中,而不会丢失 source identity。 |
| Project config | Workspace-specific settings。 | Trust 决定 workspace 是否可以影响 runtime。 |
| CLI overrides | Invocation-specific choices。 | 当前 command 可以 override lower layers,而不抹掉来源。 |
| Requirements | Managed constraints。 | Illegal choices 应该说明哪个 policy source 拒绝了它们。 |
这就是为什么简单的 “last writer wins” config merge 太弱。它可以生成一个 value,但不能可靠解释这个 value。
1.1 Project Trust 改变默认 Envelope
Project config 很强,因为 repositories 需要 local defaults。它也危险,因为 repository 是 input data。在固定源码中,ProjectConfig 存储 optional TrustLevel,config builder 先从 current working directory 和 repository root 解析 active project,再选择 permission defaults。
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
let active_project = cfg
.get_active_project(
resolved_cwd.as_path(),
repo_root.as_ref().map(AbsolutePathBuf::as_path),
)
.unwrap_or(ProjectConfig { trust_level: None });
默认 permission profile 随 active project 和 platform sandbox situation 改变。在 default_builtin_permission_profile_name 中,有显式 trusted 或 untrusted decision 的 project 通常默认 :workspace;否则 Codex fallback 到 :read-only。在没有可用 sandbox 的 Windows 上,默认仍保持 conservative。
关键阅读不是“trusted 就 safe”。关键阅读是“trust 是 envelope 的 first-class input”。Project 只能通过 resolved trust decision 影响 workspace defaults,而不是静默变成另一个 global config file。
Trust 回答 project 是否可以贡献 defaults。Requirements 回答每个 preference layer 发言后 resolved values 是否合法。
二、Requirements Clamp Preferences
Preferences 表示 user、profile 或 surface 想要什么。Requirements 表示 environment 允许什么。Codex 在 ConfigRequirements 中保持这个区别可见。这个 struct 携带 approval policy、reviewer choice、permission profile、web search mode、residency、network 与 filesystem constraints、feature requirements、hooks、MCP servers、plugins、exec policy 等 constrained values,并携带 Guardian policy config 的 source。

面向 TOML 的 shape ConfigRequirementsToml 展示 policy vocabulary。Managed layer 可以限制 approval policies、reviewers、sandbox modes、web search modes、features、hooks、MCP servers、plugins、apps、exec rules、residency、network behavior、permissions 和 Guardian policy config。
pub struct ConfigRequirements {
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
pub network: Option<Sourced<NetworkConstraints>>,
pub filesystem: Option<Sourced<FilesystemConstraints>>,
pub guardian_policy_config_source: Option<RequirementSource>,
}
Source wrapper 不是装饰。ConfigRequirementsWithSources 使用 Sourced<T>,让 rejected value 能说明哪个 requirement source 拒绝了它。测试 constraint_error_includes_cloud_requirements_source 锁住这个行为:当 cloud requirements 只允许 OnRequest,却尝试设置 AskForApproval::Never 时,返回的 invalid-value error 会携带 RequirementSource::CloudRequirements。
这个小测试捕捉了更大的规则:preference resolution 只有在 managed policy 接受 value 或产生 attributable rejection 后才算完成。
2.1 Final Config 必须消费每个 Requirement Field
在 Config::load_config_with_layer_stack 内部,Codex 把 ConfigRequirements destructure 成 local bindings。注释很直接:每个 field 都必须应用到 final Config。
// Ensure that every field of ConfigRequirements is applied to the final
// Config.
let ConfigRequirements {
approval_policy: mut constrained_approval_policy,
approvals_reviewer: mut constrained_approvals_reviewer,
permission_profile: mut constrained_permission_profile,
web_search_mode: mut constrained_web_search_mode,
feature_requirements,
managed_hooks: _,
mcp_servers,
plugins: _,
exec_policy: _,
enforce_residency,
network: network_requirements,
filesystem: filesystem_requirements,
guardian_policy_config_source: _,
} = config_layer_stack.requirements().clone();
这个 destructuring pattern 是 maintenance guard。如果出现新的 requirements field,compiler 会让 construction site 直面它。Managed requirements file 中不应该存在 runtime 忘记应用的 policy field。
2.2 Source-Aware Errors 是 Product Behavior
Source-aware errors 同时是 user-experience feature 和 safety feature。用户看到“approval policy rejected”时,需要知道 rejection 来自 local file、cloud policy、system management 还是另一个 layer。否则可用修复只有猜:改错文件、放宽错设置,或误以为 product broken。
Code path 也让 policy 可审计。ConfigRequirementsWithSources::merge_unset_fields 只填 missing fields,用 RequirementSource 标记 accepted values,并在 lower-precedence source 出现 disablement 时给 app enablement 一个 restrictive merge path。Final Config 可以继续携带 constrained values,而不是把 policy flatten 成 anonymous booleans。
三、Permission Profile 是 Canonical Security Shape
章标题说“configuration”,但最 security-sensitive 的部分是 permission resolution。在固定源码中,Permissions 不只存 legacy sandbox mode。它存 constrained canonical PermissionProfile、optional active profile identity、network proxy config、login-shell policy、shell environment policy 和 Windows sandbox settings。
Public app-server schema 也镜像了同一个架构拆分。在 app-server-protocol 中,PermissionProfile 有三个 variants:
| Public shape | Meaning |
|---|---|
Managed | Codex 拥有 filesystem 和 network 的 sandbox construction。 |
Disabled | 不应该应用 outer sandbox。 |
External | Filesystem isolation 由 external caller enforced,而 network policy 仍被表示。 |
这个 schema 很重要,因为 clients 需要谈论 permissions,而不重建 private core types。UI 可以显示 active profile。App-server client 可以收到 profile snapshot。Session 稍后可以加入 bounded writable-root modification。所有这些都不同于“模型要求运行命令”。

3.1 Built-In Profiles 直接转成 Runtime Permissions
Compiler entry point 是 compile_permission_profile_selection。Built-in profiles 会直接转换成 runtime permissions。Custom profiles 必须存在于 [permissions] 下,不能使用 reserved built-in prefixes,并且必须能编译其 filesystem 和 network entries。
pub(crate) fn compile_permission_profile_selection(
permissions: Option<&PermissionsToml>,
profile_name: &str,
workspace_write: Option<&SandboxWorkspaceWrite>,
policy_cwd: &Path,
startup_warnings: &mut Vec<String>,
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) {
return Ok(permission_profile.to_runtime_permissions());
}
reject_unknown_builtin_permission_profile(profile_name)?;
let permissions = permissions.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"default_permissions requires a `[permissions]` table",
)
})?;
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
}
更低层的 compile_permission_profile 从 restricted policy 出发,编译 filesystem entries,验证 glob scan depth,并解析 network policy。如果没有 filesystem entries,它会 push startup warning,而不是假装 profile 说了它没有说的东西。
3.2 Active Profile 是稳定 Client Contract
Profiles active 后,config builder 选择 default profile,编译它,构造 canonical PermissionProfile,并在安全时记录 ActivePermissionProfile。default_permissions 和 active_permission_profile 附近的源码值得一起读。
如果 profile 是 implicit,而 legacy workspace-write customizations 处于 active 状态,Codex 会避免把它宣传成可 re-select 的 active profile,因为这样会丢失 roots、network 或 temp settings。如果请求了 additional writable roots 且 profile 仍是 managed,它会把这些 roots 记录为 active profile modifications。微妙点在于:client-visible state 必须可 round-trip,而不能只是 descriptive。
一旦 permission resolution 有 canonical shape,同样模式也会出现在不那么显眼的地方:feature state 和 auth state 也需要 owner,runtime 才能把它们当成 facts。
四、Managed Features 是 Lifecycle Policy
Feature flags 看起来比 permission profiles 更软,但在 multi-surface runtime 中仍然需要 owner。Codex 用 ManagedFeatures 包装 feature state。这个 wrapper 存储 constrained value 和 pinned features map。
Construction path 很短,也很说明问题:
let (pinned_features, source) = match feature_requirements {
Some(Sourced {
value: feature_requirements,
source,
}) => (
parse_feature_requirements(feature_requirements, &source, startup_warnings),
Some(source),
),
None => (BTreeMap::new(), None),
};
let normalized_features = normalize_candidate(configured_features, &pinned_features);
validate_pinned_features(&normalized_features, &pinned_features, source.as_ref())?;
这段来自 ManagedFeatures::from_configured_with_optional_warnings。Helper normalize_candidate 设置 pinned values,然后 normalize dependencies。Validator 在 normalized candidate 违反 sourced requirement 时报告 invalid-value error。附近的 parse_feature_requirements 路径保持 compatibility 可见:canonical keys 被接受,legacy keys warning 后向前映射,unknown requirement keys warning 而不会变成 hidden runtime facts。
| Feature state pressure | Mechanism | Invariant |
|---|---|---|
| Profile 和 global config 都设置 feature values。 | Features::from_sources 在 management 前 merge sources。 | 一个 normalized feature object 进入 config。 |
| Managed requirement pin 住 feature。 | ManagedFeatures 存储 pinned_features。 | 后续 mutation 不能静默违反 policy。 |
| Legacy feature key 出现。 | parse_feature_requirements 映射或 warning。 | Compatibility 可见,而不是隐藏。 |
| Feature 暗含另一个 feature。 | normalize_dependencies。 | Runtime 看到 dependency-consistent set。 |
这是 lifecycle management。Feature 可以被 renamed、aliased、pinned、warned 或 staged,而不需要每个 downstream subsystem 理解每一种历史拼写。
五、Auth 是一致 Snapshot
Configuration 说明 process 允许尝试什么。Auth 说明 process 可以使用哪个 identity 和 backend capabilities。Codex 把这个 concern 放在 codex-login 中,而 Config 实现 AuthManagerConfig,让 auth manager 可以从已经 resolved 的 config 创建,而不依赖 core internals。

AuthManager 上方的源码注释直接说明设计:对 auth.json 的外部修改不会被观察到,直到 explicit reload;这符合避免 mid-run auth data 不一致的目标。
pub async fn auth(&self) -> Option<CodexAuth> {
if let Some(auth) = self.resolve_external_api_key_auth().await {
return Some(auth);
}
let auth = self.auth_cached()?;
if Self::is_stale_for_proactive_refresh(&auth)
&& let Err(err) = self.refresh_token().await
{
tracing::error!("Failed to refresh token: {}", err);
return Some(auth);
}
self.auth_cached()
}
这个 auth method 先在配置存在时解析 external API-key auth,再读取 cached auth snapshot,可能 refresh stale managed ChatGPT auth,最后返回 current cached value。它没有邀请每个 caller 自己 parse storage。
5.1 Refresh 由 Account Identity Guard
Refresh path 也以 snapshot 为中心。在 refresh_token 中,Codex 获取 refresh lock,跳过 API-key auth,记录 expected account id,仅当 persisted account id 仍然匹配时 reload,并且只有 on-disk value 没有已经变化时才从 authority refresh。
这防止了一个常见的 long-running-process bug。假设一个 surface 注意到 401 并开始 refresh,同时另一个 sign-in action 改变了 account。Naive refresh 可能覆盖或复用错误 identity 的 credentials。Guarded reload path 把 auth snapshot 当成 scoped state,而不是 global string。
更低层的 refresh_token_from_authority_impl 选择正确的 authority behavior:external ChatGPT tokens 通过 external auth provider refresh;managed ChatGPT auth refresh 并 persist new token data;API key 和 agent identity auth 对这个 path 已经是 terminal。
六、Envelope 只 Handoff 一次
到 config construction 结束时,runtime 拥有一个由几个 owner 组成的 envelope:
| Owner | Resolved value | Later consumers |
|---|---|---|
| Config loader | Layered、source-aware settings。 | TUI、exec、app-server、session startup。 |
| Requirements engine | Constrained approval、permissions、features、web search、residency、hooks、MCP 和 plugins。 | Config construction 和 client-visible errors。 |
| Permission compiler | Canonical permission profile 加 filesystem/network policy。 | Tool runtime、sandbox selection、app-server permissions schema。 |
| Managed features | Dependency-normalized feature state,带 pinned requirements。 | UI behavior、schema filtering、runtime feature checks。 |
| Auth manager | Cached identity 和 guarded refresh path。 | Model clients、backend APIs、unauthorized recovery。 |

这个 handoff 解释了为什么后续章节可以把 tools、sandboxes、sessions、protocol messages、hooks 和 MCP 作为分离边界讨论。它们是不同 owner,但不能发明各自不同的 permission 和 identity 答案。如果 envelope invalid,execution 应该在副作用前失败。如果 envelope valid,downstream code 可以 specialize 这个答案,而不重新打开整个 configuration stack。
七、常见误读
| Misreading | Correction |
|---|---|
| “Config 只是用户 preference。” | Requirements 是 restrictive constraints,且多个 fields 携带 requirement source。 |
| “Project config 要么被忽略,要么完全 trusted。” | Project trust 是 resolved input,会改变 defaults 和 effective project behavior。 |
“sandbox_mode 是 security model。” | 现代代码以 canonical PermissionProfile 为中心,再在需要处 project compatibility sandbox policy。 |
| “Feature flags 是普通 booleans。” | ManagedFeatures normalize dependencies,并 enforce pinned requirement values。 |
| “Auth refresh 只是获取新 token。” | Auth refresh 由 snapshot equality、account identity 和 refresh lock guard。 |
八、应用到实践
- 让 config values 携带 provenance。 当用户必须 debug 为什么 policy rejected 一个 value 时,final value 不够。
- 先合并 preferences,再应用 requirements。 Requirements 应该 constrain result,而不是作为另一个 peer preference layer 竞争。
- 让 trust 成为 explicit input。 Project config 不应该静默控制即将在 project 内操作的 agent。
- 优先使用 canonical permission profiles。 Legacy sandbox modes 是 compatibility projections;tooling 应尽可能围绕 profile 推理。
- 把 auth 当成 scoped state。 Long-running processes 需要 cached snapshots、explicit reloads 和 guarded refresh,而不是 ad hoc token reads。
九、结语
第 2 章解释了 startup 怎样把 package command 收窄成 typed Rust surface。本章解释为什么这个 surface 仍要等待 resolved envelope。第 4 章进入下一个边界:一旦 envelope 存在,work 必须作为 durable protocol messages 跨越 process 和 client boundaries,而不是通过 private method calls。