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.

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 role | What it can express | Why provenance matters |
|---|---|---|
| Built-in defaults | Baseline behavior when no user layer exists. | Defaulted values should not be blamed on a user file. |
| User/home config | Persistent personal defaults. | Errors can point back to the user-owned layer. |
| Profile config | Named mode choices. | A profile can be selected without losing source identity. |
| Project config | Workspace-specific settings. | Trust decides whether the workspace may affect the runtime. |
| CLI overrides | Invocation-specific choices. | The current command can override lower layers without erasing them. |
| Requirements | Managed 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.

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 shape | Meaning |
|---|---|
Managed | Codex owns sandbox construction for filesystem and network. |
Disabled | No outer sandbox should be applied. |
External | Filesystem 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.”

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 pressure | Mechanism | Invariant |
|---|---|---|
| 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.

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:
| 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, and plugins. | Config construction and client-visible errors. |
| Permission compiler | Canonical permission profile plus filesystem/network policy. | Tool runtime, sandbox selection, app-server permissions schema. |
| Managed features | Dependency-normalized feature state with pinned requirements. | UI behavior, schema filtering, runtime feature checks. |
| Auth manager | Cached identity and guarded refresh path. | Model clients, backend APIs, unauthorized recovery. |

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
| Misreading | Correction |
|---|---|
| ”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
- Carry provenance with config values. The final value is not enough when a user must debug why a policy rejected it.
- Merge preferences before applying requirements. Requirements should constrain the result, not compete as another peer preference layer.
- Make trust an explicit input. Project config should not silently control the agent that is about to operate inside the project.
- Prefer canonical permission profiles. Legacy sandbox modes are compatibility projections; tooling should reason over the profile when possible.
- 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.