中文 Books

Configuration, Authentication, and Managed Requirements

Reading Contract: Read configuration as an authority boundary, not as preferences sprinkled across the process. Track which layer owns provenance, which layer owns requirements, which layer owns permission profiles, and which layer owns auth refresh. After this chapter, you should be able to explain why a Codex turn starts only after one resolved envelope exists.

Configuration layers flowing into ConfigLayerStack, provenance, source-aware errors, and a resolved runtime envelope
Configuration becomes usable only after layers, provenance, profiles, project trust, and overrides have been compiled into one source-aware runtime envelope.

Source boundary: direct source claims in this chapter are pinned to OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3. ConfigBuilder, Config, Permissions, ManagedFeatures, permission profile compilation, and AuthManager behavior are verified source where linked. The terms “runtime envelope”, “constraint clamp”, and “same answer before tools” are surrounding contract inference from those source shapes and the public app-server permission schema; they are not claims about hidden OpenAI service internals.

Chapter 2 stopped at the Rust command router. The router can now decide whether the invocation is the TUI, exec, review, app-server, MCP, plugin, or another surface. That is still not enough to start an agent turn. Before any surface can run tools or call a model, Codex has to answer a different question:

What is allowed in this process, for this workspace, under this identity,
with these managed requirements?

Calling that answer “configuration” understates the boundary. Model choice, provider settings, approval mode, permission profile, filesystem roots, network policy, feature state, web search mode, managed hooks, MCP server requirements, and auth state all shape what later code may do. If each subsystem reread files and environment variables for itself, one component could think the workspace is writable, another could treat it as read-only, and a third could keep using a token after the auth layer has refreshed.

Codex instead compiles an envelope early. Later subsystems can still receive scoped updates, but those updates pass through constrained values and known owners. That is the difference between a pile of settings and a runtime contract.

1. The Stack Is Compiled, Not Sampled

The central source path starts in ConfigBuilder::build_inner. The builder resolves the Codex home directory, resolves the effective working directory, passes CLI overrides into the loader, and receives a ConfigLayerStack. Only then does it deserialize the merged TOML into 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));
    }
};

Two details matter. First, the stack is not just a map of final values. The Config struct keeps the config_layer_stack as provenance for how the final config was derived. Second, relative path resolution happens before final deserialization. A path from a project layer and a path from a home layer must not be interpreted against the wrong directory.

Layer roleWhat it can expressWhy provenance matters
Built-in defaultsBaseline behavior when no user layer exists.Defaulted values should not be blamed on a user file.
User/home configPersistent personal defaults.Errors can point back to the user-owned layer.
Profile configNamed mode choices.A profile can be selected without losing source identity.
Project configWorkspace-specific settings.Trust decides whether the workspace may affect the runtime.
CLI overridesInvocation-specific choices.The current command can override lower layers without erasing them.
RequirementsManaged constraints.Illegal choices should explain which policy source rejected them.

This is why a simple “last writer wins” config merge would be too weak. It can produce a value, but it cannot reliably explain it.

1.1 Project Trust Changes the Default Envelope

Project config is powerful because repositories need local defaults. It is also dangerous because a repository is input data. In the pinned source, ProjectConfig stores an optional TrustLevel, and the config builder resolves the active project from the current working directory and repository root before choosing 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 });

The default permission profile then depends on that active project and the platform sandbox situation. In default_builtin_permission_profile_name, a project with an explicit trusted or untrusted decision normally defaults to :workspace; otherwise Codex falls back to :read-only. On Windows with no usable sandbox, the default remains conservative.

The important reading is not “trusted means safe.” The important reading is “trust is a first-class input to the envelope.” A project can influence workspace defaults only through a resolved trust decision, not by silently becoming another global config file.

Trust answers whether the project may contribute defaults. Requirements answer which resolved values are legal after every preference layer has spoken.

2. Requirements Clamp Preferences

Preferences say what the user, profile, or surface asked for. Requirements say what the environment permits. Codex keeps that distinction visible in ConfigRequirements. The struct carries constrained values for approval policy, reviewer choice, permission profile, web search mode, residency, network and filesystem constraints, feature requirements, hooks, MCP servers, plugins, and exec policy, plus the source for Guardian policy config.

Managed requirements clamping preferences for approval, web search, sandbox, feature pins, residency, and illegal requests
Requirements are restrictive overlays: a requested setting must pass the allowed policy set before it becomes part of the runtime envelope.

The TOML-facing shape, ConfigRequirementsToml, shows the policy vocabulary. A managed layer can restrict approval policies, reviewers, sandbox modes, web search modes, features, hooks, MCP servers, plugins, apps, exec rules, residency, network behavior, permissions, and 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>,
}

The source wrapper is not ornamental. ConfigRequirementsWithSources uses Sourced<T> so that a rejected value can say which requirement source rejected it. The test constraint_error_includes_cloud_requirements_source locks that behavior: trying to set AskForApproval::Never against cloud requirements that allow only OnRequest returns an invalid-value error with RequirementSource::CloudRequirements.

