Files
codex/codex-rs/core/src/config/mod.rs
T
jif b9099a145b Honor project agent opt-ins over lower-priority legacy opt-outs (#59476)
## Why

Migrating a project's multi-agent setting to `[agents] enabled = true` should preserve access to agent tools when a lower-priority config still sets `features.multi_agent = false`.

## What changed

Allow an explicit project agent opt-in to override a strictly lower-priority legacy feature setting. Preserve legacy feature settings in the same or higher-priority layer, and continue enforcing managed feature requirements.

## Testing

Add regression coverage for project migration, V1 and model-selected V2 tools, same-layer and CLI opt-outs, and enterprise requirements that disable multi-agent tools.

GitOrigin-RevId: 5810a42d4b7e66cdcbef8dad5281dda4a66291b3
2026-11-04 19:43:00 +00:00

5355 lines
216 KiB
Rust

use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::context::world_state::validate_managed_developer_instructions;
use crate::path_utils::normalize_for_native_workdir;
use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS;
use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use crate::windows_sandbox::resolve_windows_sandbox_mode;
use crate::windows_sandbox::windows_sandbox_level_for_legacy_checks;
use codex_agent_roles::load_agent_roles;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ConfigPathContext;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::ConstrainedWithSource;
use codex_config::FeatureRequirementsToml;
use codex_config::ManagedAuthPolicy;
use codex_config::McpEnterpriseManagedAuthConfig;
use codex_config::McpServerRequirement;
use codex_config::PluginRequirementsToml;
use codex_config::ProfileV2Name;
use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
use codex_config::Sourced;
use codex_config::ThreadConfigLoader;
use codex_config::apply_default_gcp_auth;
use codex_config::config_toml::CircuitBreakAction;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::DEFAULT_PROJECT_DOC_MAX_BYTES;
use codex_config::config_toml::ProjectConfig;
use codex_config::config_toml::RealtimeAudioConfig;
use codex_config::config_toml::RealtimeConfig;
use codex_config::config_toml::ThreadStoreToml;
use codex_config::config_toml::validate_model_providers;
use codex_config::loader::load_config_layers_state;
use codex_config::loader::project_trust_key;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::AuthCredentialsStoreMode;
use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::History;
use codex_config::types::McpServerConfig;
use codex_config::types::MemoriesConfig;
use codex_config::types::ModelAvailabilityNuxConfig;
use codex_config::types::Notice;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_config::types::QuickDial;
use codex_config::types::ResumeCwdMode;
use codex_config::types::SessionPickerViewMode;
use codex_config::types::ToolSuggestConfig;
use codex_config::types::ToolSuggestDisabledTool;
use codex_config::types::ToolSuggestDiscoverable;
use codex_config::types::TuiKeymap;
use codex_config::types::TuiNotificationSettings;
use codex_config::types::TuiPetAnchor;
use codex_config::types::UriBasedFileOpener;
use codex_config::types::WindowsSandboxModeToml;
use codex_core_plugins::PluginLoadOutcome;
use codex_core_plugins::PluginsConfigInput;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_exec_server::ReadFileOptions;
use codex_features::CodeModeConfigToml;
use codex_features::CurrentTimeReminderConfigToml;
use codex_features::CurrentTimeReminderDeliveryMode;
use codex_features::CurrentTimeSource;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
use codex_features::FeatureToml;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_features::NetworkProxyConfigToml;
use codex_features::RuntimeAuthMode;
use codex_features::SleepToolMode;
use codex_features::TokenBudgetConfigToml;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_login::AuthManagerConfig;
use codex_login::AuthRouteConfig;
use codex_mcp::DEFAULT_OPTIONAL_MCP_STARTUP_GRACE;
use codex_mcp::McpConfig;
use codex_mcp::McpPluginAttribution;
use codex_mcp::McpProtocolMode;
use codex_mcp::McpServerRegistration;
use codex_mcp::ResolvedMcpCatalog;
use codex_model_provider::ChatgptBaseUrl;
use codex_model_provider::DEFAULT_CHATGPT_BASE_URL;
use codex_model_provider::ProviderCapabilities;
use codex_model_provider_info::GCP_PROVIDER_ID;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
use codex_model_provider_info::built_in_model_providers;
use codex_model_provider_info::merge_configured_model_providers;
use codex_models_manager::ModelsManagerConfig;
use codex_prompts::ResolvedModelMessages;
use codex_protocol::MemoryVersion;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BaseInstructionsProvenance;
use codex_protocol::models::PermissionProfile;
pub use codex_protocol::models::PermissionProfileSnapshot;
use codex_protocol::models::ProfileWorkspaceRoot;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::DenyReadValidator;
use codex_protocol::permissions::DenyReadViolation;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SandboxPolicy;
use codex_rmcp_client::McpOAuthRefreshMode;
use codex_sandboxing::SandboxType;
pub use codex_thread_store::ExtraConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use http::HeaderValue;
use rmcp::model::ElicitationCapability;
use rmcp::model::FormElicitationCapability;
use rmcp::model::UrlElicitationCapability;
use serde::Deserialize;
use serde::Serialize;
use serde::de::IntoDeserializer;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::ErrorKind;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use crate::config::permissions::BUILT_IN_READ_ONLY_PROFILE;
use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
use crate::config::permissions::default_builtin_permission_profile_name;
use crate::config::permissions::network_proxy_config_for_profile_selection;
use crate::config::permissions::validate_user_permission_profile_names;
use crate::responses_metadata::validate_extra_metadata;
use codex_network_proxy::NetworkProxyConfig;
use toml::Value as TomlValue;
use toml_edit::DocumentMut;
mod auth_keyring;
pub mod edit;
mod guardian_policy;
mod managed_features;
mod metrics;
mod network_config;
mod network_proxy_spec;
mod otel;
mod permission_path;
mod permission_profile_catalog;
mod permission_profile_selection;
mod permissions;
mod requirements;
mod resolved_permission_profile;
pub(crate) use guardian_policy::resolve_guardian_reviewer;
mod runtime_refresh;
pub(crate) use runtime_refresh::RuntimeConfigRefresh;
#[cfg(test)]
mod schema;
mod token_budget_startup;
mod windows_sandbox_config;
pub use auth_keyring::bootstrap_auth_config;
pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind;
pub use codex_agent_roles::AgentRoleConfig;
pub use codex_config::ConfigLoadOptions;
pub use codex_config::Constrained;
pub use codex_config::ConstraintError;
pub use codex_config::ConstraintResult;
pub use codex_config::LoaderOverrides;
pub use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
pub use codex_sandboxing::system_bwrap_warning;
pub use managed_features::ManagedFeatures;
pub(crate) use metrics::emit_session_start_metrics;
pub use network_config::EnvironmentNetworkConfigError;
pub use network_config::NetworkConfigInputs;
pub use network_config::PreparedNetworkConfig;
pub use network_config::project_environment_profile_network;
pub use network_config::validate_environment_network_policy;
pub use network_proxy_spec::NetworkProxySpec;
pub use network_proxy_spec::StartedNetworkProxy;
pub use permission_profile_catalog::PermissionProfileCatalogEntry;
pub use permission_profile_catalog::permission_profile_catalog;
use permission_profile_catalog::permission_profile_catalog_from_permissions;
use permission_profile_catalog::permission_profile_is_allowed;
pub use permission_profile_catalog::validate_permission_profile_for_deny_read;
pub use permission_profile_selection::ResolvedPermissionProfileSelection;
pub use permission_profile_selection::resolve_permission_profile_selection;
pub use permissions::CompiledPermissionProfile;
pub use permissions::WorkspaceWriteSettings;
pub use permissions::compile_permission_profile;
pub(crate) use permissions::is_builtin_permission_profile_name;
pub use permissions::network_proxy_config_from_profile_network;
pub use permissions::resolve_permission_profile;
pub(crate) use resolved_permission_profile::PermissionProfileState;
pub use token_budget_startup::TokenBudgetStartupConfig;
pub use windows_sandbox_config::PreparedWindowsSandboxConfig;
use windows_sandbox_config::config_allows_mxc;
pub use windows_sandbox_config::prepare_windows_sandbox_config;
use windows_sandbox_config::resolve_windows_sandbox_type;
pub use windows_sandbox_config::windows_mxc_allowed_by_config;
const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200;
const DEFAULT_IGNORE_LARGE_UNTRACKED_FILES: i64 = 10 * 1024 * 1024;
const DEFAULT_ASYNC_COMPACTION_THRESHOLD_PERCENT: u8 = 65;
/// Signals that a public config selected the retired `untrusted` approval policy.
#[derive(Debug, thiserror::Error)]
#[error("approval_policy = \"untrusted\" is no longer supported; remove this setting")]
pub struct UnsupportedUntrustedApprovalPolicyError;
/// Compatibility-only config retained so legacy `ghost_snapshot` settings
/// continue to load even though snapshots are no longer produced.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GhostSnapshotConfig {
pub ignore_large_untracked_files: Option<i64>,
pub ignore_large_untracked_dirs: Option<i64>,
pub disable_warnings: bool,
}
impl Default for GhostSnapshotConfig {
fn default() -> Self {
Self {
ignore_large_untracked_files: Some(DEFAULT_IGNORE_LARGE_UNTRACKED_FILES),
ignore_large_untracked_dirs: Some(DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS),
disable_warnings: false,
}
}
}
/// Maximum number of bytes of the documentation that will be embedded. Larger
/// files are *silently truncated* to this size so we do not take up too much of
/// the context window.
pub(crate) const AGENTS_MD_MAX_BYTES: usize = DEFAULT_PROJECT_DOC_MAX_BYTES; // 32 KiB
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usize = 4;
pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000;
pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
pub(crate) const HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS: i64 = 0;
pub(crate) const HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS: i64 =
DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS;
pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1;
const LOCAL_DEV_BUILD_VERSION: &str = "0.0.0";
pub const CONFIG_TOML_FILE: &str = "config.toml";
const CONFIG_PROFILE_V2_SUFFIX: &str = ".config.toml";
fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option<AbsolutePathBuf> {
let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
Some(AbsolutePathBuf::resolve_path_against_base(
trimmed,
resolved_cwd,
))
}
fn resolve_cli_auth_credentials_store_mode(
configured: AuthCredentialsStoreMode,
package_version: &str,
) -> AuthCredentialsStoreMode {
match (package_version, configured) {
(
LOCAL_DEV_BUILD_VERSION,
AuthCredentialsStoreMode::Keyring | AuthCredentialsStoreMode::Auto,
) => AuthCredentialsStoreMode::File,
(_, mode) => mode,
}
}
fn resolve_mcp_oauth_credentials_store_mode(
configured: OAuthCredentialsStoreMode,
package_version: &str,
) -> OAuthCredentialsStoreMode {
match (package_version, configured) {
(
LOCAL_DEV_BUILD_VERSION,
OAuthCredentialsStoreMode::Keyring | OAuthCredentialsStoreMode::Auto,
) => OAuthCredentialsStoreMode::File,
(_, mode) => mode,
}
}
#[cfg(test)]
pub(crate) async fn test_config() -> Config {
let codex_home = tempfile::tempdir().expect("create temp dir");
Config::load_from_base_config_with_overrides(
ConfigToml {
model: Some("gpt-5.5".to_string()),
..Default::default()
},
ConfigOverrides::default(),
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("temp dir should resolve"),
)
.await
.expect("load default test config")
}
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Permissions {
/// Approval policy for executing commands.
pub approval_policy: Constrained<AskForApproval>,
/// Constrained permission profile plus its selected profile identity, if
/// the profile came from a built-in or named config profile.
permission_profile_state: PermissionProfileState,
/// Managed deny-read rules retained independently from user-defined denies.
managed_deny_read_policy: Option<Arc<FileSystemSandboxPolicy>>,
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
/// entries in the permission profile are materialized against these roots.
workspace_roots: Vec<AbsolutePathBuf>,
/// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxySpec>,
/// Whether the model may request a login shell for shell-based tools.
/// Default to `true`
///
/// If `true`, the model may request a login shell (`login = true`), and
/// omitting `login` defaults to using a login shell.
/// If `false`, the model can never use a login shell: `login = true`
/// requests are rejected, and omitting `login` defaults to a non-login
/// shell.
pub allow_login_shell: bool,
/// Policy used to build process environments for shell/unified exec.
pub shell_environment_policy: ShellEnvironmentPolicy,
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
/// legacy feature keys.
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
/// Configured Windows backend; use `Config::effective_local_windows_sandbox_type()` for local selection.
pub windows_sandbox_type: SandboxType,
}
impl Permissions {
/// Build permissions from the constrained values required for a minimal
/// in-process configuration.
pub fn from_approval_and_profile(
approval_policy: Constrained<AskForApproval>,
permission_profile: Constrained<PermissionProfile>,
) -> ConstraintResult<Self> {
Ok(Self {
approval_policy,
permission_profile_state: PermissionProfileState::from_constrained_legacy(
permission_profile,
)?,
managed_deny_read_policy: None,
workspace_roots: Vec::new(),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_type: SandboxType::None,
})
}
pub(crate) fn permission_profile_state(&self) -> &PermissionProfileState {
&self.permission_profile_state
}
pub(crate) fn set_permission_profile_state(
&mut self,
permission_profile_state: PermissionProfileState,
) {
self.permission_profile_state = permission_profile_state;
}
/// Apply a permission profile snapshot emitted by core session state.
///
/// This is a trusted-state bridge for consumers of `SessionConfigured`.
/// Config loading and app-server selection should resolve named profiles
/// through config instead of constructing a snapshot directly.
pub fn set_permission_profile_from_session_snapshot(
&mut self,
snapshot: PermissionProfileSnapshot,
) -> ConstraintResult<()> {
self.permission_profile_state
.set_permission_profile_snapshot(snapshot)
}
/// Replace the current permission constraints with a trusted session
/// snapshot. This is only for clients that must mirror core session state
/// after their local config constraints reject the snapshot.
pub fn replace_permission_profile_from_session_snapshot(
&mut self,
snapshot: PermissionProfileSnapshot,
) -> ConstraintResult<()> {
let permission_profile = Constrained::allow_only(snapshot.permission_profile().clone());
self.permission_profile_state =
PermissionProfileState::from_constrained_snapshot(permission_profile, snapshot)?;
Ok(())
}
/// Borrow the canonical profile before runtime workspace-root
/// materialization has been applied.
pub fn permission_profile(&self) -> &PermissionProfile {
self.permission_profile_state.permission_profile()
}
pub fn can_set_permission_profile(
&self,
permission_profile: &PermissionProfile,
) -> ConstraintResult<()> {
let permission_profile =
self.permission_profile_preserving_managed_denied_reads(permission_profile.clone());
self.permission_profile_state
.can_set_legacy_permission_profile(&permission_profile)
}
pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
self.workspace_roots = workspace_roots;
}
pub fn workspace_roots(&self) -> &[AbsolutePathBuf] {
&self.workspace_roots
}
/// Workspace roots that came from user-visible configuration or runtime
/// selection. Internal Codex-only writable roots are intentionally excluded.
pub fn user_visible_workspace_roots(&self) -> &[AbsolutePathBuf] {
&self.workspace_roots
}
pub fn profile_workspace_roots(&self) -> &[ProfileWorkspaceRoot] {
self.permission_profile_state.profile_workspace_roots()
}
/// Effective runtime permissions after config requirements and runtime
/// workspace-root materialization have been applied.
pub fn effective_permission_profile(&self) -> PermissionProfile {
self.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
}
/// Named profile selected by config, if the current profile has one.
pub fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.permission_profile_state.active_permission_profile()
}
/// Effective filesystem sandbox policy derived from the canonical profile.
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.effective_permission_profile()
.file_system_sandbox_policy()
}
/// Effective network sandbox policy derived from the canonical profile.
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.permission_profile().network_sandbox_policy()
}
/// Legacy compatibility projection derived from the canonical profile.
pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy {
let permission_profile = self.effective_permission_profile();
compatibility_sandbox_policy_for_permission_profile(&permission_profile, cwd)
}
/// Check whether a legacy sandbox policy can be applied to this permission
/// set after projecting it into the canonical permission profile.
pub fn can_set_legacy_sandbox_policy(
&self,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
) -> ConstraintResult<()> {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd);
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
&file_system_sandbox_policy,
network_sandbox_policy,
);
self.can_set_permission_profile(&permission_profile)
}
/// Set permissions from a legacy sandbox policy and keep every permission
/// projection in sync.
pub fn set_legacy_sandbox_policy(
&mut self,
sandbox_policy: SandboxPolicy,
cwd: &Path,
) -> ConstraintResult<()> {
self.can_set_legacy_sandbox_policy(&sandbox_policy, cwd)?;
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
network_sandbox_policy,
);
self.workspace_roots = match &sandbox_policy {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
let mut workspace_roots = vec![
AbsolutePathBuf::from_absolute_path(cwd)
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
];
for root in writable_roots {
if !workspace_roots.iter().any(|existing| existing == root) {
workspace_roots.push(root.clone());
}
}
workspace_roots
}
SandboxPolicy::DangerFullAccess
| SandboxPolicy::ExternalSandbox { .. }
| SandboxPolicy::ReadOnly { .. } => vec![
AbsolutePathBuf::from_absolute_path(cwd)
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
],
};
self.set_permission_profile(permission_profile)
}
/// Set permissions from the canonical profile.
pub fn set_permission_profile(
&mut self,
permission_profile: PermissionProfile,
) -> ConstraintResult<()> {
let permission_profile =
self.permission_profile_preserving_managed_denied_reads(permission_profile);
self.permission_profile_state
.set_legacy_permission_profile(permission_profile)
}
fn permission_profile_preserving_managed_denied_reads(
&self,
permission_profile: PermissionProfile,
) -> PermissionProfile {
let Some(managed_deny_read_policy) = self.managed_deny_read_policy.as_ref() else {
return permission_profile;
};
if matches!(
sandbox_mode_requirement_for_permission_profile(&permission_profile),
SandboxModeRequirement::DangerFullAccess | SandboxModeRequirement::ExternalSandbox
) {
return permission_profile;
}
let enforcement = permission_profile.enforcement();
let (mut file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
file_system_policy.preserve_deny_read_restrictions_from(managed_deny_read_policy);
PermissionProfile::from_runtime_permissions_with_enforcement(
enforcement,
&file_system_policy,
network_policy,
)
}
}
// A profile override only inherits the selected profile's proxy/allowlist config
// when Codex is still responsible for the network policy. `Disabled` means no
// outer sandbox, so starting the managed proxy would narrow the override.
fn profile_allows_configured_network_proxy(permission_profile: &PermissionProfile) -> bool {
match permission_profile {
PermissionProfile::Managed { network, .. } | PermissionProfile::External { network } => {
network.is_enabled()
}
PermissionProfile::Disabled => false,
}
}
fn build_network_proxy_spec(
mut configured_network_proxy_config: NetworkProxyConfig,
network_requirements: Option<Sourced<codex_config::NetworkConstraints>>,
permission_profile: &PermissionProfile,
environment_overrides: &HashMap<String, String>,
features: &ManagedFeatures,
feature_settings: Option<&FeaturesToml>,
) -> std::io::Result<Option<NetworkProxySpec>> {
if features.enabled(Feature::CredentialMasking) {
configured_network_proxy_config.set_credential_broker_enabled(/*enabled*/ true);
if let Some(credentials) = network_proxy_toml_config(feature_settings)
.and_then(|network_proxy| network_proxy.credentials.as_ref())
{
configured_network_proxy_config
.credential_providers
.clone_from(credentials);
}
}
configured_network_proxy_config.configure_credential_broker_environment(environment_overrides);
PreparedNetworkConfig {
configured_proxy: configured_network_proxy_config,
}
.build(network_requirements, permission_profile)
}
/// Configured thread persistence backend.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ThreadStoreConfig {
/// Persist threads locally using rollout JSONL files and sqlite metadata.
#[default]
Local,
/// In-memory thread store for test and debug configurations.
InMemory { id: String },
}
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
/// App-server-owned destination policy; other runtimes remain unmanaged.
pub application_network_policy: codex_http_client::NetworkPolicy,
/// Auth bootstrap routing installed by the app-server configuration owner.
pub application_auth_route_config: Option<AuthRouteConfig>,
/// Provenance for how this [`Config`] was derived (merged layers + enforced
/// requirements).
pub config_layer_stack: ConfigLayerStack,
/// Plugin settings resolved with this snapshot, before capability admission.
pub plugins: codex_config::types::PluginsConfigToml,
/// Warnings collected during config load that should be shown on startup.
pub startup_warnings: Vec<String>,
/// Optional override of model selection.
pub model: Option<String>,
/// Default Daybreak preference for new threads and non-interactive turns.
pub daybreak_enabled: bool,
/// Effective service tier request id preference for new turns.
/// `default` means the user explicitly selected standard routing.
pub service_tier: Option<String>,
/// Model used specifically for review sessions.
pub review_model: Option<String>,
/// Size of the context window for the model, in tokens.
pub model_context_window: Option<i64>,
/// Token usage threshold triggering auto-compaction of conversation history.
pub model_auto_compact_token_limit: Option<i64>,
/// Controls whether `model_auto_compact_token_limit` applies to the full
/// active context or only tokens after the carried compaction-window prefix.
pub model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope,
/// Percentage of the usable context window that triggers turn-end compaction.
/// Zero disables turn-end compaction.
pub model_post_turn_compact_threshold_percent: u8,
/// Percentage of the model's full context window at which background compaction starts.
pub async_compaction_threshold_percent: u8,
/// Key into the model_providers map that specifies which provider to use.
pub model_provider_id: String,
/// Info needed to make an API request to the model.
pub model_provider: ModelProviderInfo,
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
/// Effective permission configuration for shell tool execution.
pub permissions: Permissions,
/// Whether config explicitly selected named permissions profiles instead
/// of the legacy `sandbox_mode` syntax.
pub explicit_permission_profile_mode: bool,
/// User-defined permission profiles available from effective config.
pub custom_permission_profiles: Vec<PermissionProfileCatalogEntry>,
/// Configures who approval requests are routed to for review once they have
/// been escalated. This does not disable separate safety checks such as
/// ARC.
pub approvals_reviewer: ApprovalsReviewer,
/// Requested reviewer when managed requirements replace the effective choice.
/// `None` uses `approvals_reviewer`, including programmatically constructed configs.
pub requested_approvals_reviewer: Option<ApprovalsReviewer>,
/// enforce_residency means web traffic cannot be routed outside of a
/// particular geography. HTTP clients should direct their requests
/// using backend-specific headers or URLs to enforce this.
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,
/// When `true`, `AgentReasoning` events emitted by the backend will be
/// suppressed from the frontend output. This can reduce visual noise when
/// users are only interested in the final agent responses.
pub hide_agent_reasoning: bool,
/// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
/// Defaults to `false`.
pub show_raw_agent_reasoning: bool,
/// Base instructions override.
pub base_instructions: Option<String>,
/// Origin of the configured base instructions when supplied by another session or lockfile.
pub base_instructions_provenance: Option<BaseInstructionsProvenance>,
/// Developer instructions override injected as a separate message.
pub developer_instructions: Option<String>,
/// Guardian-specific policy config override from requirements.toml or config.toml.
/// This is inserted into the fixed guardian prompt template under the
/// `# Policy Configuration` section rather than replacing the whole
/// guardian developer prompt.
pub guardian_policy_config: Option<String>,
/// Additional Guardian policy from requirements.toml or config.toml.
/// Rendered into `{{ extra_policy }}` alongside the resolved tenant policy.
pub guardian_extra_policy: Option<String>,
/// Guardian prompt template override from config.toml.
/// The resolved policy config replaces its `{{ tenant_policy_config }}`
/// placeholder when a review session is built.
pub guardian_policy_template: Option<String>,
/// Transcript encoding shared by Guardian review and scoring.
pub guardian_transcript_mode: codex_protocol::TranscriptFormat,
/// Frozen section policy for sync Guardian; absence keeps fixed budgeting.
pub guardian_sync_elastic_budget: Option<codex_guardian_context::ElasticBudgetConfig>,
/// Optional replacement for the gated history-retrieval instructions.
/// Blank config values are treated as unset, like other Guardian policy overrides.
pub guardian_conversation_history_prompt: Option<String>,
/// Optional bounded developer message for the gated denial reconsideration pass.
pub guardian_denial_reconsideration_prompt: Option<String>,
/// Optional per-response history-tool budget. Guardian defaults to 4,000 tokens and
/// preserves stricter parent tool limits.
pub guardian_conversation_history_max_output_tokens: Option<NonZeroUsize>,
/// Include a structured error when Guardian's circuit breaker interrupts a turn.
pub guardian_circuit_break_action: CircuitBreakAction,
/// Overall Guardian review budget. None means unlimited; cancellation still applies.
pub guardian_review_timeout: Option<Duration>,
/// Whether to inject the `<permissions instructions>` developer block.
pub include_permissions_instructions: bool,
/// Whether to inject the `<apps_instructions>` developer block.
pub include_apps_instructions: bool,
/// Whether to inject the `<collaboration_mode>` developer block.
pub include_collaboration_mode_instructions: bool,
/// Whether to inject the `<skills_instructions>` developer block.
pub include_skill_instructions: bool,
/// Optional token budget override for the available-skills catalog.
pub skill_max_context_tokens: Option<NonZeroUsize>,
/// Whether cloud skills are discovered and exposed to the model.
pub cloud_skill_enabled: bool,
/// Whether orchestrator-owned MCP tools are exposed to the model.
pub orchestrator_mcp_enabled: bool,
/// Whether to inject the `<environment_context>` user block.
pub include_environment_context: bool,
/// Whether environment context includes the current date and timezone.
pub include_environment_context_time: bool,
/// Compact prompt override.
pub compact_prompt: Option<String>,
/// Optional external notifier command. When set, Codex will spawn this
/// program after each completed *turn* (i.e. when the agent finishes
/// processing a user submission). The value must be the full command
/// broken into argv tokens **without** the trailing JSON argument - Codex
/// appends one extra argument containing a JSON payload describing the
/// event.
///
/// Example `~/.codex/config.toml` snippet:
///
/// ```toml
/// notify = ["notify-send", "Codex"]
/// ```
///
/// which will be invoked as:
///
/// ```shell
/// notify-send Codex '{"type":"agent-turn-complete","turn-id":"12345"}'
/// ```
///
/// If unset the feature is disabled.
pub notify: Option<Vec<String>>,
/// TUI notification settings, including enabled events, delivery method, and focus condition.
pub tui_notifications: TuiNotificationSettings,
/// Enable ASCII animations and shimmer effects in the TUI.
pub animations: bool,
/// Individual TUI effects, subordinate to the animation master switch.
pub tui_effects: codex_config::types::TuiEffects,
/// Rich content rendering preferences, independent of animations.
pub tui_rendering: codex_config::types::TuiRendering,
/// Show startup tooltips in the TUI welcome screen.
pub show_tooltips: bool,
/// Show a TUI notice when the connected app server is an older stable release.
pub tui_show_server_version_notice: bool,
/// Generate automatic TUI recaps. Manual `/recap` remains available when disabled.
pub tui_auto_recap: bool,
/// Persisted startup availability NUX state for model tooltips.
pub model_availability_nux: ModelAvailabilityNuxConfig,
/// Start the composer in Vim mode (`Normal`) by default.
pub tui_vim_mode_default: bool,
pub tui_question_esc_back: bool,
/// Start the TUI in raw scrollback mode for copy-friendly transcript output.
pub tui_raw_output_mode: bool,
/// Own the fullscreen transcript when the alternate screen is enabled.
pub tui_fullscreen_transcript: bool,
/// Mouse wheel speed multiplier for transcript scrolling; defaults to one row per event.
pub tui_mouse_scroll_speed: Option<f64>,
/// Override the terminal default for copying transcript, footer, and composer selections.
pub tui_copy_on_select: codex_config::types::CopyOnSelect,
/// Whether to deselect read-only transcript or footer text after confirmed automatic copy.
pub tui_copy_on_select_clear_selection: bool,
/// Right-click text paste fallback for the fullscreen TUI.
pub tui_right_click_paste: codex_config::types::RightClickPaste,
/// Start the TUI in the specified collaboration mode (plan/default).
/// Controls whether the TUI uses the terminal's alternate screen buffer.
///
/// This is the same `tui.alternate_screen` value from `config.toml`.
/// - `auto` (default): Use alternate screen.
/// - `always`: Always use alternate screen.
/// - `never`: Never use alternate screen (inline mode, preserves scrollback).
pub tui_alternate_screen: AltScreenMode,
/// Ordered list of status line item identifiers for the TUI.
///
/// When unset, the TUI defaults to: `model-with-reasoning` and `current-dir`.
pub tui_status_line: Option<Vec<String>>,
/// Whether to color status line items with colors from the active syntax theme.
pub tui_status_line_use_colors: bool,
/// Ordered list of terminal title item identifiers for the TUI.
///
/// When unset, the TUI defaults to: `activity` and `project`.
/// The `activity` item spins while working and shows an action-required
/// message when blocked on the user.
pub tui_terminal_title: Option<Vec<String>>,
/// Syntax highlighting theme override (kebab-case name).
pub tui_theme: Option<String>,
/// Pet id preselected by the terminal pet picker.
pub tui_pet: Option<String>,
/// Vertical anchor used by terminal pet rendering.
pub tui_pet_anchor: TuiPetAnchor,
/// Preferred layout for resume/fork session picker results.
pub tui_session_picker_view: SessionPickerViewMode,
/// Last selected grouping in Agent Command Center.
pub tui_agents_overview_grouping: codex_config::types::AgentsOverviewGrouping,
/// Working directory to use when resuming or forking a session.
/// When unset, prompt if the current and session directories differ.
pub tui_resume_cwd: Option<ResumeCwdMode>,
/// Terminal resize-reflow tuning knobs.
pub terminal_resize_reflow: TerminalResizeReflowConfig,
/// Keybinding overrides for the TUI.
///
/// Precedence is:
///
/// 1. context table (`tui.keymap.chat`, `tui.keymap.composer`, etc.)
/// 2. `tui.keymap.global`
/// 3. built-in defaults
pub tui_keymap: TuiKeymap,
/// Saved TUI inputs keyed by a single digit (`0` through `9`).
pub tui_quick_dials: BTreeMap<String, QuickDial>,
/// The absolute directory that should be treated as the current working
/// directory for the session. All relative paths inside the business-logic
/// layer are resolved against this path.
pub cwd: AbsolutePathBuf,
/// Absolute runtime workspace roots for the session. Symbolic
/// `:workspace_roots` permission entries are materialized against these
/// roots while profile-defined workspace roots remain encoded directly in
/// the permission profile.
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Whether runtime workspace roots were supplied explicitly by the caller
/// or legacy config, rather than defaulting to `cwd`.
pub workspace_roots_explicit: bool,
/// Preferred store for CLI auth credentials.
/// file (default): Use a file in the Codex home directory.
/// keyring: Use an OS-specific keyring service.
/// auto: Use the OS-specific keyring service if available, otherwise use a file.
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
/// Definition for MCP servers that Codex can reach out to for tool calls.
pub mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
/// Trusted IdP shared by all permitted EMA MCP registrations.
pub mcp_enterprise_managed_auth: Option<McpEnterpriseManagedAuthConfig>,
/// When present, only these MCP servers omit the legacy `mcp__` namespace prefix.
pub non_prefixed_mcp_tool_servers: Option<Vec<String>>,
/// Preferred store for MCP OAuth credentials.
/// keyring: Use an OS-specific keyring service.
/// Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access.
/// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2
/// file: CODEX_HOME/.credentials.json
/// This file will be readable to Codex and other applications running as the same user.
/// auto (default): keyring if available, otherwise file.
pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode,
/// Optional fixed port to use for the local HTTP callback server used during MCP OAuth login.
///
/// When unset, Codex will bind to an ephemeral port chosen by the OS.
pub mcp_oauth_callback_port: Option<u16>,
/// Optional redirect URI to use during MCP OAuth login.
///
/// When set, this URI is used in the OAuth authorization request instead
/// of the local listener address. The local callback listener still binds
/// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided).
pub mcp_oauth_callback_url: Option<String>,
/// How long to wait for optional MCP servers while building the initial tool catalog.
pub mcp_optional_startup_grace: Duration,
/// Combined provider map (defaults plus user-defined providers).
pub model_providers: HashMap<String, ModelProviderInfo>,
/// Maximum total bytes of project instruction content across all selected environments.
pub project_doc_max_bytes: usize,
/// Additional filenames to try when looking for project-level docs.
pub project_doc_fallback_filenames: Vec<String>,
/// Token budget applied when storing tool/function outputs in the context manager.
pub tool_output_token_limit: Option<usize>,
/// Whether multi-agent tools are enabled through `[agents]`.
pub agents_enabled: bool,
/// User-configured maximum number of spawned agent threads per session.
pub agent_max_threads: Option<usize>,
/// Default model for spawned subagents when the spawn call does not select one.
pub agent_default_subagent_model: Option<String>,
/// Default reasoning effort for spawned subagents when the spawn call does not select one.
pub agent_default_subagent_reasoning_effort: Option<ReasoningEffort>,
/// Whether to record a model-visible message when an agent turn is interrupted.
pub agent_interrupt_message_enabled: bool,
/// Maximum nesting depth for V1 agent threads. Ignored by V2.
pub agent_max_depth: i32,
/// User-defined role declarations keyed by role name.
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
/// Maximum token budget allowed for a goal and default budget for new goals.
pub max_goal_token_budget: Option<i64>,
/// Memories subsystem settings.
pub memories: MemoriesConfig,
/// Directory containing all Codex state (defaults to `~/.codex` but can be
/// overridden by the `CODEX_HOME` environment variable).
pub codex_home: AbsolutePathBuf,
/// Resolved configuration shared by all Codex SQLite databases.
pub sqlite: codex_state::SqliteConfig,
/// Directory for log files (defaults to `$CODEX_HOME/log`).
/// SQLite logs use this directory only when `log_dir` is explicitly configured
/// and no managed `sqlite_home` requirement applies.
pub log_dir: PathBuf,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
pub history: History,
/// When true, session is not persisted on disk. Default to `false`
pub ephemeral: bool,
/// Enable persistent dynamic tool updates only when every reader, including rollback
/// versions, can restore the updated tools and preserve them through compaction.
pub allow_persistent_dynamic_tool_updates: bool,
/// Optional extra configuration fields for the thread.
pub extra_config: Option<ExtraConfig>,
/// Whether enabled hooks should run without requiring persisted hook trust for this session.
///
/// This is a runtime-only knob populated from invocation overrides, not from config files.
pub bypass_hook_trust: bool,
/// Optional URI-based file opener. If set, citations to files in the model
/// output will be hyperlinked using the specified URI scheme.
pub file_opener: UriBasedFileOpener,
/// Path to the current Codex executable. This cannot be set in the config
/// file: it must be set in code via [`ConfigOverrides`].
pub codex_self_exe: Option<PathBuf>,
/// Path to the `codex-linux-sandbox` executable. This must be set if
/// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this
/// cannot be set in the config file: it must be set in code via
/// [`ConfigOverrides`].
///
/// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
pub codex_linux_sandbox_exe: Option<PathBuf>,
/// Ignored compatibility field for clients that still pass the retired wrapper path.
pub main_execve_wrapper_exe: Option<PathBuf>,
/// Ignored compatibility field for clients that still pass a patched zsh path.
pub zsh_path: Option<PathBuf>,
/// Value to use for `reasoning.effort` when making a request using the
/// Responses API.
pub model_reasoning_effort: Option<ReasoningEffort>,
/// Optional Plan-mode-specific reasoning effort override used by the TUI.
///
/// When unset, Plan mode uses the built-in Plan preset default (currently
/// `medium`). When explicitly set (including `none`), this overrides the
/// Plan preset. The `none` value means "no reasoning" (not "inherit the
/// global default").
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
/// Optional value to use for `reasoning.summary` when making a request
/// using the Responses API. When unset, the model catalog default is used.
pub model_reasoning_summary: Option<ReasoningSummary>,
/// Optional full model catalog loaded from `model_catalog_json`.
/// When set, this replaces the bundled catalog for the current process.
pub model_catalog: Option<ModelsResponse>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,
/// Whether configuration or managed requirements explicitly selected the ChatGPT base URL.
pub chatgpt_base_url_is_explicit: bool,
/// Whether Codex-owned clients should respect host system proxy settings.
pub respect_system_proxy: bool,
/// Optional product SKU forwarded to the host-owned apps MCP server.
pub apps_mcp_product_sku: Option<String>,
/// Bounded, product-owned metadata attached to every Responses API request.
pub responses_api_metadata: BTreeMap<String, String>,
/// Machine-local realtime audio device preferences used by realtime voice.
pub realtime_audio: RealtimeAudioConfig,
/// Experimental / do not use. Overrides only the realtime conversation
/// websocket transport base URL (the `Op::RealtimeConversation`
/// `/v1/realtime`
/// connection) without changing normal provider HTTP requests.
pub experimental_realtime_ws_base_url: Option<String>,
/// Experimental / do not use. Overrides only the WebRTC realtime call
/// creation base URL.
pub experimental_realtime_webrtc_call_base_url: Option<String>,
/// Experimental / do not use. Selects the realtime websocket model/snapshot
/// used for the `Op::RealtimeConversation` connection.
pub experimental_realtime_ws_model: Option<String>,
/// Experimental / do not use. Realtime websocket session selection.
/// `version` controls v1/v2 and `type` controls conversational/transcription.
pub realtime: RealtimeConfig,
/// Experimental / do not use. Overrides only the realtime conversation
/// websocket transport instructions (the `Op::RealtimeConversation`
/// `/ws` session.update instructions) without changing normal prompts.
pub experimental_realtime_ws_backend_prompt: Option<String>,
/// Experimental / do not use. Replaces the synthesized realtime startup
/// context appended to websocket session instructions. An empty string
/// disables startup context injection entirely.
pub experimental_realtime_ws_startup_context: Option<String>,
/// Experimental / do not use. Replaces the built-in realtime start
/// instructions inserted into developer messages when realtime becomes
/// active.
pub experimental_realtime_start_instructions: Option<String>,
/// Experimental / do not use. Selects the thread persistence backend.
pub experimental_thread_store: ThreadStoreConfig,
/// When set, restricts ChatGPT login to one or more workspace identifiers.
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
/// When set, restricts the login mechanism users may use.
pub forced_login_method: Option<ForcedLoginMethod>,
/// Explicit or feature-derived web search mode.
pub web_search_mode: Constrained<WebSearchMode>,
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// Whether to register the experimental request_user_input tool.
pub experimental_request_user_input_enabled: bool,
/// Whether to register the update_plan tool.
pub update_plan_enabled: bool,
/// Policy for collecting and validating tool runtimes.
pub tool_registry: ToolRegistryConfig,
/// Configuration for the experimental code-mode tool surface.
pub code_mode: CodeModeConfig,
/// Maximum poll window for background terminal output (`write_stdin`), in milliseconds.
/// Default: `300000` (5 minutes).
pub background_terminal_max_timeout: u64,
/// Idle timeout for unsubscribed app-server threads.
pub thread_unload_delay: Duration,
/// Compatibility-only settings retained for legacy `ghost_snapshot`
/// config loading.
pub ghost_snapshot: GhostSnapshotConfig,
/// Settings specific to the task-path-based multi-agent tool surface.
pub multi_agent_v2: MultiAgentV2Config,
/// Context-window token budget configuration, when enabled.
pub token_budget: Option<TokenBudgetConfig>,
/// Runtime snapshot of configured token-budget preferences before startup activation.
pub token_budget_startup_config: Option<TokenBudgetStartupConfig>,
/// Shared token budget for the root thread and its sub-agents.
pub rollout_budget: Option<RolloutBudgetConfig>,
/// Current-time reminder and clock tool configuration, when enabled.
pub current_time_reminder: Option<CurrentTimeReminderConfig>,
/// How the sleep tool is selected when its feature gate is enabled.
pub sleep_tool_mode: SleepToolMode,
/// Authentication mode used when runtime agent instances are enabled.
pub runtime_auth_mode: RuntimeAuthMode,
/// Centralized feature flags; source of truth for feature gating.
pub features: ManagedFeatures,
/// Local rollout preference after checking network restrictions and native availability.
pub prefer_mxc: bool,
/// Host feature defaults retained beneath explicit configuration overrides.
pub runtime_feature_defaults: BTreeMap<Feature, bool>,
/// When `true`, suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: bool,
/// The currently active project config, resolved by checking if cwd:
/// is (1) part of a git repo, (2) a git worktree, or (3) just using the cwd
pub active_project: ProjectConfig,
/// Collection of various notices we show the user
pub notices: Notice,
/// When `true`, checks for Codex updates on startup and surfaces update prompts.
/// Set to `false` only if your Codex updates are centrally managed.
/// Defaults to `true`.
pub check_for_update_on_startup: bool,
/// When true, disables burst-paste detection for typed input entirely.
/// All characters are inserted as they are received, and no buffering
/// or placeholder replacement will occur for fast keypress bursts.
pub disable_paste_burst: bool,
/// When `false`, disables OpenAI analytics across Codex product surfaces on this machine.
/// Custom OTLP metrics exporters are controlled by `otel.metrics_exporter`.
/// Voluntarily left as Optional because the default value might depend on the client.
pub analytics_enabled: Option<bool>,
/// When `false`, disables feedback uploads and background Guardian review retention. Explicit local exports remain available.
/// Defaults to `true`.
pub feedback_enabled: bool,
/// Configured discoverable tools for tool suggestions.
pub tool_suggest: ToolSuggestConfig,
/// OTEL configuration (exporter type, endpoint, headers, etc.).
pub otel: codex_config::types::OtelConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ToolRegistryConfig {
/// Fail the turn when multiple tools share the same effective name.
pub error_on_tool_collisions: bool,
/// Include authoritative tool information in per-turn request metadata.
pub turn_metadata_includes_tool_info: bool,
}
const DEFAULT_CODE_MODE_EXEC_YIELD_TIME_MS: u64 = 30_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CodeModeConfig {
pub default_exec_yield_time_ms: u64,
/// Show handler duration, code-mode host duration, and harness overhead
/// in each code-mode cell response.
/// Experimental: this option and the response format may change or be removed.
pub experimental_show_cell_overhead: bool,
pub tool_input_schema_max_bytes: Option<usize>,
pub excluded_tool_namespaces: Vec<String>,
pub direct_only_tool_namespaces: Vec<String>,
/// Keep code mode fail-closed when the standalone host is unavailable.
pub disable_in_process_fallback: bool,
}
impl Default for CodeModeConfig {
fn default() -> Self {
Self {
default_exec_yield_time_ms: DEFAULT_CODE_MODE_EXEC_YIELD_TIME_MS,
experimental_show_cell_overhead: false,
tool_input_schema_max_bytes: None,
excluded_tool_namespaces: Vec::new(),
direct_only_tool_namespaces: Vec::new(),
disable_in_process_fallback: false,
}
}
}
const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 2000;
const TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES: usize = 2000;
const AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES: usize = 2000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TokenBudgetConfig {
pub use_history_notes_extension: bool,
pub reminder_threshold_tokens: Option<i64>,
pub reminder_message_template: String,
pub guidance_message: Option<String>,
pub auto_compact_fallback_prompt: Option<String>,
pub auto_compact_fallback_buffer_tokens: Option<i64>,
}
impl TokenBudgetConfig {
pub(crate) fn validate(&self) -> std::io::Result<()> {
if self
.reminder_threshold_tokens
.is_some_and(|tokens| tokens <= 0)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.reminder_threshold_tokens must be positive",
));
}
if self.reminder_message_template.trim().is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.reminder_message_template must not be empty",
));
}
if self.reminder_message_template.len() > TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"features.token_budget.reminder_message_template must not exceed {TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES} bytes"
),
));
}
if self
.guidance_message
.as_ref()
.is_some_and(|message| message.len() > TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"features.token_budget.guidance_message must not exceed {TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES} bytes"
),
));
}
if self
.auto_compact_fallback_prompt
.as_ref()
.is_some_and(|prompt| prompt.len() > AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"features.token_budget.auto_compact_fallback_prompt must not exceed {AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES} bytes"
),
));
}
if self.auto_compact_fallback_prompt.is_some()
&& self.auto_compact_fallback_buffer_tokens.is_none()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.auto_compact_fallback_buffer_tokens is required when auto_compact_fallback_prompt is set",
));
}
if self
.auto_compact_fallback_buffer_tokens
.is_some_and(|tokens| tokens <= 0)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.token_budget.auto_compact_fallback_buffer_tokens must be positive",
));
}
Ok(())
}
pub(crate) fn fallback_buffer_tokens(&self) -> i64 {
if self.auto_compact_fallback_prompt.is_some() {
self.auto_compact_fallback_buffer_tokens.unwrap_or(0)
} else {
0
}
}
}
impl Default for TokenBudgetConfig {
fn default() -> Self {
Self {
use_history_notes_extension: false,
reminder_threshold_tokens: None,
reminder_message_template: ResolvedModelMessages::bundled()
.token_budget_reminder_template()
.to_owned(),
guidance_message: None,
auto_compact_fallback_prompt: None,
auto_compact_fallback_buffer_tokens: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RolloutBudgetConfig {
pub enforce_limit: bool,
pub limit_tokens: i64,
pub reminder_at_remaining_tokens: Vec<i64>,
pub sampling_token_weight: f64,
pub prefill_token_weight: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct CurrentTimeReminderConfig {
pub reminder_interval_seconds: u64,
pub clock_source: CurrentTimeSource,
pub delivery_mode: CurrentTimeReminderDeliveryMode,
/// Whether to expose the input-interruptible `clock.sleep` tool.
pub sleep_tool: bool,
}
impl Default for CurrentTimeReminderConfig {
fn default() -> Self {
Self {
reminder_interval_seconds: 1,
clock_source: CurrentTimeSource::System,
delivery_mode: CurrentTimeReminderDeliveryMode::AnyInference,
sleep_tool: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MultiAgentV2Config {
pub max_concurrent_threads_per_session: usize,
pub min_wait_timeout_ms: i64,
pub max_wait_timeout_ms: i64,
pub default_wait_timeout_ms: i64,
pub usage_hint_text: Option<String>,
pub root_agent_usage_hint_text: Option<String>,
pub subagent_usage_hint_text: Option<String>,
pub subagent_developer_instructions: Option<String>,
pub preserve_fork_prefix: bool,
pub multi_agent_mode_hint_text: Option<String>,
pub tool_namespace: Option<String>,
pub hide_spawn_agent_metadata: bool,
pub expose_spawn_agent_model_overrides: bool,
pub wait_agent_enabled: bool,
pub disable_native_tools: bool,
pub disable_direct_message: bool,
pub message_board_in_memory: bool,
pub message_board_remote: Option<codex_features::RemoteMessageBoardConfigToml>,
pub non_code_mode_only: bool,
}
impl MultiAgentV2Config {
fn defaults_for_max_concurrency(max_concurrent_threads_per_session: usize) -> Self {
Self {
max_concurrent_threads_per_session,
min_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS,
max_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS,
default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS,
usage_hint_text: None,
root_agent_usage_hint_text: None,
subagent_usage_hint_text: None,
subagent_developer_instructions: None,
preserve_fork_prefix: true,
multi_agent_mode_hint_text: None,
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
hide_spawn_agent_metadata: true,
expose_spawn_agent_model_overrides: true,
wait_agent_enabled: true,
disable_native_tools: false,
disable_direct_message: false,
message_board_in_memory: false,
message_board_remote: None,
non_code_mode_only: true,
}
}
}
impl Default for MultiAgentV2Config {
fn default() -> Self {
Self::defaults_for_max_concurrency(
DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TerminalResizeReflowMaxRows {
/// Use the runtime terminal detector to choose a scrollback-sized cap.
#[default]
Auto,
/// Keep all rendered transcript rows during resize reflow.
Disabled,
/// Keep at most this many rendered transcript rows during resize reflow.
Limit(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TerminalResizeReflowConfig {
pub max_rows: TerminalResizeReflowMaxRows,
}
impl AuthManagerConfig for Config {
fn codex_home(&self) -> PathBuf {
self.codex_home.to_path_buf()
}
fn cli_auth_credentials_store_mode(&self) -> AuthCredentialsStoreMode {
self.cli_auth_credentials_store_mode
}
fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind {
Config::auth_keyring_backend_kind(self)
}
fn forced_login_method(&self) -> Option<ForcedLoginMethod> {
self.forced_login_method
}
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
self.forced_chatgpt_workspace_id.clone()
}
fn managed_auth_policy(&self) -> ManagedAuthPolicy {
self.config_layer_stack.requirements().managed_auth_policy()
}
fn chatgpt_base_url(&self) -> String {
self.chatgpt_base_url.clone()
}
fn auth_route_config(&self) -> AuthRouteConfig {
Config::auth_route_config(self)
}
}
#[derive(Clone, Default)]
pub struct ConfigBuilder {
codex_home: Option<PathBuf>,
cli_overrides: Option<Vec<(String, TomlValue)>>,
harness_overrides: Option<ConfigOverrides>,
loader_overrides: Option<LoaderOverrides>,
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
fallback_cwd: Option<PathBuf>,
without_project_context: bool,
}
impl ConfigBuilder {
pub fn codex_home(mut self, codex_home: PathBuf) -> Self {
self.codex_home = Some(codex_home);
self
}
pub fn cli_overrides(mut self, cli_overrides: Vec<(String, TomlValue)>) -> Self {
self.cli_overrides = Some(cli_overrides);
self
}
pub fn harness_overrides(mut self, harness_overrides: ConfigOverrides) -> Self {
self.harness_overrides = Some(harness_overrides);
self
}
pub fn loader_overrides(mut self, loader_overrides: LoaderOverrides) -> Self {
self.loader_overrides = Some(loader_overrides);
self
}
pub fn strict_config(mut self, strict_config: bool) -> Self {
self.strict_config = strict_config;
self
}
pub fn cloud_config_bundle(mut self, cloud_config_bundle: CloudConfigBundleLoader) -> Self {
self.cloud_config_bundle = cloud_config_bundle;
self
}
pub fn thread_config_loader(
mut self,
thread_config_loader: Arc<dyn ThreadConfigLoader>,
) -> Self {
self.thread_config_loader = Some(thread_config_loader);
self
}
pub fn fallback_cwd(mut self, fallback_cwd: Option<PathBuf>) -> Self {
self.fallback_cwd = fallback_cwd;
self
}
/// Materializes `Config.cwd` without using it as config-layer context.
pub fn without_project_context(mut self) -> Self {
self.without_project_context = true;
self
}
pub async fn build(self) -> std::io::Result<Config> {
// Keep the large config-loading future off small runtime thread stacks.
// Avoid an additional async frame for ordinary config loads.
Box::pin(self.build_inner(|_, _, _| Ok(()))).await
}
/// Validates the same loaded configuration that will be materialized, before provider
/// selection or initialization. Validation cannot alter the loaded configuration.
pub async fn build_with_validation(
self,
validate: impl FnOnce(&ConfigToml, &ConfigOverrides, &ConfigLayerStack) -> std::io::Result<()>,
) -> std::io::Result<Config> {
Box::pin(self.build_inner(validate)).await
}
async fn build_inner(
self,
validate: impl FnOnce(&ConfigToml, &ConfigOverrides, &ConfigLayerStack) -> std::io::Result<()>,
) -> std::io::Result<Config> {
let Self {
codex_home,
cli_overrides,
harness_overrides,
loader_overrides,
strict_config,
cloud_config_bundle,
thread_config_loader,
fallback_cwd,
without_project_context,
} = self;
let codex_home = match codex_home {
Some(codex_home) => AbsolutePathBuf::from_absolute_path(codex_home)?,
None => find_codex_home()?,
};
let cli_overrides = cli_overrides.unwrap_or_default();
let mut harness_overrides = harness_overrides.unwrap_or_default();
let loader_overrides = loader_overrides.unwrap_or_default();
let cwd_override = harness_overrides.cwd.as_deref().or(fallback_cwd.as_deref());
let cwd = match cwd_override {
Some(path) => AbsolutePathBuf::relative_to_current_dir(path)?,
None => AbsolutePathBuf::current_dir()?,
};
harness_overrides.cwd = Some(cwd.to_path_buf());
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
(!without_project_context).then_some(cwd),
&cli_overrides,
ConfigLoadOptions {
loader_overrides,
strict_config,
cloud_config_bundle,
},
thread_config_loader
.as_deref()
.unwrap_or(&codex_config::NoopThreadConfigLoader),
)
.await?;
let config_toml = config_toml_from_layers(&config_layer_stack).await?;
validate(&config_toml, &harness_overrides, &config_layer_stack)?;
Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
harness_overrides,
codex_home,
config_layer_stack,
)
.await
}
#[cfg(test)]
pub(crate) fn without_managed_config_for_tests() -> Self {
Self::default().loader_overrides(LoaderOverrides::without_managed_config_for_tests())
}
}
async fn config_toml_from_layers(layers: &ConfigLayerStack) -> std::io::Result<ConfigToml> {
// The loader resolves paths relative to each layer's file before deserialization.
match ConfigToml::try_from(layers.effective_config()) {
Ok(config_toml) => Ok(config_toml),
Err(err) => {
if let Some(config_error) = codex_config::first_layer_config_error::<ConfigToml>(
layers,
codex_config::CONFIG_TOML_FILE,
)
.await
{
return Err(codex_config::io_error_from_config_error(
std::io::ErrorKind::InvalidData,
config_error,
Some(err),
));
}
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err))
}
}
}
impl Config {
/// Capture storage and sync preparation use one session-static rollout choice.
pub(crate) fn guardian_context_budgeting(&self) -> codex_guardian_context::ContextBudgeting {
if self.guardian_sync_elastic_budget.is_some() {
codex_guardian_context::ContextBudgeting::Elastic
} else {
codex_guardian_context::ContextBudgeting::Fixed
}
}
pub fn sqlite_config(&self) -> &codex_state::SqliteConfig {
&self.sqlite
}
/// Returns `log_dir` when explicitly set by configuration or managed requirements.
pub fn configured_log_dir(&self) -> Option<&Path> {
let configured = self
.config_layer_stack
.effective_config()
.as_table()
.is_some_and(|table| table.contains_key("log_dir"))
|| self
.config_layer_stack
.requirements_toml()
.log_dir
.is_some();
configured.then_some(self.log_dir.as_path())
}
/// Resolves the configured, reviewer-catalog, or bundled Guardian policy.
pub fn resolve_guardian_policy<'a>(
&'a self,
model_messages: ResolvedModelMessages<'a>,
) -> &'a str {
self.guardian_policy_config
.as_deref()
.unwrap_or(model_messages.auto_review().policy)
}
pub(crate) fn multi_agent_version_override(&self) -> Option<MultiAgentVersion> {
if self.features.enabled(Feature::MultiAgentV2) {
Some(MultiAgentVersion::V2)
} else if !self.agents_enabled {
Some(MultiAgentVersion::Disabled)
} else {
None
}
}
pub(crate) fn multi_agent_version_from_features(&self) -> MultiAgentVersion {
self.multi_agent_version_override().unwrap_or_else(|| {
if self.features.enabled(Feature::Collab) {
MultiAgentVersion::V1
} else {
MultiAgentVersion::Disabled
}
})
}
pub(crate) fn multi_agent_version_for_model(
&self,
model_multi_agent_version: Option<MultiAgentVersion>,
) -> MultiAgentVersion {
self.multi_agent_version_override()
.or(model_multi_agent_version)
.unwrap_or_else(|| self.multi_agent_version_from_features())
}
pub(crate) fn effective_agent_max_threads(
&self,
multi_agent_version: MultiAgentVersion,
) -> Option<usize> {
match multi_agent_version {
MultiAgentVersion::V2 => Some(
self.multi_agent_v2
.max_concurrent_threads_per_session
.saturating_sub(1),
),
MultiAgentVersion::Disabled | MultiAgentVersion::V1 => {
self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS)
}
}
}
pub fn legacy_sandbox_policy(&self) -> SandboxPolicy {
self.permissions.legacy_sandbox_policy(self.cwd.as_path())
}
pub fn set_legacy_sandbox_policy(
&mut self,
sandbox_policy: SandboxPolicy,
) -> ConstraintResult<()> {
self.workspace_roots_explicit = matches!(
&sandbox_policy,
SandboxPolicy::WorkspaceWrite { writable_roots, .. } if !writable_roots.is_empty()
);
self.permissions
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())?;
self.workspace_roots = self.permissions.workspace_roots().to_vec();
Ok(())
}
/// Combine runtime and profile roots without interpreting them on the current host.
pub fn effective_workspace_roots(&self) -> Vec<PathUri> {
let mut workspace_roots = self
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect::<Vec<_>>();
workspace_roots.extend(
self.permissions
.profile_workspace_roots()
.iter()
.map(|root| root.as_uri().clone()),
);
// Preserve spelling changes even when Windows path comparison folds case.
let mut seen = HashSet::new();
workspace_roots.retain(|root| seen.insert(root.to_string()));
workspace_roots
}
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
ModelsManagerConfig {
model_context_window: self.model_context_window,
model_auto_compact_token_limit: self.model_auto_compact_token_limit,
tool_output_token_limit: self.tool_output_token_limit,
base_instructions: self.base_instructions.clone().filter(|_| {
!matches!(
self.base_instructions_provenance,
Some(BaseInstructionsProvenance::Model { .. })
)
}),
personality: self.personality,
model_catalog: self.model_catalog.clone(),
}
}
/// Returns backend routing that can resolve defaults against the request's auth snapshot.
pub fn chatgpt_backend_url(&self) -> ChatgptBaseUrl {
if !self.chatgpt_base_url_is_explicit && self.chatgpt_base_url == DEFAULT_CHATGPT_BASE_URL {
ChatgptBaseUrl::Default
} else {
ChatgptBaseUrl::Configured(self.chatgpt_base_url.clone())
}
}
/// Returns auth routing resolved from the effective feature configuration.
pub fn auth_route_config(&self) -> AuthRouteConfig {
self.application_auth_route_config
.clone()
.unwrap_or_else(|| {
AuthRouteConfig::from_http_client_factory(
self.http_client_factory()
.with_network_policy(self.application_network_policy.clone()),
)
})
}
/// Creates the HTTP client factory resolved from the effective feature configuration.
pub fn http_client_factory(&self) -> HttpClientFactory {
let outbound_proxy_policy = if self.respect_system_proxy {
OutboundProxyPolicy::RespectSystemProxy
} else {
OutboundProxyPolicy::ReqwestDefault
};
let mut factory = HttpClientFactory::new(outbound_proxy_policy)
.with_network_policy(self.application_network_policy.clone());
if !self.respect_system_proxy && self.features.enabled(Feature::SystemProxyFallback) {
factory = factory.with_system_proxy_fallback();
}
if self.features.enabled(Feature::Psp) {
factory.with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")])
} else {
factory
}
}
/// Build the plugin-manager input from the effective config.
pub fn plugins_config_input(&self) -> PluginsConfigInput {
PluginsConfigInput::new(
self.config_layer_stack.clone(),
self.plugins.clone(),
self.model_provider_id.clone(),
self.features.enabled(Feature::Plugins),
self.features.enabled(Feature::RemotePlugin),
self.chatgpt_base_url.clone(),
self.http_client_factory(),
self.apps_mcp_product_sku.clone(),
)
}
/// Applies managed MCP requirements to servers supplied by one plugin.
pub fn apply_plugin_mcp_server_requirements(
&self,
plugin_id: &str,
mcp_servers: &mut HashMap<String, McpServerConfig>,
) {
filter_plugin_mcp_servers_by_requirements(
plugin_id,
mcp_servers,
self.config_layer_stack.requirements().plugins.as_ref(),
);
let empty_mcp_allowlist = self
.config_layer_stack
.requirements()
.mcp_servers
.as_ref()
.filter(|requirements| requirements.value.is_empty());
filter_mcp_servers_by_requirements(mcp_servers, empty_mcp_allowlist);
}
pub async fn to_mcp_config(
&self,
plugins_manager: &codex_core_plugins::PluginsManager,
) -> McpConfig {
self.to_mcp_config_with_plugin_registrations(
plugins_manager,
std::iter::empty::<McpServerRegistration>(),
)
.await
}
pub(crate) async fn to_mcp_config_with_plugin_registrations(
&self,
plugins_manager: &codex_core_plugins::PluginsManager,
additional_plugin_registrations: impl IntoIterator<Item = McpServerRegistration>,
) -> McpConfig {
let plugins_input = self.plugins_config_input();
let loaded_plugins = plugins_manager.plugins_for_config(&plugins_input).await;
self.to_mcp_config_with_loaded_plugins(&loaded_plugins, additional_plugin_registrations)
}
pub(crate) fn to_mcp_config_with_loaded_plugins(
&self,
loaded_plugins: &PluginLoadOutcome,
additional_plugin_registrations: impl IntoIterator<Item = McpServerRegistration>,
) -> McpConfig {
let mut catalog = ResolvedMcpCatalog::builder();
if self.features.enabled(Feature::UseXaa)
&& let Some(auth) = &self.mcp_enterprise_managed_auth
{
catalog.enable_ema(auth.idp.clone());
}
for (plugin_order, plugin) in loaded_plugins
.plugins()
.iter()
.filter(|plugin| plugin.is_active())
.enumerate()
{
let mut plugin_mcp_servers = plugin.mcp_servers.clone();
self.apply_plugin_mcp_server_requirements(&plugin.config_name, &mut plugin_mcp_servers);
let attribution = if plugin.is_agent_plugin() {
McpPluginAttribution::agent_plugin(
plugin.config_name.clone(),
plugin.display_name().to_string(),
)
} else {
McpPluginAttribution::new(
plugin.config_name.clone(),
plugin.display_name().to_string(),
)
}
.with_host_root(PathUri::from_abs_path(&plugin.root));
for (name, plugin_server) in plugin_mcp_servers {
catalog.register(McpServerRegistration::from_plugin(
name,
attribution.clone(),
plugin_order,
plugin_server,
));
}
}
for registration in additional_plugin_registrations {
catalog.register(registration);
}
for (name, server) in self.mcp_servers.get() {
catalog.register(McpServerRegistration::from_config(
name.clone(),
server.clone(),
));
}
McpConfig {
chatgpt_base_url: self.chatgpt_base_url.clone(),
apps_mcp_product_sku: self.apps_mcp_product_sku.clone(),
requires_read_only_mcp_tools: false,
codex_home: self.codex_home.to_path_buf(),
mcp_enterprise_managed_auth: self.mcp_enterprise_managed_auth.clone(),
ema_state: Default::default(),
mcp_ema_cancellation: self.config_layer_stack.mcp_ema_cancellation().clone(),
xaa_enabled: self.features.enabled(Feature::UseXaa)
&& self.mcp_enterprise_managed_auth.is_some(),
mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode,
mcp_tool_list_changed: self.features.enabled(Feature::McpToolListChanged),
oauth_refresh_mode: if self.features.enabled(Feature::McpOAuthRefreshCoordination) {
McpOAuthRefreshMode::Coordinated
} else {
McpOAuthRefreshMode::Legacy
},
auth_keyring_backend_kind: self.auth_keyring_backend_kind(),
mcp_oauth_callback_port: self.mcp_oauth_callback_port,
mcp_oauth_callback_url: self.mcp_oauth_callback_url.clone(),
optional_mcp_startup_grace: self.mcp_optional_startup_grace,
skill_mcp_dependency_install_enabled: self
.features
.enabled(Feature::SkillMcpDependencyInstall),
approval_policy: self.permissions.approval_policy.clone(),
permission_profile: self.permissions.permission_profile().clone(),
host_cwd: self.cwd.clone(),
config_layer_stack: self.config_layer_stack.clone(),
plugins: self.plugins.clone(),
approvals_reviewer: self.approvals_reviewer,
environment_cwds: HashMap::new(),
environment_use_mxc: HashMap::new(),
server_permissions: HashMap::new(),
codex_self_exe: self.codex_self_exe.clone(),
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
use_legacy_landlock: self.features.use_legacy_landlock(),
apps_enabled: self.features.enabled(Feature::Apps),
apps_early_resource_discovery: self
.features
.enabled(Feature::AppsEarlyResourceDiscovery),
prefix_mcp_tool_names: self.prefix_mcp_tool_names(),
non_prefixed_mcp_tool_servers: if self
.features
.enabled(Feature::NonPrefixedMcpToolNames)
{
self.non_prefixed_mcp_tool_servers
.clone()
.unwrap_or_default()
} else {
Vec::new()
},
protocol_mode: self.mcp_protocol_mode(),
discover_local_stdio: self.features.enabled(Feature::McpConnectionSharing),
host_owned_apps_protocol_mode: if self.features.enabled(Feature::CodexAppsMcp20260728) {
McpProtocolMode::V20260728
} else {
McpProtocolMode::Legacy
},
client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) {
ElicitationCapability::new()
.with_form(FormElicitationCapability::new())
.with_url(UrlElicitationCapability::new())
} else {
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities
// indicates this should be an empty object.
ElicitationCapability::default()
},
mcp_server_catalog: catalog.build(),
connector_snapshot:
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
),
}
}
pub(crate) fn prefix_mcp_tool_names(&self) -> bool {
!self.features.enabled(Feature::NonPrefixedMcpToolNames)
|| self.non_prefixed_mcp_tool_servers.is_some()
}
pub fn mcp_protocol_mode(&self) -> McpProtocolMode {
if self.features.enabled(Feature::Mcp20260728) {
McpProtocolMode::V20260728
} else {
McpProtocolMode::Legacy
}
}
pub fn workspace_routing_context(&self) -> codex_model_provider::WorkspaceRoutingContext {
codex_model_provider::WorkspaceRoutingContext::new(self.chatgpt_base_url.clone())
.with_session(codex_login::WorkspaceRoutingSession {
cwd: self.cwd.to_path_buf(),
config_layer_stack: self.config_layer_stack.clone(),
})
}
pub async fn rebuild_with_session_layers(
session_layers: &ConfigLayerStack,
cwd: PathBuf,
refreshed_layers: &ConfigLayerStack,
codex_home: AbsolutePathBuf,
) -> std::io::Result<Self> {
let config_layer_stack =
Self::layer_stack_preserving_session(session_layers, refreshed_layers)?;
let cfg = config_toml_from_layers(&config_layer_stack).await?;
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
cfg,
ConfigOverrides {
cwd: Some(cwd),
..Default::default()
},
codex_home,
config_layer_stack,
)
.await
}
fn layer_stack_preserving_session(
session_layers: &ConfigLayerStack,
refreshed_layers: &ConfigLayerStack,
) -> std::io::Result<ConfigLayerStack> {
let mut layers = refreshed_layers
.all_layers_low_to_high()
.filter(|layer| !is_session_layer(&layer.name))
.cloned()
.collect::<Vec<_>>();
layers.extend(
session_layers
.all_layers_low_to_high()
.filter(|layer| is_session_layer(&layer.name))
.cloned(),
);
layers.sort_by_key(|layer| layer.name.precedence());
Ok(ConfigLayerStack::new(
layers,
refreshed_layers.requirements().clone(),
refreshed_layers.requirements_toml().clone(),
)?
.with_cloud_config_binding(refreshed_layers.cloud_config_binding().cloned())
.with_applied_config_server(refreshed_layers.applied_config_server().cloned())
.with_user_and_project_exec_policy_rules_ignored(
refreshed_layers.ignore_user_and_project_exec_policy_rules(),
))
}
/// This is the preferred way to create an instance of [Config].
pub async fn load_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
ConfigBuilder::default()
.cli_overrides(cli_overrides)
.build()
.await
}
/// Load a default configuration when user config files are invalid.
pub async fn load_default_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
let codex_home = find_codex_home()?;
Self::load_default_with_cli_overrides_for_codex_home(
codex_home.to_path_buf(),
cli_overrides,
)
.await
}
/// Load a default configuration for a specific Codex home without reading
/// user, project, or system config layers.
pub async fn load_default_with_cli_overrides_for_codex_home(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
let mut merged = toml::Value::try_from(ConfigToml::default()).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to serialize default config: {e}"),
)
})?;
let cli_layer = codex_config::build_cli_overrides_layer(&cli_overrides);
codex_config::merge_toml_values(&mut merged, &cli_layer);
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)?;
let config_toml = deserialize_config_toml_with_base(merged, &codex_home)?;
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
ConfigOverrides::default(),
codex_home,
ConfigLayerStack::default(),
)
.await
}
/// This is a secondary way of creating [Config], which is appropriate when
/// the harness is meant to be used with a specific configuration that
/// ignores user settings. For example, the `codex exec` subcommand is
/// designed to use [AskForApproval::Never] exclusively.
///
/// Further, [ConfigOverrides] contains some options that are not supported
/// in [ConfigToml], such as `cwd`, `codex_self_exe`, and `codex_linux_sandbox_exe`.
pub async fn load_with_cli_overrides_and_harness_overrides(
cli_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
) -> std::io::Result<Self> {
ConfigBuilder::default()
.cli_overrides(cli_overrides)
.harness_overrides(harness_overrides)
.build()
.await
}
}
pub fn resolve_profile_v2_config_path(
codex_home: &Path,
profile_name: &ProfileV2Name,
) -> AbsolutePathBuf {
AbsolutePathBuf::resolve_path_against_base(
format!("{profile_name}{CONFIG_PROFILE_V2_SUFFIX}"),
codex_home,
)
}
/// DEPRECATED: Use [Config::load_with_cli_overrides()] instead because working
/// with [ConfigToml] directly means that [ConfigRequirements] have not been
/// applied yet, which risks failing to enforce required constraints.
pub async fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<ConfigToml> {
load_config_as_toml_with_cli_and_loader_overrides(
codex_home,
cwd,
cli_overrides,
loader_overrides,
)
.await
}
/// DEPRECATED for most callers: prefer [Config::load_with_cli_overrides()] or
/// [ConfigBuilder] because working with [ConfigToml] directly means
/// [ConfigRequirements] have not been applied yet, which risks skipping
/// required constraints.
pub async fn load_config_as_toml_with_cli_and_loader_overrides(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<ConfigToml> {
load_config_as_toml_with_cli_and_load_options(codex_home, cwd, cli_overrides, loader_overrides)
.await
}
/// DEPRECATED for most callers: prefer [Config::load_with_cli_overrides()] or
/// [ConfigBuilder] because working with [ConfigToml] directly means
/// [ConfigRequirements] have not been applied yet, which risks skipping
/// required constraints.
pub async fn load_config_as_toml_with_cli_and_load_options(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
options: impl Into<ConfigLoadOptions>,
) -> std::io::Result<ConfigToml> {
load_config_toml_with_layer_stack(codex_home, cwd, cli_overrides, options)
.await
.map(|result| result.config_toml)
}
/// Partially loaded config plus the layer stack used to derive it.
///
/// This is intended for startup paths that must inspect raw config before a
/// full [`Config`] can be constructed, but still need access to managed
/// requirements loaded with the config layers.
pub struct ConfigTomlLoadResult {
pub config_toml: ConfigToml,
pub config_layer_stack: ConfigLayerStack,
}
/// Loads the partially merged config together with the layer stack used to
/// derive it, before constructing a full [`Config`].
pub async fn load_config_toml_with_layer_stack(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
options: impl Into<ConfigLoadOptions>,
) -> std::io::Result<ConfigTomlLoadResult> {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd.cloned(),
&cli_overrides,
options,
&codex_config::NoopThreadConfigLoader,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
let cfg = deserialize_config_toml_with_base(merged_toml, codex_home).map_err(|e| {
tracing::error!("Failed to deserialize overridden config: {e}");
e
})?;
Ok(ConfigTomlLoadResult {
config_toml: cfg,
config_layer_stack,
})
}
pub fn deserialize_config_toml_with_base(
root_value: TomlValue,
config_base_dir: &Path,
) -> std::io::Result<ConfigToml> {
// This guard ensures that any relative paths that is deserialized into an
// [AbsolutePathBuf] is resolved against `config_base_dir`.
let _guard = AbsolutePathBufGuard::new(config_base_dir);
ConfigToml::try_from(root_value)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
/// Validate user-visible feature settings against managed feature requirements.
pub fn validate_feature_requirements_for_config_toml(
cfg: &ConfigToml,
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<()> {
managed_features::validate_explicit_feature_settings_in_config_toml(cfg, feature_requirements)?;
managed_features::validate_feature_requirements_in_config_toml(cfg, feature_requirements)
}
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
let file_contents = std::fs::read_to_string(path)?;
let catalog = serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidData,
format!(
"failed to parse model_catalog_json path `{}` as JSON: {err}",
path.display()
),
)
})?;
if catalog.models.is_empty() {
return Err(std::io::Error::new(
ErrorKind::InvalidData,
format!(
"model_catalog_json path `{}` must contain at least one model",
path.display()
),
));
}
Ok(catalog)
}
fn load_model_catalog(
model_catalog_json: Option<AbsolutePathBuf>,
) -> std::io::Result<Option<ModelsResponse>> {
model_catalog_json
.map(|path| load_catalog_json(&path))
.transpose()
}
fn filter_mcp_servers_by_requirements(
mcp_servers: &mut HashMap<String, McpServerConfig>,
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
) {
let Some(allowlist) = mcp_requirements else {
return;
};
let source = allowlist.source.clone();
for (name, server) in mcp_servers.iter_mut() {
let allowed = allowlist
.value
.get(name)
.is_some_and(|requirement| server.matches_requirement(requirement));
if allowed {
server.disabled_reason = None;
} else {
server.disable_by_requirements(source.clone());
}
}
}
fn filter_plugin_mcp_servers_by_requirements(
plugin_config_name: &str,
mcp_servers: &mut HashMap<String, McpServerConfig>,
plugin_requirements: Option<&Sourced<BTreeMap<String, PluginRequirementsToml>>>,
) {
let Some(requirements) = plugin_requirements else {
return;
};
if !requirements
.value
.values()
.any(|plugin| plugin.mcp_servers.is_some())
{
return;
}
let source = requirements.source.clone();
let plugin_mcp_requirements = requirements
.value
.get(plugin_config_name)
.and_then(|plugin| plugin.mcp_servers.as_ref());
for (name, server) in mcp_servers.iter_mut() {
let allowed = plugin_mcp_requirements
.and_then(|mcp_requirements| mcp_requirements.get(name))
.is_some_and(|requirement| server.matches_requirement(requirement));
if allowed {
server.disabled_reason = None;
} else {
server.disable_by_requirements(source.clone());
}
}
}
fn constrain_mcp_servers(
mcp_servers: HashMap<String, McpServerConfig>,
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
) -> ConstraintResult<Constrained<HashMap<String, McpServerConfig>>> {
if mcp_requirements.is_none() {
return Ok(Constrained::allow_any(mcp_servers));
}
let mcp_requirements = mcp_requirements.cloned();
Constrained::normalized(mcp_servers, move |mut servers| {
filter_mcp_servers_by_requirements(&mut servers, mcp_requirements.as_ref());
servers
})
}
fn apply_requirement_constrained_value<T>(
field_name: &'static str,
configured_value: T,
constrained_value: &mut ConstrainedWithSource<T>,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<bool>
where
T: Clone + std::fmt::Debug + Send + Sync,
{
if let Err(err) = constrained_value.set(configured_value) {
let fallback_value = constrained_value.get().clone();
tracing::warn!(
error = %err,
?fallback_value,
requirement_source = ?constrained_value.source,
"configured value is disallowed by requirements; falling back to required value for {field_name}"
);
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| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"configured value for `{field_name}` is disallowed by requirements ({err}); fallback to a requirement-compliant value also failed ({fallback_err})"
),
)
})?;
return Ok(true);
}
Ok(false)
}
pub async fn load_global_mcp_servers(
codex_home: &Path,
) -> std::io::Result<BTreeMap<String, McpServerConfig>> {
// In general, Config::load_with_cli_overrides() should be used to load the
// full config with requirements.toml applied, but in this case, we need
// access to the raw TOML in order to warn the user about deprecated fields.
//
// Note that a more precise way to do this would be to audit the individual
// config layers for deprecated fields rather than reporting on the merged
// result.
let cli_overrides = Vec::<(String, TomlValue)>::new();
// There is no cwd/project context for this query, so this will not include
// MCP servers defined in in-repo .codex/ folders.
let cwd: Option<AbsolutePathBuf> = None;
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd,
&cli_overrides,
LoaderOverrides::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
let Some(servers_value) = merged_toml.get("mcp_servers") else {
return Ok(BTreeMap::new());
};
ensure_no_inline_bearer_tokens(servers_value)?;
servers_value
.clone()
.try_into()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
/// We briefly allowed plain text bearer_token fields in MCP server configs.
/// We want to warn people who recently added these fields but can remove this after a few months.
fn ensure_no_inline_bearer_tokens(value: &TomlValue) -> std::io::Result<()> {
let Some(servers_table) = value.as_table() else {
return Ok(());
};
for (server_name, server_value) in servers_table {
if let Some(server_table) = server_value.as_table()
&& server_table.contains_key("bearer_token")
{
let message = format!(
"mcp_servers.{server_name} uses unsupported `bearer_token`; set `bearer_token_env_var`."
);
return Err(std::io::Error::new(ErrorKind::InvalidData, message));
}
}
Ok(())
}
pub(crate) fn set_project_trust_level_inner(
doc: &mut DocumentMut,
project_path: &Path,
trust_level: TrustLevel,
) -> anyhow::Result<()> {
// Ensure we render a human-friendly structure:
//
// [projects]
// [projects."/path/to/project"]
// trust_level = "trusted" or "untrusted"
//
// rather than inline tables like:
//
// [projects]
// "/path/to/project" = { trust_level = "trusted" }
let project_key = project_trust_key(project_path);
// Ensure top-level `projects` exists as a non-inline, explicit table. If it
// exists but was previously represented as a non-table (e.g., inline),
// replace it with an explicit table.
{
let root = doc.as_table_mut();
// If `projects` exists but isn't a standard table (e.g., it's an inline table),
// convert it to an explicit table while preserving existing entries.
let existing_projects = root.get("projects").cloned();
if existing_projects.as_ref().is_none_or(|i| !i.is_table()) {
let mut projects_tbl = toml_edit::Table::new();
projects_tbl.set_implicit(true);
// If there was an existing inline table, migrate its entries to explicit tables.
if let Some(inline_tbl) = existing_projects.as_ref().and_then(|i| i.as_inline_table()) {
for (k, v) in inline_tbl.iter() {
if let Some(inner_tbl) = v.as_inline_table() {
let new_tbl = inner_tbl.clone().into_table();
projects_tbl.insert(k, toml_edit::Item::Table(new_tbl));
}
}
}
root.insert("projects", toml_edit::Item::Table(projects_tbl));
}
}
let Some(projects_tbl) = doc["projects"].as_table_mut() else {
return Err(anyhow::anyhow!(
"projects table missing after initialization"
));
};
// Ensure the per-project entry is its own explicit table. If it exists but
// is not a table (e.g., an inline table), replace it with an explicit table.
let needs_proj_table = !projects_tbl.contains_key(project_key.as_str())
|| projects_tbl
.get(project_key.as_str())
.and_then(|i| i.as_table())
.is_none();
if needs_proj_table {
projects_tbl.insert(project_key.as_str(), toml_edit::table());
}
let Some(proj_tbl) = projects_tbl
.get_mut(project_key.as_str())
.and_then(|i| i.as_table_mut())
else {
return Err(anyhow::anyhow!("project table missing for {project_key}"));
};
proj_tbl.set_implicit(false);
proj_tbl["trust_level"] = toml_edit::value(trust_level.to_string());
Ok(())
}
/// Patch `CODEX_HOME/config.toml` project state to set trust level.
/// Use with caution.
pub fn set_project_trust_level(
codex_home: &Path,
project_path: &Path,
trust_level: TrustLevel,
) -> anyhow::Result<()> {
use crate::config::edit::ConfigEditsBuilder;
ConfigEditsBuilder::new(codex_home)
.set_project_trust_level(project_path, trust_level)
.apply_blocking()
}
/// Save the default OSS provider preference to config.toml
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
codex_config::config_toml::validate_oss_provider(provider)?;
use toml_edit::value;
let edits = [ConfigEdit::SetPath {
segments: vec!["oss_provider".to_string()],
value: value(provider),
}];
ConfigEditsBuilder::new(codex_home)
.with_edits(edits)
.apply_blocking()
.map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}")))
}
fn resolve_tool_suggest_config(
config_toml: &ConfigToml,
config_layer_stack: &ConfigLayerStack,
) -> ToolSuggestConfig {
resolve_tool_suggest_config_from_config(config_toml.tool_suggest.as_ref(), config_layer_stack)
}
pub(crate) fn resolve_tool_suggest_config_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
) -> ToolSuggestConfig {
let tool_suggest = config_layer_stack
.effective_config()
.get("tool_suggest")
.cloned()
.and_then(|value| value.try_into::<ToolSuggestConfig>().ok());
resolve_tool_suggest_config_from_config(tool_suggest.as_ref(), config_layer_stack)
}
fn resolve_tool_suggest_config_from_config(
tool_suggest: Option<&ToolSuggestConfig>,
config_layer_stack: &ConfigLayerStack,
) -> ToolSuggestConfig {
let discoverables = tool_suggest
.into_iter()
.flat_map(|tool_suggest| tool_suggest.discoverables.iter())
.filter_map(|discoverable| {
let trimmed = discoverable.id.trim();
if trimmed.is_empty() {
None
} else {
Some(ToolSuggestDiscoverable {
kind: discoverable.kind,
id: trimmed.to_string(),
})
}
})
.collect();
let mut seen_disabled_tools = HashSet::new();
let mut disabled_tools = Vec::new();
let mut add_disabled_tool = |disabled_tool: ToolSuggestDisabledTool| {
if let Some(disabled_tool) = disabled_tool.normalized()
&& seen_disabled_tools.insert(disabled_tool.clone())
{
disabled_tools.push(disabled_tool);
}
};
let mut layers = config_layer_stack.layers_low_to_high().peekable();
if layers.peek().is_none() {
for disabled_tool in tool_suggest
.into_iter()
.flat_map(|tool_suggest| tool_suggest.disabled_tools.iter().cloned())
{
add_disabled_tool(disabled_tool);
}
} else {
for layer in layers {
let Some(tool_suggest) = layer
.config
.get("tool_suggest")
.cloned()
.and_then(|value| value.try_into::<ToolSuggestConfig>().ok())
else {
continue;
};
for disabled_tool in tool_suggest.disabled_tools {
add_disabled_tool(disabled_tool);
}
}
}
ToolSuggestConfig {
discoverables,
disabled_tools,
}
}
fn thread_store_config(thread_store: Option<ThreadStoreToml>) -> ThreadStoreConfig {
match thread_store {
Some(ThreadStoreToml::Local {}) => ThreadStoreConfig::Local,
Some(ThreadStoreToml::InMemory { id }) => ThreadStoreConfig::InMemory { id },
None => ThreadStoreConfig::Local,
}
}
fn is_session_layer(source: &ConfigLayerSource) -> bool {
matches!(source, ConfigLayerSource::SessionFlags)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PermissionConfigSyntax {
Legacy,
Profiles,
}
#[derive(Debug, Deserialize, Default)]
struct PermissionSelectionToml {
default_permissions: Option<String>,
sandbox_mode: Option<SandboxMode>,
}
// Resolve the named-profile catalog and selected profile id together. Runtime
// profile constraints are applied later after this selection compiles into a
// concrete `PermissionProfile`.
#[derive(Debug)]
struct EffectivePermissionSelection<'a> {
profiles: Option<PermissionsToml>,
selected_profile_id: Option<&'a str>,
persisted_profile_id_was_provided: bool,
requirements_force_profile_selection: bool,
}
impl EffectivePermissionSelection<'_> {
fn has_profiles(&self) -> bool {
self.profiles
.as_ref()
.is_some_and(|profiles| !profiles.is_empty())
}
fn profiles_are_active(
&self,
default_permissions_override: Option<&str>,
permission_config_syntax: Option<PermissionConfigSyntax>,
) -> bool {
self.persisted_profile_id_was_provided
|| self.requirements_force_profile_selection
|| default_permissions_override.is_some()
|| matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
)
|| permission_config_syntax.is_none()
}
}
fn resolve_permission_config_syntax(
config_layer_stack: &ConfigLayerStack,
cfg: &ConfigToml,
sandbox_mode_override: Option<SandboxMode>,
) -> Option<PermissionConfigSyntax> {
if sandbox_mode_override.is_some() {
return Some(PermissionConfigSyntax::Legacy);
}
let session_flags_select_profiles = config_layer_stack
.layers_high_to_low()
.find(|layer| matches!(layer.name, ConfigLayerSource::SessionFlags))
.and_then(|layer| {
layer
.config
.clone()
.try_into::<PermissionSelectionToml>()
.ok()
})
.is_some_and(|selection| selection.default_permissions.is_some());
if session_flags_select_profiles {
return Some(PermissionConfigSyntax::Profiles);
}
let mut selection = None;
for layer in config_layer_stack.layers_low_to_high() {
let Ok(layer_selection) = layer.config.clone().try_into::<PermissionSelectionToml>() else {
continue;
};
if layer_selection.sandbox_mode.is_some() {
selection = Some(PermissionConfigSyntax::Legacy);
}
if layer_selection.default_permissions.is_some() {
selection = Some(PermissionConfigSyntax::Profiles);
}
}
selection.or_else(|| {
if cfg.default_permissions.is_some() {
Some(PermissionConfigSyntax::Profiles)
} else if cfg.sandbox_mode.is_some() {
Some(PermissionConfigSyntax::Legacy)
} else {
None
}
})
}
/// Optional overrides for user configuration (e.g., from CLI flags).
#[derive(Default, Debug, Clone)]
pub struct ConfigOverrides {
pub model: Option<String>,
pub review_model: Option<String>,
pub cwd: Option<PathBuf>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_mode: Option<SandboxMode>,
pub permission_profile: Option<PermissionProfile>,
pub default_permissions: Option<String>,
/// Permission profile ID recovered from persisted thread state. Explicit
/// permission overrides take precedence, and stale profile IDs fall back
/// to the configured default.
pub persisted_permission_profile_id: Option<String>,
/// Unnamed permissions from a thread-owned rollout snapshot. These are
/// fallback state: explicit overrides and current requirements take priority.
pub persisted_permission_profile: Option<PermissionProfile>,
pub model_provider: Option<String>,
pub service_tier: Option<Option<String>>,
pub codex_self_exe: Option<PathBuf>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
/// Ignored compatibility field for clients that still pass the retired wrapper path.
pub main_execve_wrapper_exe: Option<PathBuf>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
pub compact_prompt: Option<String>,
pub show_raw_agent_reasoning: Option<bool>,
pub tools_web_search_request: Option<bool>,
pub ephemeral: Option<bool>,
pub bypass_hook_trust: Option<bool>,
/// Additional directories that should be treated as writable roots for this session.
pub additional_writable_roots: Vec<PathBuf>,
/// Explicit absolute runtime workspace roots for this session. When set,
/// this is the full runtime root list rather than an additive override.
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
}
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
let mut seen = HashSet::new();
paths.retain(|path| seen.insert(path.clone()));
}
/// Resolves the OSS provider from CLI override or global config.
/// Returns `None` if no provider is configured at any level.
pub fn resolve_oss_provider(
explicit_provider: Option<&str>,
config_toml: &ConfigToml,
) -> Option<String> {
if let Some(provider) = explicit_provider {
// Explicit provider specified (e.g., via --local-provider)
Some(provider.to_string())
} else {
config_toml.oss_provider.clone()
}
}
/// Resolve the web search mode from explicit config and feature flags.
fn resolve_web_search_mode(config_toml: &ConfigToml, features: &Features) -> Option<WebSearchMode> {
if let Some(mode) = config_toml.web_search {
return Some(mode);
}
if features.enabled(Feature::WebSearchCached) {
return Some(WebSearchMode::Cached);
}
if features.enabled(Feature::WebSearchRequest) {
return Some(WebSearchMode::Live);
}
None
}
fn resolve_web_search_config(config_toml: &ConfigToml) -> Option<WebSearchConfig> {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.web_search.as_ref())
.cloned()
.map(Into::into)
}
fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) -> bool {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.experimental_request_user_input.as_ref())
.is_none_or(|config| config.enabled)
}
fn resolve_update_plan_enabled(config_toml: &ConfigToml) -> bool {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.update_plan.as_ref())
.is_some_and(|config| config.enabled)
}
fn resolve_feature_enabled(feature: Option<&codex_config::config_toml::FeatureToggleToml>) -> bool {
feature.and_then(|feature| feature.enabled).unwrap_or(true)
}
fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
let base = code_mode_toml_config(config_toml.features.as_ref());
let host = config_toml
.features
.as_ref()
.and_then(|features| features.code_mode_host.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
});
CodeModeConfig {
default_exec_yield_time_ms: base
.and_then(|config| config.default_exec_yield_time_ms)
.unwrap_or(DEFAULT_CODE_MODE_EXEC_YIELD_TIME_MS),
experimental_show_cell_overhead: base
.and_then(|config| config.experimental_show_cell_overhead)
.unwrap_or_default(),
tool_input_schema_max_bytes: base
.and_then(|config| config.tool_input_schema_max_bytes)
.map(NonZeroUsize::get),
excluded_tool_namespaces: base
.and_then(|config| config.excluded_tool_namespaces.as_ref())
.cloned()
.unwrap_or_default(),
direct_only_tool_namespaces: base
.and_then(|config| config.direct_only_tool_namespaces.as_ref())
.cloned()
.unwrap_or_default(),
disable_in_process_fallback: host
.and_then(|config| config.disable_in_process_fallback)
.unwrap_or_default(),
}
}
fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
let max_concurrent_threads_per_session = base
.and_then(|config| config.max_concurrent_threads_per_session)
.or_else(|| {
config_toml
.agents
.as_ref()
.and_then(|agents| agents.max_concurrent_threads_per_session)
.map(|max_threads| max_threads.saturating_add(1))
})
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION);
let default =
MultiAgentV2Config::defaults_for_max_concurrency(max_concurrent_threads_per_session);
let min_wait_timeout_ms = base
.and_then(|config| config.min_wait_timeout_ms)
.unwrap_or(default.min_wait_timeout_ms);
let max_wait_timeout_ms = base
.and_then(|config| config.max_wait_timeout_ms)
.unwrap_or(default.max_wait_timeout_ms);
let default_wait_timeout_ms = base
.and_then(|config| config.default_wait_timeout_ms)
.unwrap_or(default.default_wait_timeout_ms);
let usage_hint_text = base
.and_then(|config| config.usage_hint_text.as_ref())
.cloned()
.or(default.usage_hint_text);
let hide_spawn_agent_metadata = base
.and_then(|config| config.hide_spawn_agent_metadata)
.unwrap_or(default.hide_spawn_agent_metadata);
let expose_spawn_agent_model_overrides = base
.and_then(|config| config.expose_spawn_agent_model_overrides)
.unwrap_or(default.expose_spawn_agent_model_overrides);
let root_agent_usage_hint_text = base
.and_then(|config| config.root_agent_usage_hint_text.as_ref())
.cloned();
let subagent_usage_hint_text = base
.and_then(|config| config.subagent_usage_hint_text.as_ref())
.cloned();
let wait_agent_enabled = base
.and_then(|config| config.wait_agent_enabled)
.unwrap_or(default.wait_agent_enabled);
let disable_native_tools = base
.and_then(|config| config.disable_native_tools)
.unwrap_or(default.disable_native_tools);
let disable_direct_message = base
.and_then(|config| config.disable_direct_message)
.unwrap_or(default.disable_direct_message);
let message_board_in_memory = base
.and_then(|config| config.message_board_in_memory)
.unwrap_or(default.message_board_in_memory);
let subagent_developer_instructions = base
.and_then(|config| config.subagent_developer_instructions.as_ref())
.map(|instructions| instructions.trim().to_string());
let preserve_fork_prefix = base
.and_then(|config| config.preserve_fork_prefix)
.unwrap_or(default.preserve_fork_prefix);
let multi_agent_mode_hint_text = base
.and_then(|config| config.multi_agent_mode_hint_text.as_ref())
.cloned()
.or(default.multi_agent_mode_hint_text);
let tool_namespace = base
.and_then(|config| config.tool_namespace.as_ref())
.cloned()
.or(default.tool_namespace);
let non_code_mode_only = base
.and_then(|config| config.non_code_mode_only)
.unwrap_or(default.non_code_mode_only);
MultiAgentV2Config {
max_concurrent_threads_per_session,
min_wait_timeout_ms,
max_wait_timeout_ms,
default_wait_timeout_ms,
usage_hint_text,
root_agent_usage_hint_text,
subagent_usage_hint_text,
subagent_developer_instructions,
preserve_fork_prefix,
multi_agent_mode_hint_text,
tool_namespace,
hide_spawn_agent_metadata,
expose_spawn_agent_model_overrides,
wait_agent_enabled,
disable_native_tools,
disable_direct_message,
message_board_in_memory,
message_board_remote: base.and_then(|config| config.message_board_remote.clone()),
non_code_mode_only,
}
}
pub(crate) fn resolve_token_budget_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
) -> std::io::Result<Option<TokenBudgetConfig>> {
if !features.enabled(Feature::TokenBudget) {
return Ok(None);
}
let token_budget_config = token_budget_toml_config(config_toml.features.as_ref());
let use_history_notes_extension = token_budget_config
.and_then(|config| config.use_history_notes_extension)
.unwrap_or_default();
let reminder_threshold_tokens =
token_budget_config.and_then(|config| config.reminder_threshold_tokens);
let reminder_message_template = token_budget_config
.and_then(|config| config.reminder_message_template.clone())
.unwrap_or_else(|| {
ResolvedModelMessages::bundled()
.token_budget_reminder_template()
.to_owned()
});
let guidance_message = token_budget_config
.and_then(|config| config.guidance_message.clone())
.filter(|message| !message.trim().is_empty());
let auto_compact_fallback_prompt = token_budget_config
.and_then(|config| config.auto_compact_fallback_prompt.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
let auto_compact_fallback_buffer_tokens =
token_budget_config.and_then(|config| config.auto_compact_fallback_buffer_tokens);
let token_budget = TokenBudgetConfig {
use_history_notes_extension,
reminder_threshold_tokens,
reminder_message_template,
guidance_message,
auto_compact_fallback_prompt,
auto_compact_fallback_buffer_tokens,
};
token_budget.validate()?;
Ok(Some(token_budget))
}
fn resolve_rollout_budget_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
) -> std::io::Result<Option<RolloutBudgetConfig>> {
if !features.enabled(Feature::RolloutBudget) {
return Ok(None);
}
let missing_limit_error = || {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.limit_tokens is required when rollout_budget is enabled",
)
};
let Some(FeatureToml::Config(config)) = config_toml
.features
.as_ref()
.and_then(|features| features.rollout_budget.as_ref())
else {
return Err(missing_limit_error());
};
let Some(limit_tokens) = config.limit_tokens else {
return Err(missing_limit_error());
};
if limit_tokens <= 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.limit_tokens must be positive",
));
}
let reminder_at_remaining_tokens =
config
.reminder_at_remaining_tokens
.clone()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.reminder_at_remaining_tokens is required when rollout_budget is enabled",
)
})?;
if reminder_at_remaining_tokens
.iter()
.any(|&tokens| tokens <= 0 || tokens >= limit_tokens)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.rollout_budget.reminder_at_remaining_tokens must contain only positive values below limit_tokens",
));
}
let sampling_token_weight = config.sampling_token_weight.unwrap_or(1.0);
let prefill_token_weight = config.prefill_token_weight.unwrap_or(1.0);
for (field, weight) in [
("sampling_token_weight", sampling_token_weight),
("prefill_token_weight", prefill_token_weight),
] {
if !weight.is_finite() || weight < 0.0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("features.rollout_budget.{field} must be finite and non-negative"),
));
}
}
Ok(Some(RolloutBudgetConfig {
enforce_limit: config.enforce_limit.unwrap_or(true),
limit_tokens,
reminder_at_remaining_tokens,
sampling_token_weight,
prefill_token_weight,
}))
}
fn resolve_current_time_reminder_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
) -> std::io::Result<Option<CurrentTimeReminderConfig>> {
if !features.enabled(Feature::CurrentTimeReminder) {
return Ok(None);
}
let base = current_time_reminder_toml_config(config_toml.features.as_ref());
let default = CurrentTimeReminderConfig::default();
let reminder_interval_seconds = base
.and_then(|config| config.reminder_interval_seconds)
.unwrap_or(default.reminder_interval_seconds);
Ok(Some(CurrentTimeReminderConfig {
reminder_interval_seconds,
clock_source: base
.and_then(|config| config.clock_source)
.unwrap_or(default.clock_source),
delivery_mode: base
.and_then(|config| config.delivery_mode)
.unwrap_or(default.delivery_mode),
sleep_tool: base
.and_then(|config| config.sleep_tool)
.unwrap_or(default.sleep_tool),
}))
}
fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalResizeReflowConfig {
let Some(tui) = config_toml.tui.as_ref() else {
return TerminalResizeReflowConfig::default();
};
TerminalResizeReflowConfig {
max_rows: match tui.terminal_resize_reflow_max_rows {
Some(0) => TerminalResizeReflowMaxRows::Disabled,
Some(rows) => TerminalResizeReflowMaxRows::Limit(rows),
None => TerminalResizeReflowMaxRows::Auto,
},
}
}
fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> {
match features?.code_mode.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> {
match features?.multi_agent_v2.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn token_budget_toml_config(features: Option<&FeaturesToml>) -> Option<&TokenBudgetConfigToml> {
match features?.token_budget.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn current_time_reminder_toml_config(
features: Option<&FeaturesToml>,
) -> Option<&CurrentTimeReminderConfigToml> {
match features?.current_time_reminder.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn network_proxy_toml_config(features: Option<&FeaturesToml>) -> Option<&NetworkProxyConfigToml> {
match features?.network_proxy.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
/// Bootstrap-only resolver for the cloud-config fetch.
///
/// Call before a cloud-config bundle is available. Final [`Config`] loading
/// resolves the effective feature value after all layers are available.
pub fn resolve_bootstrap_respect_system_proxy(
cfg: &ConfigToml,
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<bool> {
resolve_bootstrap_http_client_factory(cfg, feature_requirements)
.map(|factory| factory.outbound_proxy_policy() == OutboundProxyPolicy::RespectSystemProxy)
}
/// Resolves auth route settings for the initial cloud-config bootstrap.
pub fn resolve_bootstrap_auth_route_config(
cfg: &ConfigToml,
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<AuthRouteConfig> {
resolve_bootstrap_http_client_factory(cfg, feature_requirements)
.map(AuthRouteConfig::from_http_client_factory)
}
/// Resolves shared HTTP routing for startup work that runs before final [`Config`] loading.
pub fn resolve_bootstrap_http_client_factory(
cfg: &ConfigToml,
feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<HttpClientFactory> {
let configured_features = Features::from_sources(
FeatureConfigSource {
features: cfg.features.as_ref(),
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
let features =
ManagedFeatures::from_configured(configured_features, feature_requirements.cloned())?;
let outbound_proxy_policy = if features.enabled(Feature::RespectSystemProxy) {
OutboundProxyPolicy::RespectSystemProxy
} else {
OutboundProxyPolicy::ReqwestDefault
};
let mut factory = HttpClientFactory::new(outbound_proxy_policy);
if outbound_proxy_policy == OutboundProxyPolicy::ReqwestDefault
&& features.enabled(Feature::SystemProxyFallback)
{
factory = factory.with_system_proxy_fallback();
}
Ok(factory)
}
pub(crate) fn resolve_web_search_mode_for_turn(
web_search_mode: &Constrained<WebSearchMode>,
permission_profile: &PermissionProfile,
provider_capabilities: ProviderCapabilities,
) -> WebSearchMode {
let preferred = web_search_mode.value();
let is_allowed = |mode: WebSearchMode| {
let provider_supports_mode = match mode {
WebSearchMode::Live | WebSearchMode::Indexed => {
provider_capabilities.external_web_access
}
WebSearchMode::Cached | WebSearchMode::Disabled => true,
};
provider_supports_mode && web_search_mode.can_set(&mode).is_ok()
};
if matches!(permission_profile, PermissionProfile::Disabled)
&& !matches!(preferred, WebSearchMode::Disabled | WebSearchMode::Indexed)
{
for mode in [
WebSearchMode::Live,
WebSearchMode::Cached,
WebSearchMode::Disabled,
] {
if is_allowed(mode) {
return mode;
}
}
} else {
if is_allowed(preferred) {
return preferred;
}
for mode in [
WebSearchMode::Cached,
WebSearchMode::Live,
WebSearchMode::Disabled,
] {
if is_allowed(mode) {
return mode;
}
}
}
WebSearchMode::Disabled
}
fn validate_multi_agent_v2_wait_timeout(label: &str, value: i64) -> std::io::Result<()> {
if value < HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{label} must be at least {HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS}"),
));
}
if value > HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{label} must be at most {HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS}"),
));
}
Ok(())
}
fn validate_multi_agent_v2_tool_namespace(namespace: Option<&str>) -> std::io::Result<()> {
const LABEL: &str = "features.multi_agent_v2.tool_namespace";
const MAX_LEN: usize = 64;
const RESERVED_RESPONSES_NAMESPACES: &[&str] = &[
"api_tool",
"browser",
"computer",
"container",
"file_search",
"functions",
"image_gen",
"multi_tool_use",
"python",
"python_user_visible",
"submodel_delegator",
"terminal",
"tool_search",
"web",
];
let Some(namespace) = namespace else {
return Ok(());
};
if namespace.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{LABEL} must not be empty"),
));
}
if namespace.trim() != namespace {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{LABEL} must not have leading or trailing whitespace"),
));
}
if !namespace
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{LABEL} must match ^[a-zA-Z0-9_-]+$"),
));
}
if namespace.chars().count() > MAX_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{LABEL} must be at most {MAX_LEN} characters"),
));
}
if namespace == "mcp"
|| namespace.starts_with("mcp__")
|| RESERVED_RESPONSES_NAMESPACES.contains(&namespace)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{LABEL} uses a reserved namespace: {namespace}"),
));
}
Ok(())
}
impl Config {
#[cfg(test)]
async fn load_from_base_config_with_overrides(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
) -> std::io::Result<Self> {
// Note this ignores requirements.toml enforcement for tests.
let config_layer_stack = ConfigLayerStack::default();
Self::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
cfg,
overrides,
codex_home,
config_layer_stack,
)
.await
}
pub(crate) async fn load_config_with_layer_stack(
fs: &dyn ExecutorFileSystem,
mut cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
) -> std::io::Result<Self> {
config_layer_stack.validate_project_mitm_profiles()?;
// Keep the large config-construction future off small test thread stacks.
Box::pin(async move {
if cfg.experimental_thread_store_endpoint.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`experimental_thread_store_endpoint` is no longer supported; remove it from config.toml",
));
}
validate_model_providers(&cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
for (slot, dial) in cfg.tui.iter().flat_map(|tui| &tui.quick_dials) {
if !matches!(slot.as_bytes(), [b'0'..=b'9']) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("tui.presets.{slot}: slot must be a single digit from 0 to 9"),
));
}
dial.validate().map_err(|reason| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("tui.presets.{slot}: {reason}"),
)
})?;
}
if cfg.model_post_turn_compact_threshold_percent.is_some_and(|percent| percent > 100) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"model_post_turn_compact_threshold_percent must be between 0 and 100",
));
}
let async_compaction_threshold_percent = cfg.features.as_ref()
.and_then(|features| features.async_compaction.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => config.model_async_compact_threshold_percent,
})
.unwrap_or(DEFAULT_ASYNC_COMPACTION_THRESHOLD_PERCENT);
if !(1..=100).contains(&async_compaction_threshold_percent) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"model_async_compact_threshold_percent must be between 1 and 100",
));
}
if let Some(responses_api_metadata) = cfg.responses_api_metadata.as_ref() {
validate_extra_metadata(responses_api_metadata.iter()).map_err(|message| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, message)
})?;
}
validate_managed_developer_instructions(
config_layer_stack
.requirements()
.additional_developer_instructions
.as_ref(),
)?;
let orchestrator = cfg.orchestrator.as_ref();
let cloud_skill_enabled = cfg
.cloud
.as_ref()
.and_then(|cloud| cloud.skills.as_ref())
.and_then(|skills| skills.enabled)
.unwrap_or(true);
let orchestrator_mcp_enabled =
resolve_feature_enabled(orchestrator.and_then(|value| value.mcp.as_ref()));
let mut startup_warnings = config_layer_stack
.startup_warnings()
.unwrap_or_default()
.to_vec();
let configured_sqlite_home = cfg.sqlite_home.clone();
requirements::apply_to_config(
&mut cfg,
config_layer_stack.requirements(),
&mut startup_warnings,
);
// Packaged defaults supply a URL even when no override was configured.
let chatgpt_base_url_is_explicit =
config_layer_stack.requirements().chatgpt_base_url.is_some()
|| config_layer_stack
.layers_high_to_low()
.find(|layer| layer.config.get("chatgpt_base_url").is_some())
.map_or(cfg.chatgpt_base_url.is_some(), |layer| {
!matches!(layer.name, ConfigLayerSource::PackagedDefaults { .. })
});
// Destructure every field to ensure ConfigRequirements additions are
// either applied above or handled while constructing the final Config.
let ConfigRequirements {
allowed_login_methods: _,
allowed_chatgpt_workspaces: _,
cli_auth_credentials_store,
chatgpt_base_url: _,
sqlite_home: _,
unsafe_enforce_sqlite_journal_mode,
log_dir: _,
model_catalog_json: _,
model_provider: _,
model_providers: _,
check_for_update_on_startup: _,
allow_login_shell: _,
feedback: _,
otel: _,
approval_policy: mut constrained_approval_policy,
approvals_reviewer: mut constrained_approvals_reviewer,
auto_review_required_models: _,
permission_profile: mut constrained_permission_profile,
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
web_search_mode: mut constrained_web_search_mode,
allow_managed_hooks_only: _,
allow_appshots: _,
allow_remote_control: _,
computer_use: _,
feature_requirements,
managed_hooks: _,
mcp_servers,
plugins: _,
marketplaces: _,
exec_policy: _,
enforce_residency,
network: network_requirements,
application: _,
filesystem: filesystem_requirements,
additional_developer_instructions: _,
guardian_policy_config_source: _,
guardian_extra_policy_source: _,
} = config_layer_stack.requirements().clone();
// Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
model,
review_model: override_review_model,
cwd,
approval_policy: approval_policy_override,
approvals_reviewer: approvals_reviewer_override,
sandbox_mode,
permission_profile,
default_permissions: default_permissions_override,
persisted_permission_profile_id,
persisted_permission_profile,
model_provider,
service_tier: service_tier_override,
codex_self_exe,
codex_linux_sandbox_exe,
main_execve_wrapper_exe: _,
base_instructions,
developer_instructions,
personality,
compact_prompt,
show_raw_agent_reasoning,
tools_web_search_request: override_tools_web_search_request,
ephemeral,
bypass_hook_trust,
additional_writable_roots,
workspace_roots: workspace_roots_override,
} = overrides;
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
if bypass_hook_trust {
startup_warnings.push(
"`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."
.to_string(),
);
}
if sandbox_mode.is_some() && permission_profile.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`sandbox_mode` and `permission_profile` overrides cannot both be set",
));
}
if sandbox_mode.is_some() && default_permissions_override.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`sandbox_mode` and `default_permissions` overrides cannot both be set",
));
}
if permission_profile.is_some() && default_permissions_override.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`permission_profile` and `default_permissions` overrides cannot both be set",
));
}
if let Some(profile) = cfg.profile.as_deref() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"legacy `profile = \"{profile}\"` config is no longer supported; use `--profile {profile}` with `{profile}.config.toml` instead"
),
));
}
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
let feature_overrides = FeatureOverrides {
web_search_request: override_tools_web_search_request,
};
let mut configured_features = Features::from_sources(
FeatureConfigSource {
features: cfg.features.as_ref(),
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
},
FeatureConfigSource {
..Default::default()
},
feature_overrides,
);
if cfg.agents.as_ref().and_then(|agents| agents.enabled) == Some(true)
&& !configured_features.enabled(Feature::Collab)
&& managed_features::project_overrides_legacy_subagent_opt_out(&config_layer_stack)
{
configured_features.enable(Feature::Collab);
}
let features = ManagedFeatures::from_configured_with_warnings(
configured_features,
feature_requirements,
&mut startup_warnings,
)?;
if let Some(FeatureToml::Config(guardian)) = cfg
.features
.as_ref()
.and_then(|features| features.guardianv2.as_ref())
&& guardian.async_classifier_mode.is_some()
{
startup_warnings.push(
"`[features.guardianv2].async_classifier_mode` is deprecated and ignored. Async classification always uses conversations; remove this setting from config.toml or profile configuration files.".to_owned(),
);
}
let mcp_enterprise_managed_auth = McpEnterpriseManagedAuthConfig::resolve(
&config_layer_stack,
cfg.mcp_enterprise_managed_auth.as_ref(),
&cfg.mcp_servers,
features.enabled(Feature::UseXaa),
)?;
let non_prefixed_mcp_tool_servers = if features.enabled(Feature::NonPrefixedMcpToolNames) {
cfg.features
.as_ref()
.and_then(|features| features.non_prefixed_mcp_tool_names.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => config.server_names.clone(),
})
} else {
None
};
let respect_system_proxy = features.enabled(Feature::RespectSystemProxy);
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
let allow_mxc =
cfg.windows.as_ref().and_then(|windows| windows.allow_mxc) != Some(false);
if !allow_mxc && resolve_windows_sandbox_mode(&cfg) == Some(WindowsSandboxModeToml::Mxc) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"windows.sandbox = \"mxc\" is not allowed when windows.allow_mxc = false",
));
}
let PreparedWindowsSandboxConfig {
mode: windows_sandbox_mode,
sandbox_type: windows_sandbox_type,
level: windows_sandbox_level,
} = prepare_windows_sandbox_config(
resolve_windows_sandbox_mode(&cfg),
WindowsSandboxLevel::from_features(&features),
&mut constrained_windows_sandbox_mode,
&mut startup_warnings,
)?;
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
use std::env;
match cwd {
None => {
tracing::info!("cwd not set, using current dir");
env::current_dir()?
}
Some(p) if p.is_absolute() => p,
Some(p) => {
// Resolve relative path against the current working directory.
tracing::info!("cwd is relative, resolving against current dir");
let mut current = env::current_dir()?;
current.push(p);
current
}
}
}))?;
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
.collect();
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 });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
&cfg,
sandbox_mode,
);
let requirements_toml = config_layer_stack.requirements_toml();
let persisted_permission_profile = persisted_permission_profile.filter(|profile| {
sandbox_mode.is_none()
&& permission_profile.is_none()
&& default_permissions_override.is_none()
&& persisted_permission_profile_id.is_none()
&& requirements_toml.allowed_permission_profiles.is_none()
// Executor-owned enforcement and materialized grants are not portable
// thread defaults. Reject the original mode before preserving denials.
&& !matches!(profile, PermissionProfile::External { .. })
&& !profile.file_system_sandbox_policy().entries.iter().any(|entry| {
entry.access.can_read()
&& matches!(entry.path, FileSystemPath::Path { .. } | FileSystemPath::GlobPattern { .. })
})
&& permission_profile_catalog::permission_profile_satisfies_requirements(
&config_layer_stack,
profile,
)
});
let persisted_permission_profile_id = if sandbox_mode.is_some()
|| permission_profile.is_some()
|| default_permissions_override.is_some()
{
None
} else {
persisted_permission_profile_id.as_deref()
};
let permission_path_context = ConfigPathContext::new(
PathConvention::native(),
Some(PathUri::from_abs_path(&resolved_cwd)),
AbsolutePathBufGuard::home_directory()
.and_then(|home| PathUri::from_host_native_path(home).ok()),
);
let effective_permission_selection = resolve_effective_permission_selection(
cfg.permissions.as_ref(),
default_permissions_override.as_deref(),
persisted_permission_profile_id.map(|profile_id| (profile_id, &permission_path_context)),
cfg.default_permissions.as_deref(),
requirements_toml,
&mut startup_warnings,
)?;
if effective_permission_selection.has_profiles()
&& !matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Legacy)
)
&& effective_permission_selection.selected_profile_id.is_none()
&& !effective_permission_selection.requirements_force_profile_selection
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"config defines `[permissions]` profiles but does not set `default_permissions`",
));
}
let mut memories = cfg.memories.clone().unwrap_or_default();
// Fresh profiles have no V1 context to preserve or warm up before using V2.
// Keep explicit selections and transition opt-outs, and fail closed on IO errors.
if features.enabled(Feature::MemoryTool)
&& memories.version.is_none()
&& memories.dual_write != Some(false)
&& matches!(
tokio::fs::try_exists(codex_home.join(MemoryVersion::V1.directory_name())).await,
Ok(false)
)
{
memories.version = Some(MemoryVersion::V2);
}
let memories_config: MemoriesConfig = memories.into();
let memories_root = codex_home.join(memories_config.version.directory_name());
let profiles_are_active = effective_permission_selection.profiles_are_active(
default_permissions_override.as_deref(),
permission_config_syntax,
);
let prefer_mxc = features.enabled(Feature::PreferMxc)
&& config_allows_mxc(
&constrained_windows_sandbox_mode,
&effective_permission_selection,
profiles_are_active,
permission_profile.as_ref(),
network_requirements.as_ref(),
&cfg,
enable_network_proxy,
)?
&& codex_sandboxing::windows_mxc_available();
let local_windows_sandbox_type =
resolve_windows_sandbox_type(windows_sandbox_type, prefer_mxc);
let legacy_windows_sandbox_level = windows_sandbox_level_for_legacy_checks(
local_windows_sandbox_type,
windows_sandbox_level,
);
let explicit_permission_profile_mode = effective_permission_selection
.persisted_profile_id_was_provided
|| default_permissions_override.is_some()
|| matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
);
let custom_permission_profiles = permission_profile_catalog_from_permissions(
&config_layer_stack,
effective_permission_selection.profiles.as_ref(),
&permission_path_context,
)?
.into_iter()
.filter(|profile| !is_builtin_permission_profile_name(&profile.id))
.collect();
let using_implicit_builtin_profile = !effective_permission_selection
.persisted_profile_id_was_provided
&& permission_config_syntax.is_none()
&& effective_permission_selection.selected_profile_id.is_none();
let should_seed_legacy_workspace_roots = effective_permission_selection
.selected_profile_id
.is_none()
&& matches!(
permission_config_syntax,
None | Some(PermissionConfigSyntax::Legacy)
);
let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots
&& cfg
.sandbox_workspace_write
.as_ref()
.is_some_and(|sandbox_workspace_write| {
!sandbox_workspace_write.writable_roots.is_empty()
});
let workspace_roots_explicit = workspace_roots_override.is_some()
|| !requested_additional_writable_roots.is_empty()
|| legacy_workspace_roots_explicit;
let mut workspace_roots = match workspace_roots_override {
Some(workspace_roots) => workspace_roots,
None => {
let mut workspace_roots = vec![resolved_cwd.clone()];
workspace_roots.extend(requested_additional_writable_roots.clone());
if should_seed_legacy_workspace_roots
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
{
workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
}
workspace_roots
}
};
dedupe_absolute_paths(&mut workspace_roots);
let (
configured_network_proxy_config,
mut permission_profile,
mut file_system_sandbox_policy,
mut active_permission_profile,
mut profile_workspace_roots,
) = if let Some(permission_profile) = permission_profile {
let (file_system_sandbox_policy, _network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let configured_network_proxy_config =
if profile_allows_configured_network_proxy(&permission_profile)
&& profiles_are_active
{
// PermissionProfile carries the active network sandbox bit, not the configured
// proxy/allowlist policy. Keep that config so active profiles can round-trip
// without broadening network behavior.
let default_permissions = effective_permission_selection
.selected_profile_id
.unwrap_or_else(|| {
default_builtin_permission_profile_name(
&active_project,
legacy_windows_sandbox_level,
)
});
network_proxy_config_for_profile_selection(
effective_permission_selection.profiles.as_ref(),
default_permissions,
)?
} else {
NetworkProxyConfig::default()
};
(
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
None,
Vec::new(),
)
} else if profiles_are_active {
let default_permissions = effective_permission_selection
.selected_profile_id
.unwrap_or_else(|| {
default_builtin_permission_profile_name(
&active_project,
legacy_windows_sandbox_level,
)
});
let builtin_workspace_write_settings = if using_implicit_builtin_profile {
cfg.sandbox_workspace_write.as_ref().map(|settings| WorkspaceWriteSettings {
writable_roots: settings.writable_roots.iter().map(PathUri::from_abs_path).collect(),
network_access: settings.network_access,
exclude_tmpdir_env_var: settings.exclude_tmpdir_env_var,
exclude_slash_tmp: settings.exclude_slash_tmp,
})
} else {
None
};
let configured_network_proxy_config = network_proxy_config_for_profile_selection(
effective_permission_selection.profiles.as_ref(),
default_permissions,
)?;
let CompiledPermissionProfile {
permission_profile,
workspace_roots: configured_workspace_roots,
} = compile_permission_profile(
effective_permission_selection.profiles.as_ref(),
default_permissions,
&permission_path_context,
builtin_workspace_write_settings.as_ref(),
&mut startup_warnings,
)?;
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
let active_permission_profile = if using_implicit_builtin_profile
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
&& cfg.sandbox_workspace_write.is_some()
{
// The implicit built-in profile preserves legacy
// `[sandbox_workspace_write]` customizations, but explicitly
// selecting `:workspace` intentionally ignores those legacy
// settings. Do not advertise a re-selectable active profile
// when doing so would lose roots, network, or tmp settings.
None
} else {
let selected_profile_extends = cfg
.permissions
.as_ref()
.and_then(|permissions| permissions.entries.get(default_permissions))
.and_then(|profile| profile.extends.clone());
Some(ActivePermissionProfile {
id: default_permissions.to_string(),
extends: selected_profile_extends,
})
};
(
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
active_permission_profile,
configured_workspace_roots,
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
// No named `[permissions]` profile is active, but permissions
// should still flow through the canonical profile representation.
// Derive the old `sandbox_mode` defaults as a profile first, then
// keep a legacy-compatible projection only for the remaining code
// paths that still speak `SandboxPolicy`.
let mut permission_profile = cfg
.derive_permission_profile(
sandbox_mode,
legacy_windows_sandbox_level,
Some(&active_project),
Some(&constrained_permission_profile),
)
.await;
// The legacy-derived profiles above are expected to be
// representable as `SandboxPolicy`. This guard keeps the old safe
// fallback behavior if future changes make this branch derive a
// profile with split-only filesystem semantics, such as root write
// with carveouts or writes that are not expressible as
// workspace-write roots.
if let Err(err) = permission_profile.to_legacy_sandbox_policy(resolved_cwd.as_path()) {
tracing::warn!(
error = %err,
"derived permission profile cannot be represented as a legacy sandbox policy; falling back to read-only"
);
permission_profile = PermissionProfile::read_only();
}
let (file_system_sandbox_policy, _network_sandbox_policy) =
permission_profile.to_runtime_permissions();
(
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
None,
Vec::new(),
)
};
if let Some(saved) = persisted_permission_profile {
let (mut saved_file_system, saved_network) = saved.to_runtime_permissions();
saved_file_system.preserve_deny_read_restrictions_from(&file_system_sandbox_policy);
let restored = PermissionProfile::from_runtime_permissions_with_enforcement(
saved.enforcement(),
&saved_file_system,
saved_network,
);
if permission_profile_catalog::permission_profile_satisfies_requirements(
&config_layer_stack,
&restored,
) {
permission_profile = restored;
file_system_sandbox_policy = saved_file_system;
active_permission_profile = None;
profile_workspace_roots.clear();
}
}
let prepared_network = PreparedNetworkConfig::from_inputs(NetworkConfigInputs {
configured_proxy: configured_network_proxy_config,
feature_enabled: enable_network_proxy,
features: cfg.features.as_ref(),
candidate_permission_profile: &permission_profile,
credential_broker_base_url: cfg.openai_base_url.as_deref(),
});
if cfg.approval_policy == Some(AskForApproval::UnlessTrusted) {
return Err(std::io::Error::new(
ErrorKind::InvalidData,
UnsupportedUntrustedApprovalPolicyError,
));
}
let approval_policy_was_explicit =
approval_policy_override.is_some() || cfg.approval_policy.is_some();
let mut approval_policy = approval_policy_override
.or(cfg.approval_policy)
.unwrap_or_else(|| {
if active_project.is_trusted() {
AskForApproval::OnRequest
} else if active_project.is_untrusted() {
AskForApproval::UnlessTrusted
} else {
AskForApproval::default()
}
});
if !approval_policy_was_explicit
&& let Err(err) = constrained_approval_policy.can_set(&approval_policy)
{
tracing::warn!(
error = %err,
"default approval policy is disallowed by requirements; falling back to required default"
);
approval_policy = constrained_approval_policy.value();
}
let approvals_reviewer_was_explicit =
approvals_reviewer_override.is_some() || cfg.approvals_reviewer.is_some();
let mut approvals_reviewer = approvals_reviewer_override
.or(cfg.approvals_reviewer)
.unwrap_or(ApprovalsReviewer::User);
let requested_approvals_reviewer = approvals_reviewer;
if !approvals_reviewer_was_explicit
&& let Err(err) = constrained_approvals_reviewer.can_set(&approvals_reviewer)
{
tracing::warn!(
error = %err,
"default approvals reviewer is disallowed by requirements; falling back to required default"
);
approvals_reviewer = constrained_approvals_reviewer.value();
}
let web_search_mode =
resolve_web_search_mode(&cfg, &features).unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg);
let experimental_request_user_input_enabled =
resolve_experimental_request_user_input_enabled(&cfg);
let update_plan_enabled = resolve_update_plan_enabled(&cfg);
let tool_registry = ToolRegistryConfig {
error_on_tool_collisions: cfg
.features
.as_ref()
.and_then(|features| features.tool_registry.as_ref())
.and_then(|config| config.error_on_tool_collisions)
.unwrap_or_default(),
turn_metadata_includes_tool_info: cfg
.features
.as_ref()
.and_then(|features| features.tool_registry.as_ref())
.and_then(|config| config.turn_metadata_includes_tool_info)
.unwrap_or_default(),
};
let code_mode = resolve_code_mode_config(&cfg);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
let token_budget = resolve_token_budget_config(&cfg, &features)?;
let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?;
let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?;
let sleep_tool_mode = cfg
.features
.as_ref()
.and_then(|features| features.sleep_tool.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => config.mode,
})
.unwrap_or_default();
let runtime_auth_mode = cfg
.features
.as_ref()
.and_then(|features| features.runtime_agent_instances.as_ref())
.and_then(|feature| match feature {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => config.mode,
})
.unwrap_or_default();
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
let agent_roles =
load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings).await?;
let openai_base_url = cfg
.openai_base_url
.clone()
.filter(|value| !value.is_empty());
let mut model_providers =
merge_configured_model_providers(built_in_model_providers(openai_base_url), cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidData, message))?;
if let Some(provider) = model_providers.get_mut(GCP_PROVIDER_ID) {
apply_default_gcp_auth(provider, &resolved_cwd);
}
let model_provider_id = config_layer_stack.required_model_provider().map(str::to_string)
.or(model_provider)
.or(cfg.model_provider)
.unwrap_or_else(|| "openai".to_string());
let model_provider = model_providers
.get(&model_provider_id)
.ok_or_else(|| {
let message = if model_provider_id == LEGACY_OLLAMA_CHAT_PROVIDER_ID {
OLLAMA_CHAT_PROVIDER_REMOVED_ERROR.to_string()
} else {
format!("Model provider `{model_provider_id}` not found")
};
std::io::Error::new(std::io::ErrorKind::NotFound, message)
})?
.clone();
let shell_environment_policy = ShellEnvironmentPolicy::from(cfg.shell_environment_policy);
let allow_login_shell = cfg.allow_login_shell.unwrap_or(true);
let history = cfg.history.unwrap_or_default();
if multi_agent_v2.max_concurrent_threads_per_session == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.multi_agent_v2.max_concurrent_threads_per_session must be at least 1",
));
}
validate_multi_agent_v2_wait_timeout(
"features.multi_agent_v2.min_wait_timeout_ms",
multi_agent_v2.min_wait_timeout_ms,
)?;
validate_multi_agent_v2_wait_timeout(
"features.multi_agent_v2.max_wait_timeout_ms",
multi_agent_v2.max_wait_timeout_ms,
)?;
validate_multi_agent_v2_wait_timeout(
"features.multi_agent_v2.default_wait_timeout_ms",
multi_agent_v2.default_wait_timeout_ms,
)?;
if multi_agent_v2.min_wait_timeout_ms > multi_agent_v2.max_wait_timeout_ms {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.multi_agent_v2.min_wait_timeout_ms must be at most features.multi_agent_v2.max_wait_timeout_ms",
));
}
if multi_agent_v2.default_wait_timeout_ms < multi_agent_v2.min_wait_timeout_ms {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.multi_agent_v2.default_wait_timeout_ms must be at least features.multi_agent_v2.min_wait_timeout_ms",
));
}
if multi_agent_v2.default_wait_timeout_ms > multi_agent_v2.max_wait_timeout_ms {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"features.multi_agent_v2.default_wait_timeout_ms must be at most features.multi_agent_v2.max_wait_timeout_ms",
));
}
validate_multi_agent_v2_tool_namespace(multi_agent_v2.tool_namespace.as_deref())?;
let agents_enabled = cfg
.agents
.as_ref()
.and_then(|agents| agents.enabled)
.unwrap_or(true);
let agent_max_threads = cfg
.agents
.as_ref()
.and_then(|agents| agents.max_concurrent_threads_per_session);
if agent_max_threads == Some(0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"agents.max_concurrent_threads_per_session must be at least 1",
));
}
let agent_max_depth = cfg
.agents
.as_ref()
.and_then(|agents| agents.max_depth)
.unwrap_or(DEFAULT_AGENT_MAX_DEPTH);
let agent_default_subagent_model = cfg
.agents
.as_ref()
.and_then(|agents| agents.default_subagent_model.clone());
let agent_default_subagent_reasoning_effort = cfg
.agents
.as_ref()
.and_then(|agents| agents.default_subagent_reasoning_effort.clone());
let agent_interrupt_message_enabled = cfg
.agents
.as_ref()
.and_then(|agents| agents.interrupt_message)
.unwrap_or(true);
let background_terminal_max_timeout = cfg
.background_terminal_max_timeout
.unwrap_or(DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS)
.max(MIN_EMPTY_YIELD_TIME_MS);
let thread_unload_delay =
Duration::from_secs(cfg.thread_unload_delay_secs.unwrap_or(/*default*/ 60));
if std::time::Instant::now()
.checked_add(thread_unload_delay)
.is_none()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"thread_unload_delay_secs is too large",
));
}
let ghost_snapshot = {
let mut config = GhostSnapshotConfig::default();
if let Some(ghost_snapshot) = cfg.ghost_snapshot.as_ref()
&& let Some(ignore_over_bytes) = ghost_snapshot.ignore_large_untracked_files
{
config.ignore_large_untracked_files = if ignore_over_bytes > 0 {
Some(ignore_over_bytes)
} else {
None
};
}
if let Some(ghost_snapshot) = cfg.ghost_snapshot.as_ref()
&& let Some(threshold) = ghost_snapshot.ignore_large_untracked_dirs
{
config.ignore_large_untracked_dirs =
if threshold > 0 { Some(threshold) } else { None };
}
if let Some(ghost_snapshot) = cfg.ghost_snapshot.as_ref()
&& let Some(disable_warnings) = ghost_snapshot.disable_warnings
{
config.disable_warnings = disable_warnings;
}
config
};
let forced_chatgpt_workspace_id = cfg
.forced_chatgpt_workspace_id
.clone()
.map(codex_config::config_toml::ForcedChatgptWorkspaceIds::into_vec)
.map(|values| {
values
.into_iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect::<Vec<_>>()
})
.filter(|values| !values.is_empty());
let forced_login_method = cfg.forced_login_method;
let model = model.or(cfg.model);
let notices = cfg.notice.unwrap_or_default();
let service_tier = match service_tier_override {
Some(Some(service_tier)) => Some(service_tier),
Some(None) => Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
None => cfg.service_tier,
};
let service_tier = service_tier.and_then(|service_tier| {
match ServiceTier::from_request_value(&service_tier) {
Some(ServiceTier::Fast) => features
.enabled(Feature::FastMode)
.then(|| ServiceTier::Fast.request_value().to_string()),
Some(ServiceTier::Flex) => Some(ServiceTier::Flex.request_value().to_string()),
None if service_tier == "ultrafast" => features
.enabled(Feature::UltrafastMode)
.then_some(service_tier),
None => Some(service_tier),
}
});
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
let model_instructions_path = cfg.model_instructions_file.as_ref();
let file_base_instructions = Self::try_read_non_empty_file(
fs,
model_instructions_path,
"model instructions file",
)
.await?;
let base_instructions = base_instructions
.or(file_base_instructions)
.or(cfg.instructions.clone());
let base_instructions_provenance = base_instructions
.as_ref()
.map(|_| BaseInstructionsProvenance::Custom);
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
let include_permissions_instructions = cfg.include_permissions_instructions.unwrap_or(true);
let include_apps_instructions = cfg.include_apps_instructions.unwrap_or(true);
let include_collaboration_mode_instructions =
cfg.include_collaboration_mode_instructions.unwrap_or(true);
let include_skill_instructions = cfg
.skills
.as_ref()
.and_then(|skills| skills.include_instructions)
.unwrap_or(true);
let skill_max_context_tokens = cfg
.skills
.as_ref()
.and_then(|skills| skills.max_context_tokens);
let include_environment_context = cfg.include_environment_context.unwrap_or(true);
let include_environment_context_time = cfg.include_environment_context_time.unwrap_or(true);
let guardian_policy_config =
guardian_policy_config_from_requirements(config_layer_stack.requirements_toml())
.or_else(|| {
cfg.auto_review
.as_ref()
.and_then(|auto_review| normalize_guardian_policy_config(
auto_review.policy.as_deref(),
))
});
let guardian_extra_policy = normalize_guardian_policy_config(
config_layer_stack
.requirements_toml()
.guardian_extra_policy
.as_deref(),
)
.or_else(|| {
cfg.auto_review.as_ref().and_then(|auto_review| {
normalize_guardian_policy_config(auto_review.extra_policy.as_deref())
})
});
let guardian_transcript_mode = cfg
.features
.as_ref()
.and_then(|features| features.guardianv2.as_ref())
.and_then(|feature| match feature {
FeatureToml::Config(config) => config.transcript_mode,
FeatureToml::Enabled(_) => None,
})
.unwrap_or_default();
let guardian_sync_elastic_budget = cfg.features.as_ref()
.and_then(|features| features.guardianv2.as_ref())
.and_then(|feature| match feature {
FeatureToml::Config(guardian) if guardian.sync_elastic_budgeting == Some(true) => Some(guardian),
FeatureToml::Config(_) | FeatureToml::Enabled(_) => None,
})
.map(|guardian| {
let sections = guardian.sync_elastic_sections.as_ref().filter(|sections| !sections.is_empty())
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData,
"sync_elastic_sections is required when sync_elastic_budgeting is enabled"))?;
let mut decoded_sections = BTreeMap::new();
for (name, limits) in sections {
let section = codex_guardian_context::ElasticSection::deserialize(name.as_str().into_deserializer())
.map_err(|error: serde::de::value::Error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut section_limits = codex_guardian_context::ElasticSectionLimits::new(
limits.min_total_section_tokens,
limits.max_section_entry_tokens,
).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
if let Some(maximum) = limits.max_total_section_tokens {
section_limits = section_limits.with_max_total_section_tokens(maximum)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
}
if decoded_sections.insert(section, section_limits).is_some() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
format!("duplicate sync_elastic_sections section: {name}")));
}
}
let policy = codex_guardian_context::ElasticBudgetConfig { sections: decoded_sections };
policy.validate().map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
Ok::<_, std::io::Error>(policy)
}).transpose()?;
let guardian_policy_template = cfg
.auto_review
.as_ref()
.and_then(|auto_review| {
normalize_guardian_policy_config(
auto_review.experimental_policy_template.as_deref(),
)
});
let guardian_conversation_history_prompt = cfg.auto_review.as_ref().and_then(|auto_review| {
normalize_guardian_policy_config(
auto_review.experimental_conversation_history_prompt.as_deref(),
)
});
let guardian_conversation_history_max_output_tokens = cfg
.auto_review
.as_ref()
.and_then(|auto_review| auto_review.conversation_history_max_output_tokens);
let guardian_denial_reconsideration_prompt = cfg.auto_review.as_ref().and_then(|auto_review| {
normalize_guardian_policy_config(
auto_review.experimental_denial_reconsideration_prompt.as_deref(),
)
});
if guardian_denial_reconsideration_prompt
.as_ref()
.is_some_and(|prompt| prompt.len() > crate::context::GuardianDenialReconsideration::MAX_BYTES)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"auto_review.experimental_denial_reconsideration_prompt exceeds 1,000 UTF-8 bytes",
));
}
let personality = personality.or(cfg.personality);
let experimental_compact_prompt_path = cfg.experimental_compact_prompt_file.as_ref();
let file_compact_prompt = Self::try_read_non_empty_file(
fs,
experimental_compact_prompt_path,
"experimental compact prompt file",
)
.await?;
let compact_prompt = compact_prompt.or(file_compact_prompt);
let review_model = override_review_model.or(cfg.review_model);
let check_for_update_on_startup = cfg.check_for_update_on_startup.unwrap_or(true);
let model_catalog = load_model_catalog(cfg.model_catalog_json.clone())?;
let log_dir = cfg
.log_dir
.as_ref()
.map(AbsolutePathBuf::to_path_buf)
.unwrap_or_else(|| codex_home.join("log").to_path_buf());
let sqlite_home_env = resolve_sqlite_home_env(&resolved_cwd);
requirements::push_sqlite_home_env_override_warning(
configured_sqlite_home.as_ref(),
sqlite_home_env.as_deref(),
config_layer_stack.requirements().sqlite_home.as_ref(),
&mut startup_warnings,
);
let sqlite_home = cfg
.sqlite_home
.as_ref()
.cloned()
.or(sqlite_home_env)
.unwrap_or_else(|| codex_home.clone());
let enforce_delete_on_nfs =
unsafe_enforce_sqlite_journal_mode.is_some_and(|required| required.value);
let mut sqlite = codex_state::SqliteConfig::from_sqlite_home_with_nfs_enforcement(
sqlite_home,
enforce_delete_on_nfs,
);
let requirements = config_layer_stack.requirements_toml();
if cfg.log_dir.is_some() && requirements.sqlite_home.is_none() {
sqlite = sqlite.with_local_logs_home(log_dir.clone());
}
let original_permission_profile = permission_profile.clone();
apply_requirement_constrained_value(
"approval_policy",
approval_policy,
&mut constrained_approval_policy,
&mut startup_warnings,
)?;
if let Some(Sourced {
value: filesystem_requirements,
source: filesystem_requirements_source,
}) = filesystem_requirements.as_ref()
&& !filesystem_requirements.deny_read.is_empty()
{
let requirement_source = filesystem_requirements_source.clone();
constrained_permission_profile
.value
.add_validator(move |permission_profile| {
validate_permission_profile_for_deny_read(
permission_profile,
&requirement_source,
)
})
.map_err(std::io::Error::from)?;
}
apply_requirement_constrained_value(
"approvals_reviewer",
approvals_reviewer,
&mut constrained_approvals_reviewer,
&mut startup_warnings,
)?;
let permission_profile_was_constrained = apply_requirement_constrained_value(
"permission_profile",
permission_profile,
&mut constrained_permission_profile,
&mut startup_warnings,
)?;
if permission_profile_was_constrained
&& sandbox_mode_requirement_for_permission_profile(&original_permission_profile)
== SandboxModeRequirement::DangerFullAccess
&& constrained_permission_profile.get() == &PermissionProfile::read_only()
&& constrained_approval_policy.value() == AskForApproval::Never
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode.",
));
}
if permission_profile_was_constrained {
// The selected profile no longer describes the effective
// permissions after requirements forced a fallback.
active_permission_profile = None;
profile_workspace_roots.clear();
}
apply_requirement_constrained_value(
"web_search_mode",
web_search_mode,
&mut constrained_web_search_mode,
&mut startup_warnings,
)?;
let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
let network_permission_profile = constrained_permission_profile.get().clone();
let network = build_network_proxy_spec(
prepared_network.configured_proxy,
network_requirements,
&network_permission_profile,
&shell_environment_policy.r#set,
&features,
cfg.features.as_ref(),
)?;
let mut helper_readable_roots: Vec<_> = std::env::var_os("PATH")
.as_deref()
.and_then(|path| permissions::active_arg0_helper_dir(&codex_home, path))
.into_iter()
.collect();
if features.enabled(Feature::MemoryTool) && memories_config.use_memories {
helper_readable_roots.push(memories_root);
}
let effective_permission_profile = constrained_permission_profile.value.get().clone();
let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) =
effective_permission_profile.to_runtime_permissions();
if effective_permission_profile != original_permission_profile {
effective_file_system_sandbox_policy
.preserve_deny_read_restrictions_from(&file_system_sandbox_policy);
}
let managed_deny_read_policy = filesystem_requirements
.as_ref()
.filter(|Sourced { value, .. }| !value.deny_read.is_empty())
.map(|Sourced { value, .. }| -> std::io::Result<_> {
let mut policy = FileSystemSandboxPolicy::restricted(Vec::new());
value.apply_to_policy(&mut policy, codex_utils_path_uri::PathConvention::native())?;
Ok(Arc::new(policy))
})
.transpose()?;
if let Some(managed_deny_read_policy) = managed_deny_read_policy.as_ref() {
effective_file_system_sandbox_policy
.preserve_deny_read_restrictions_from(managed_deny_read_policy);
}
let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy
.with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots);
let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
effective_permission_profile.enforcement(),
&effective_file_system_sandbox_policy,
effective_network_sandbox_policy,
);
constrained_permission_profile
.value
.set(effective_permission_profile)
.map_err(std::io::Error::from)?;
if let Some(Sourced {
source: requirement_source,
..
}) = filesystem_requirements.as_ref()
&& let Some(managed_file_system_policy) = managed_deny_read_policy.as_ref()
{
let cwd = PathUri::from_abs_path(&resolved_cwd);
let user_home_dir = PathUri::from_host_native_path("~").ok();
let temporary_directories = std::env::var_os("TMPDIR")
.filter(|path| !path.is_empty())
.and_then(|path| AbsolutePathBuf::from_absolute_path(PathBuf::from(path)).ok())
.map(PathUri::from)
.into_iter()
.collect::<Vec<_>>();
let context = FileSystemSandboxPolicyContext {
cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
user_home_dir: user_home_dir.as_ref(),
temporary_directories: Some(&temporary_directories),
};
let validator = DenyReadValidator::new(managed_file_system_policy, &context)
.map_err(std::io::Error::other)?;
let requirement_source = requirement_source.clone();
constrained_permission_profile
.value
.add_validator(move |permission_profile| {
let mut file_system_policy = permission_profile.file_system_sandbox_policy();
// Preserve the native conversion boundary before the shared URI checks.
// Mandatory entries are Deny entries, so their identity stays unchanged.
file_system_policy.entries.retain_mut(|entry| {
if entry.access.can_read()
&& let FileSystemPath::Path { path } = &mut entry.path
{
let Ok(native_path) = path.to_abs_path() else {
return false;
};
*path = PathUri::from(native_path);
}
true
});
let context = FileSystemSandboxPolicyContext {
cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
user_home_dir: user_home_dir.as_ref(),
temporary_directories: Some(&temporary_directories),
};
validator.validate(&file_system_policy, &context).map_err(|violation| {
let candidate = match violation {
DenyReadViolation::MissingRequiredDeny => "missing managed deny".to_string(),
DenyReadViolation::ReadablePath(path) => path.to_abs_path().map_or_else(
|_| path.to_string(),
|path| path.to_string_lossy().into_owned(),
),
};
ConstraintError::InvalidValue {
field_name: "permissions.filesystem",
candidate,
allowed: "all managed deny_read restrictions".to_string(),
requirement_source: requirement_source.clone(),
}
})
})
.map_err(std::io::Error::from)?;
}
let permission_profile_state = PermissionProfileState::from_constrained_active_profile(
constrained_permission_profile.value,
active_permission_profile,
profile_workspace_roots,
)
.map_err(std::io::Error::from)?;
let otel = otel::resolve_config(cfg.otel.unwrap_or_default(), &mut startup_warnings);
let mut config = Self {
plugins: cfg.plugins,
prefer_mxc,
model,
daybreak_enabled: cfg.daybreak.unwrap_or(false),
service_tier,
review_model,
model_context_window: cfg.model_context_window,
model_auto_compact_token_limit: cfg.model_auto_compact_token_limit,
model_auto_compact_token_limit_scope: cfg
.model_auto_compact_token_limit_scope
.unwrap_or_default(),
model_post_turn_compact_threshold_percent: cfg
.model_post_turn_compact_threshold_percent
.unwrap_or_default(),
async_compaction_threshold_percent,
model_provider_id,
model_provider,
cwd: resolved_cwd,
workspace_roots: workspace_roots.clone(),
workspace_roots_explicit,
startup_warnings,
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
permission_profile_state,
managed_deny_read_policy,
workspace_roots,
network,
allow_login_shell,
shell_environment_policy,
windows_sandbox_mode,
windows_sandbox_type,
},
explicit_permission_profile_mode,
custom_permission_profiles,
approvals_reviewer: constrained_approvals_reviewer.value(),
requested_approvals_reviewer: (requested_approvals_reviewer
!= constrained_approvals_reviewer.value())
.then_some(requested_approvals_reviewer),
enforce_residency: enforce_residency.value,
notify: cfg.notify,
base_instructions,
base_instructions_provenance,
personality,
developer_instructions,
compact_prompt,
include_permissions_instructions,
include_apps_instructions,
include_collaboration_mode_instructions,
include_skill_instructions,
skill_max_context_tokens,
cloud_skill_enabled,
orchestrator_mcp_enabled,
include_environment_context,
include_environment_context_time,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: match cli_auth_credentials_store {
Some(required) => required.value,
None => resolve_cli_auth_credentials_store_mode(
cfg.cli_auth_credentials_store.unwrap_or_default(),
env!("CARGO_PKG_VERSION"),
),
},
mcp_servers,
non_prefixed_mcp_tool_servers,
mcp_enterprise_managed_auth,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
cfg.mcp_oauth_credentials_store.unwrap_or_default(),
env!("CARGO_PKG_VERSION"),
),
mcp_oauth_callback_port: cfg.mcp_oauth_callback_port,
mcp_oauth_callback_url: cfg.mcp_oauth_callback_url.clone(),
mcp_optional_startup_grace: cfg
.mcp_optional_startup_grace_ms
.map(Duration::from_millis)
.unwrap_or(DEFAULT_OPTIONAL_MCP_STARTUP_GRACE),
model_providers,
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(AGENTS_MD_MAX_BYTES),
project_doc_fallback_filenames: cfg
.project_doc_fallback_filenames
.unwrap_or_default()
.into_iter()
.filter_map(|name| {
let trimmed = name.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
})
.collect(),
tool_output_token_limit: cfg.tool_output_token_limit,
agents_enabled,
agent_max_threads,
agent_default_subagent_model,
agent_default_subagent_reasoning_effort,
agent_max_depth,
agent_roles,
max_goal_token_budget: cfg
.goals
.as_ref()
.and_then(|goals| goals.max_goal_token_budget)
.map(|max_goal_token_budget| {
i64::try_from(max_goal_token_budget.get()).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"goals.max_goal_token_budget exceeds the maximum supported token budget",
)
})
})
.transpose()?,
memories: memories_config,
agent_interrupt_message_enabled,
codex_home,
sqlite,
log_dir,
config_layer_stack,
application_network_policy: Default::default(),
application_auth_route_config: None,
history,
ephemeral: ephemeral.unwrap_or_default(),
allow_persistent_dynamic_tool_updates: false,
extra_config: None,
bypass_hook_trust,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
codex_self_exe,
codex_linux_sandbox_exe,
main_execve_wrapper_exe: None,
zsh_path: None,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
show_raw_agent_reasoning: cfg
.show_raw_agent_reasoning
.or(show_raw_agent_reasoning)
.unwrap_or(false),
guardian_policy_config,
guardian_extra_policy,
guardian_policy_template,
guardian_transcript_mode,
guardian_sync_elastic_budget,
guardian_conversation_history_prompt,
guardian_denial_reconsideration_prompt,
guardian_conversation_history_max_output_tokens,
guardian_review_timeout: cfg
.auto_review
.as_ref()
.and_then(|auto_review| auto_review.timeout_ms)
.map_or(Some(codex_guardian_reviewer::REVIEW_TIMEOUT), |timeout_ms| {
(timeout_ms != 0).then(|| Duration::from_millis(u64::from(timeout_ms)))
}),
guardian_circuit_break_action: cfg
.auto_review
.as_ref()
.and_then(|auto_review| auto_review.circuit_break_action)
.unwrap_or_default(),
model_reasoning_effort: cfg.model_reasoning_effort,
plan_mode_reasoning_effort: cfg.plan_mode_reasoning_effort,
model_reasoning_summary: cfg.model_reasoning_summary,
model_catalog,
model_verbosity: cfg.model_verbosity,
chatgpt_base_url_is_explicit,
chatgpt_base_url: cfg
.chatgpt_base_url
.unwrap_or_else(|| DEFAULT_CHATGPT_BASE_URL.to_string()),
respect_system_proxy,
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
responses_api_metadata: cfg.responses_api_metadata.unwrap_or_default(),
realtime_audio: cfg
.audio
.map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig {
microphone: audio.microphone,
microphone_channel: audio.microphone_channel,
speaker: audio.speaker,
}),
experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_base_url,
experimental_realtime_webrtc_call_base_url: cfg
.experimental_realtime_webrtc_call_base_url,
experimental_realtime_ws_model: cfg.experimental_realtime_ws_model,
realtime: cfg
.realtime
.map_or_else(RealtimeConfig::default, |realtime| {
let defaults = RealtimeConfig::default();
RealtimeConfig {
version: realtime.version.unwrap_or(defaults.version),
session_type: realtime.session_type.unwrap_or(defaults.session_type),
transport: realtime.transport.unwrap_or(defaults.transport),
voice: realtime.voice,
}
}),
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
experimental_thread_store: thread_store_config(cfg.experimental_thread_store),
forced_chatgpt_workspace_id,
forced_login_method,
web_search_mode: constrained_web_search_mode.value,
web_search_config,
experimental_request_user_input_enabled,
update_plan_enabled,
tool_registry,
code_mode,
background_terminal_max_timeout,
thread_unload_delay,
ghost_snapshot,
multi_agent_v2,
token_budget,
token_budget_startup_config: None,
rollout_budget,
current_time_reminder,
sleep_tool_mode,
runtime_auth_mode,
features,
runtime_feature_defaults: BTreeMap::new(),
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
.unwrap_or(false),
active_project,
notices,
check_for_update_on_startup,
disable_paste_burst: cfg
.tui
.as_ref()
.and_then(|tui| tui.disable_paste_burst)
.or(cfg.disable_paste_burst)
.unwrap_or(false),
analytics_enabled: cfg.analytics.as_ref().and_then(|a| a.enabled),
feedback_enabled: cfg
.feedback
.as_ref()
.and_then(|feedback| feedback.enabled)
.unwrap_or(true),
tool_suggest,
tui_notifications: cfg
.tui
.as_ref()
.map(|t| t.notification_settings.clone())
.unwrap_or_default(),
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
tui_effects: cfg.tui.as_ref().map(|t| t.effects).unwrap_or_default(),
tui_rendering: cfg.tui.as_ref().map(|t| t.rendering).unwrap_or_default(),
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
tui_show_server_version_notice: cfg
.tui
.as_ref()
.map(|t| t.show_server_version_notice)
.unwrap_or(true),
tui_auto_recap: cfg.tui.as_ref().map(|t| t.auto_recap).unwrap_or(/*default*/ true),
model_availability_nux: cfg
.tui
.as_ref()
.map(|t| t.model_availability_nux.clone())
.unwrap_or_default(),
tui_question_esc_back: cfg.tui.as_ref().map(|t| t.question_esc_back).unwrap_or(true),
tui_vim_mode_default: cfg
.tui
.as_ref()
.map(|t| t.vim_mode_default)
.unwrap_or(false),
tui_raw_output_mode: cfg
.tui
.as_ref()
.map(|t| t.raw_output_mode)
.unwrap_or(false),
tui_fullscreen_transcript: cfg
.tui
.as_ref()
.is_none_or(|tui| tui.fullscreen_transcript),
tui_mouse_scroll_speed: cfg.tui.as_ref().and_then(|tui| tui.mouse_scroll_speed),
tui_copy_on_select: cfg
.tui
.as_ref()
.map(|tui| tui.copy_on_select)
.unwrap_or_default(),
tui_copy_on_select_clear_selection: cfg
.tui
.as_ref()
.is_some_and(|tui| tui.copy_on_select_clear_selection),
tui_right_click_paste: cfg
.tui
.as_ref()
.map(|tui| tui.right_click_paste)
.unwrap_or_default(),
tui_alternate_screen: cfg
.tui
.as_ref()
.map(|t| t.alternate_screen)
.unwrap_or_default(),
tui_status_line: cfg.tui.as_ref().and_then(|t| t.status_line.clone()),
tui_status_line_use_colors: cfg
.tui
.as_ref()
.map(|t| t.status_line_use_colors)
.unwrap_or(true),
tui_terminal_title: cfg.tui.as_ref().and_then(|t| t.terminal_title.clone()),
tui_theme: cfg.tui.as_ref().and_then(|t| t.theme.clone()),
tui_pet: cfg.tui.as_ref().and_then(|t| t.pet.clone()),
tui_pet_anchor: cfg
.tui
.as_ref()
.map(|t| t.pet_anchor)
.unwrap_or_default(),
tui_session_picker_view: cfg
.tui
.as_ref()
.and_then(|t| t.session_picker_view)
.unwrap_or_default(),
tui_agents_overview_grouping: cfg.tui.as_ref().map(|t| t.agents_overview_grouping).unwrap_or_default(),
tui_resume_cwd: cfg.tui.as_ref().and_then(|t| t.resume_cwd),
terminal_resize_reflow,
tui_keymap: cfg
.tui
.as_ref()
.map(|t| t.keymap.clone())
.unwrap_or_default(),
tui_quick_dials: cfg
.tui
.as_ref()
.map(|t| t.quick_dials.clone())
.unwrap_or_default(),
otel,
};
if let Some(message) = config.credential_masking_warning(config.permissions.permission_profile()) {
tracing::warn!("{message}");
config.startup_warnings.push(message.to_string());
}
Ok(config)
})
.await
}
/// If `path` is `Some`, attempts to read the file at the given path and
/// returns its contents as a trimmed `String`. If the file is empty, or
/// is `Some` but cannot be read, returns an `Err`.
async fn try_read_non_empty_file(
fs: &dyn ExecutorFileSystem,
path: Option<&AbsolutePathBuf>,
context: &str,
) -> std::io::Result<Option<String>> {
let Some(path) = path else {
return Ok(None);
};
let path_uri = PathUri::from_abs_path(path);
let contents = fs
.read_file_text(&path_uri, ReadFileOptions::default(), /*sandbox*/ None)
.await
.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read {context} {}: {e}", path.display()),
)
})?;
let s = contents.trim().to_string();
if s.is_empty() {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{context} is empty: {}", path.display()),
))
} else {
Ok(Some(s))
}
}
pub fn set_windows_sandbox_enabled(&mut self, value: bool) {
self.permissions.windows_sandbox_mode = if value {
Some(WindowsSandboxModeToml::Unelevated)
} else if matches!(
self.permissions.windows_sandbox_mode,
Some(WindowsSandboxModeToml::Unelevated)
) {
None
} else {
self.permissions.windows_sandbox_mode
};
}
pub fn set_windows_elevated_sandbox_enabled(&mut self, value: bool) {
self.permissions.windows_sandbox_mode = if value {
Some(WindowsSandboxModeToml::Elevated)
} else if matches!(
self.permissions.windows_sandbox_mode,
Some(WindowsSandboxModeToml::Elevated)
) {
None
} else {
self.permissions.windows_sandbox_mode
};
}
pub fn managed_network_requirements_enabled(&self) -> bool {
!matches!(
self.permissions.permission_profile(),
PermissionProfile::Disabled
) && self.config_layer_stack.requirements().network.is_some()
}
/// Resolves a named permission profile from effective config and managed requirements.
pub fn resolve_permission_profile(
&self,
profile_name: &str,
) -> std::io::Result<PermissionProfileToml> {
let cfg =
ConfigToml::try_from(self.config_layer_stack.effective_config()).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"failed to read effective config for selected permission profile: {err}"
),
)
})?;
let permissions = merge_managed_permission_profiles(
cfg.permissions.as_ref(),
self.config_layer_stack.requirements_toml(),
)?
.unwrap_or_default();
permissions::resolve_permission_profile(&permissions, profile_name)
}
pub fn network_proxy_spec_for_active_permission_profile(
&self,
active_permission_profile: &ActivePermissionProfile,
permission_profile: &PermissionProfile,
) -> std::io::Result<Option<NetworkProxySpec>> {
let profile_allows_network_proxy =
profile_allows_configured_network_proxy(permission_profile);
let cfg =
ConfigToml::try_from(self.config_layer_stack.effective_config()).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"failed to read effective config for selected permission profile: {err}"
),
)
})?;
let configured_network_proxy_config = if profile_allows_network_proxy {
let permissions = merge_managed_permission_profiles(
cfg.permissions.as_ref(),
self.config_layer_stack.requirements_toml(),
)?;
network_proxy_config_for_profile_selection(
permissions.as_ref(),
active_permission_profile.id.as_str(),
)?
} else {
NetworkProxyConfig::default()
};
let prepared_network = PreparedNetworkConfig::from_inputs(NetworkConfigInputs {
configured_proxy: configured_network_proxy_config,
feature_enabled: profile_allows_network_proxy
&& self.features.enabled(Feature::NetworkProxy),
features: cfg.features.as_ref(),
candidate_permission_profile: permission_profile,
credential_broker_base_url: cfg.openai_base_url.as_deref(),
});
build_network_proxy_spec(
prepared_network.configured_proxy,
self.config_layer_stack.requirements().network.clone(),
permission_profile,
&self.permissions.shell_environment_policy.r#set,
&self.features,
cfg.features.as_ref(),
)
}
pub(crate) fn credential_masking_warning(
&self,
permission_profile: &PermissionProfile,
) -> Option<&'static str> {
if !self.features.enabled(Feature::CredentialMasking) {
return None;
}
// The session bypasses the configured proxy for unsandboxed execution,
// even when managed requirements keep the proxy spec enabled.
if matches!(permission_profile, PermissionProfile::Disabled) {
return Some(
"Credential masking is inactive because sandboxing is disabled. Commands will receive unmasked credentials. Select a sandboxed permission profile to use credential masking.",
);
}
if self
.permissions
.network
.as_ref()
.is_some_and(NetworkProxySpec::credential_broker_enabled)
{
return None;
}
if self
.permissions
.network
.as_ref()
.is_some_and(NetworkProxySpec::enabled)
{
Some(
"Credential masking is inactive because shell environment overrides contain conflicting case-insensitive provider keys. Commands will receive unmasked credentials. Remove the conflicting entries from `shell_environment_policy.set`.",
)
} else {
Some(
"Credential masking is inactive because the network proxy is disabled. Commands will receive unmasked credentials. Enable `features.network_proxy` or configure managed network requirements to use credential masking.",
)
}
}
pub fn bundled_skills_enabled(&self) -> bool {
codex_config::bundled_skills_enabled_from_stack(&self.config_layer_stack)
}
/// Returns whether effective requirements allow selecting a concrete profile.
pub fn is_permission_profile_allowed(
&self,
profile_id: &str,
permission_profile: &PermissionProfile,
) -> bool {
permission_profile_is_allowed(&self.config_layer_stack, profile_id, permission_profile)
}
}
fn guardian_policy_config_from_requirements(
requirements_toml: &ConfigRequirementsToml,
) -> Option<String> {
normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref())
}
fn merge_managed_permission_profiles(
configured_permissions: Option<&PermissionsToml>,
requirements_toml: &ConfigRequirementsToml,
) -> std::io::Result<Option<PermissionsToml>> {
let managed_profiles = requirements_toml
.permissions
.as_ref()
.map(|permissions| &permissions.profiles)
.filter(|profiles| !profiles.is_empty());
let Some(managed_profiles) = managed_profiles else {
return Ok(configured_permissions.cloned());
};
let mut merged_permissions = configured_permissions.cloned().unwrap_or_default();
for (profile_id, managed_profile) in managed_profiles {
if merged_permissions.entries.contains_key(profile_id) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name"
),
));
}
merged_permissions
.entries
.insert(profile_id.clone(), managed_profile.clone());
}
Ok(Some(merged_permissions))
}
fn resolve_effective_permission_selection<'a>(
configured_profiles: Option<&PermissionsToml>,
default_permissions_override: Option<&'a str>,
persisted_profile_id: Option<(&'a str, &ConfigPathContext)>,
configured_default_profile_id: Option<&'a str>,
requirements_toml: &'a ConfigRequirementsToml,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<EffectivePermissionSelection<'a>> {
let profiles = merge_managed_permission_profiles(configured_profiles, requirements_toml)?;
validate_user_permission_profile_names(profiles.as_ref())?;
validate_required_permission_profile_catalog(requirements_toml, profiles.as_ref())?;
let valid_persisted_profile_id = persisted_profile_id.and_then(|(profile_id, context)| {
compile_permission_profile(
profiles.as_ref(),
profile_id,
context,
/*workspace_write*/ None,
&mut Vec::new(),
)
.is_ok()
.then_some(profile_id)
});
let selected_profile_id = resolve_default_permissions(
default_permissions_override.or(valid_persisted_profile_id),
configured_default_profile_id,
requirements_toml,
startup_warnings,
)?;
Ok(EffectivePermissionSelection {
profiles,
selected_profile_id,
persisted_profile_id_was_provided: default_permissions_override.is_none()
&& valid_persisted_profile_id.is_some(),
requirements_force_profile_selection: requirements_toml
.allowed_permission_profiles
.is_some(),
})
}
fn resolve_default_permissions<'a>(
default_permissions_override: Option<&'a str>,
configured_default_permissions: Option<&'a str>,
requirements_toml: &'a ConfigRequirementsToml,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<Option<&'a str>> {
let selected_permissions = default_permissions_override.or(configured_default_permissions);
let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref()
else {
return Ok(selected_permissions);
};
let Some(fallback_permissions) = requirements_toml
.default_permissions
.as_deref()
.or_else(|| implicit_default_permissions(allowed_permission_profiles))
else {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
"requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`",
));
};
match selected_permissions {
None => Ok(Some(fallback_permissions)),
Some(selected_permissions)
if is_permission_allowed(allowed_permission_profiles, selected_permissions) =>
{
Ok(Some(selected_permissions))
}
Some(selected_permissions) => {
startup_warnings.push(format!(
"Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`."
));
Ok(Some(fallback_permissions))
}
}
}
fn validate_required_permission_profile_catalog(
requirements_toml: &ConfigRequirementsToml,
available_permissions: Option<&PermissionsToml>,
) -> std::io::Result<()> {
let is_known_profile = |profile_id: &str| {
is_builtin_permission_profile_name(profile_id)
|| available_permissions
.as_ref()
.is_some_and(|permissions| permissions.entries.contains_key(profile_id))
};
let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref()
else {
if requirements_toml.default_permissions.is_some() {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
"requirements.toml default_permissions requires allowed_permission_profiles",
));
}
return Ok(());
};
for profile_id in allowed_permission_profiles.keys() {
if !is_known_profile(profile_id) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"requirements.toml allowed_permission_profiles refers to undefined profile `{profile_id}`"
),
));
}
}
let Some(default_permissions) = requirements_toml
.default_permissions
.as_deref()
.or_else(|| implicit_default_permissions(allowed_permission_profiles))
else {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
"requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`",
));
};
if !is_permission_allowed(allowed_permission_profiles, default_permissions) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"requirements.toml default_permissions `{default_permissions}` must be allowed by allowed_permission_profiles"
),
));
}
Ok(())
}
fn implicit_default_permissions(
allowed_permission_profiles: &BTreeMap<String, bool>,
) -> Option<&'static str> {
(is_permission_allowed(allowed_permission_profiles, BUILT_IN_WORKSPACE_PROFILE)
&& is_permission_allowed(allowed_permission_profiles, BUILT_IN_READ_ONLY_PROFILE))
.then_some(BUILT_IN_WORKSPACE_PROFILE)
}
fn is_permission_allowed(
allowed_permission_profiles: &BTreeMap<String, bool>,
profile_id: &str,
) -> bool {
allowed_permission_profiles
.get(profile_id)
.copied()
.unwrap_or(false)
}
fn normalize_guardian_policy_config(value: Option<&str>) -> Option<String> {
value.and_then(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
}
/// Returns the path to the Codex configuration directory, which can be
/// specified by the `CODEX_HOME` environment variable. If not set, defaults to
/// `~/.codex`.
///
/// - If `CODEX_HOME` is set, the value must exist and be a directory. The
/// value will be canonicalized and this function will Err otherwise.
/// - If `CODEX_HOME` is not set, this function does not verify that the
/// directory exists.
pub fn find_codex_home() -> std::io::Result<AbsolutePathBuf> {
codex_utils_home_dir::find_codex_home()
}
/// Returns the path to the folder where Codex logs are stored. Does not verify
/// that the directory exists.
pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
Ok(cfg.log_dir.clone())
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "config_loader_tests.rs"]
mod config_loader_tests;