That small test captures the larger rule: preference resolution is incomplete until managed policy either admits the value or produces an attributable rejection.

2.1 The Final Config Must Consume Every Requirement Field

Inside Config::load_config_with_layer_stack, Codex destructures ConfigRequirements into local bindings. The comment is blunt: every field must be applied to the 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();

That destructuring pattern is a maintenance guard. If a new requirements field appears, the compiler makes the construction site confront it. A policy field should not exist in a managed requirements file while the runtime forgets to apply it.

2.2 Source-Aware Errors Are Product Behavior

Source-aware errors are a user-experience feature and a safety feature at the same time. A user who sees “approval policy rejected” needs to know whether the rejection came from a local file, cloud policy, system management, or another layer. Without that, the only available fixes are guesswork: edit the wrong file, loosen the wrong setting, or assume the product is broken.

The code path also makes policy auditable. ConfigRequirementsWithSources::merge_unset_fields fills only missing fields, tags accepted values with their RequirementSource, and gives app enablement a restrictive merge path when disablement appears in a lower-precedence source. The final Config can then carry constrained values forward instead of flattening policy into anonymous booleans.

3. Permission Profile Is the Canonical Security Shape

The chapter title says “configuration”, but the most security-sensitive part is permission resolution. In the pinned source, Permissions does not store only a legacy sandbox mode. It stores a constrained canonical PermissionProfile, an optional active profile identity, network proxy config, login-shell policy, shell environment policy, and Windows sandbox settings.

The public app-server schema mirrors the same architectural split. In app-server-protocol, PermissionProfile has three variants:

Public shapeMeaning
ManagedCodex owns sandbox construction for filesystem and network.
DisabledNo outer sandbox should be applied.
ExternalFilesystem isolation is enforced by an external caller, while network policy remains represented.

That schema is important because clients need to talk about permissions without reconstructing private core types. A UI can display an active profile. An app-server client can receive a profile snapshot. A session can later add a bounded writable-root modification. All of those are different from “the model asked to run a command.”

Permission profile compiler turning builtin and custom profiles into filesystem policy, network policy, writable roots, deny globs, and startup warnings
Permission profiles are compiled into concrete filesystem and network policy before tool execution sees them.

3.1 Built-In Profiles Short-Circuit to Runtime Permissions

The compiler entry point is compile_permission_profile_selection. Built-in profiles are converted directly to runtime permissions. Custom profiles must exist under [permissions], must not use reserved built-in prefixes, and must compile their filesystem and 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)
}

The lower-level compile_permission_profile starts from restricted policy, compiles filesystem entries, validates glob scan depth, and resolves network policy. If no filesystem entries are present, it pushes a startup warning rather than pretending a profile means something it does not say.

3.2 The Active Profile Is a Stable Client Contract

Once profiles are active, the config builder chooses the default profile, compiles it, constructs a canonical PermissionProfile, and records an ActivePermissionProfile when doing so is safe. The source around default_permissions and active_permission_profile is worth reading together.

If the profile is implicit and legacy workspace-write customizations are active, Codex avoids advertising a re-selectable active profile because doing so would lose roots, network, or temp settings. If additional writable roots were requested and the profile remains managed, it records those roots as active profile modifications. That is the subtle part: client-visible state must be round-trippable, not merely descriptive.

Once permission resolution has a canonical shape, the same pattern appears in less obvious places: feature state and auth state also need owners before the runtime can treat them as facts.

4. Managed Features Are Lifecycle Policy

Feature flags look softer than permission profiles, but in a multi-surface runtime they still need an owner. Codex wraps feature state in ManagedFeatures. The wrapper stores a constrained value and a map of pinned features.

The construction path is short and revealing:

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())?;

That source comes from ManagedFeatures::from_configured_with_optional_warnings. The helper normalize_candidate sets pinned values and then normalizes dependencies. The validator reports an invalid-value error when a normalized candidate violates a sourced requirement. The nearby parse_feature_requirements path keeps compatibility visible: canonical keys are accepted, legacy keys warn and map forward, and unknown requirement keys warn instead of becoming hidden runtime facts.

Feature state pressureMechanismInvariant
A profile and global config both set feature values.Features::from_sources merges sources before management.One normalized feature object enters config.
A managed requirement pins a feature.ManagedFeatures stores pinned_features.Later mutation cannot silently violate policy.
A legacy feature key appears.parse_feature_requirements maps or warns.Compatibility is visible, not hidden.
A feature implies another feature.normalize_dependencies.Runtime sees a dependency-consistent set.

This is lifecycle management. A feature can be renamed, aliased, pinned, warned, or staged without making every downstream subsystem understand every historical spelling.

5. Auth Is a Coherent Snapshot

Configuration says what the process is allowed to try. Auth says which identity and backend capabilities the process can use. Codex keeps that concern in codex-login, and Config implements AuthManagerConfig so the auth manager can be created from the already-resolved config without depending on core internals.

AuthManager snapshot with API key, ChatGPT OAuth, external bearer, cached auth, guarded reload, refresh lock, consumers, and unauthorized recovery
AuthManager gives later layers a coherent auth snapshot and serializes refresh so different clients do not observe competing identities.

The source comment above AuthManager states the design directly: external modifications to auth.json are not observed until explicit reload, matching the goal of avoiding inconsistent auth data mid-run.

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()
}

This auth method first resolves external API-key auth if configured, then reads the cached auth snapshot, refreshes stale managed ChatGPT auth when possible, and returns the current cached value. It does not invite every caller to parse storage.

5.1 Refresh Is Guarded by Account Identity

The refresh path is also snapshot-oriented. In refresh_token, Codex acquires a refresh lock, skips API-key auth, records the expected account id, reloads only when the persisted account id still matches, and refreshes from the authority only if the on-disk value did not already change.

That prevents a common long-running-process bug. Suppose one surface notices a 401 and begins refresh while another sign-in action changes the account. A naive refresh could overwrite or reuse credentials for the wrong identity. The guarded reload path treats the auth snapshot as scoped state, not as a global string.

The lower-level refresh_token_from_authority_impl then chooses the correct authority behavior: external ChatGPT tokens refresh through the external auth provider; managed ChatGPT auth refreshes and persists new token data; API key and agent identity auth are already terminal for this path.

6. The Envelope Is Handed Off Once

By the end of config construction, the runtime has one envelope with several owners:

OwnerResolved valueLater consumers
Config loaderLayered, source-aware settings.TUI, exec, app-server, session startup.
Requirements engineConstrained approval, permissions, features, web search, residency, hooks, MCP, and plugins.Config construction and client-visible errors.
Permission compilerCanonical permission profile plus filesystem/network policy.Tool runtime, sandbox selection, app-server permissions schema.
Managed featuresDependency-normalized feature state with pinned requirements.UI behavior, schema filtering, runtime feature checks.
Auth managerCached identity and guarded refresh path.Model clients, backend APIs, unauthorized recovery.
Resolved envelope handed to session loop, model provider, tool runtime, and app-server with invalid envelope blocked before execution
The protected invariant is simple: the session loop, model provider, tool runtime, and app-server should consume the same answer before tools run.

That handoff is why later chapters can talk about tools, sandboxes, sessions, protocol messages, hooks, and MCP as separate boundaries. They are separate owners, but they do not get to invent separate answers to the permission and identity questions. If the envelope is invalid, execution should fail before side effects. If it is valid, downstream code can specialize the answer without reopening the whole configuration stack.

Common Misreadings

MisreadingCorrection
”Config is just user preference.”Requirements are restrictive constraints, and several fields carry requirement source.
”Project config is either ignored or fully trusted.”Project trust is a resolved input that changes defaults and effective project behavior.
sandbox_mode is the security model.”Modern code centers the canonical PermissionProfile, then projects compatibility sandbox policy where needed.
”Feature flags are plain booleans.”ManagedFeatures normalizes dependencies and enforces pinned requirement values.
”Auth refresh is just fetching a new token.”Auth refresh is guarded by snapshot equality, account identity, and a refresh lock.

Apply This

  1. Carry provenance with config values. The final value is not enough when a user must debug why a policy rejected it.
  2. Merge preferences before applying requirements. Requirements should constrain the result, not compete as another peer preference layer.
  3. Make trust an explicit input. Project config should not silently control the agent that is about to operate inside the project.
  4. Prefer canonical permission profiles. Legacy sandbox modes are compatibility projections; tooling should reason over the profile when possible.
  5. Treat auth as scoped state. Long-running processes need cached snapshots, explicit reloads, and guarded refresh, not ad hoc token reads.

Closing

Chapter 2 explained how startup narrows a package command into a typed Rust surface. This chapter explained why that surface still waits for a resolved envelope. Chapter 4 moves to the next boundary: once the envelope exists, work has to cross process and client boundaries as durable protocol messages rather than private method calls.

Source Map

ConceptSource anchor
Config builder and layer stackcodex-rs/core/src/config/mod.rs
Config and Permissions shapescodex-rs/core/src/config/mod.rs
Requirements shape and source trackingcodex-rs/config/src/config_requirements.rs
Requirements TOML vocabularycodex-rs/config/src/config_requirements.rs
Requirement source mergecodex-rs/config/src/config_requirements.rs
Requirement source error testcodex-rs/config/src/config_requirements.rs
Project trustcodex-rs/config/src/config_toml.rs
Active project resolutioncodex-rs/core/src/config/mod.rs
Default permission profilecodex-rs/core/src/config/permissions.rs
Permission profile compilationcodex-rs/core/src/config/permissions.rs
Active profile round-trip behaviorcodex-rs/core/src/config/mod.rs
Public permission profile schemacodex-rs/app-server-protocol/src/protocol/v2/permissions.rs
Managed featurescodex-rs/core/src/config/managed_features.rs
Feature source mergecodex-rs/features/src/lib.rs
Feature requirement parsingcodex-rs/core/src/config/managed_features.rs
Auth manager snapshotcodex-rs/login/src/auth/manager.rs
Guarded auth refreshcodex-rs/login/src/auth/manager.rs