use crate::agents_md::AgentsMdManager; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; 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::resolve_windows_sandbox_private_desktop; use codex_config::CloudRequirementsLoader; use codex_config::ConfigLayerSource; use codex_config::ConfigLayerStack; use codex_config::ConfigLayerStackOrdering; use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::ConstrainedWithSource; use codex_config::FeatureRequirementsToml; use codex_config::McpServerIdentity; 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::config_toml::ConfigLockfileToml; 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::profile_toml::ConfigProfile; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::History; use codex_config::types::McpServerConfig; use codex_config::types::McpServerDisabledReason; use codex_config::types::McpServerTransportConfig; use codex_config::types::MemoriesConfig; use codex_config::types::ModelAvailabilityNuxConfig; use codex_config::types::Notice; use codex_config::types::OAuthCredentialsStoreMode; 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::PluginsConfigInput; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; use codex_features::AppsMcpPathOverrideConfigToml; 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_git_utils::resolve_root_git_project_for_trust; use codex_login::AuthManagerConfig; use codex_mcp::McpConfig; use codex_memories_read::memory_root; 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_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; 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::PermissionProfile; use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use rmcp::model::ElicitationCapability; use rmcp::model::FormElicitationCapability; use rmcp::model::UrlElicitationCapability; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE; use crate::config::permissions::apply_network_proxy_feature_config; use crate::config::permissions::builtin_permission_profile; use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::compile_permission_profile_workspace_roots; use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; use crate::config_lock::config_without_lock_controls; use crate::config_lock::lock_layer_from_config; use crate::config_lock::read_config_lock_from_path; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; use toml_edit::DocumentMut; pub(crate) mod agent_roles; pub mod edit; mod managed_features; mod network_proxy_spec; mod otel; mod permissions; mod resolved_permission_profile; #[cfg(test)] mod schema; 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 use network_proxy_spec::NetworkProxySpec; pub use network_proxy_spec::StartedNetworkProxy; pub(crate) use permissions::resolve_permission_profile; pub(crate) use resolved_permission_profile::PermissionProfileState; const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200; const DEFAULT_IGNORE_LARGE_UNTRACKED_FILES: i64 = 10 * 1024 * 1024; /// 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, pub ignore_large_untracked_dirs: Option, 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 = 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; 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; pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; 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 { let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?; let trimmed = raw.trim(); if trimmed.is_empty() { return None; } let path = PathBuf::from(trimmed); if path.is_absolute() { Some(path) } else { Some(resolved_cwd.join(path)) } } 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::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, /// Constrained permission profile plus its selected profile identity, if /// the profile came from a built-in or named config profile. permission_profile_state: PermissionProfileState, /// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots` /// entries in the permission profile are materialized against these roots. workspace_roots: Vec, /// Effective network configuration applied to all spawned processes. pub network: Option, /// 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, /// Whether the final Windows sandboxed child should run on a private desktop. pub windows_sandbox_private_desktop: bool, } impl Permissions { /// Build permissions from the constrained values required for a minimal /// in-process configuration. pub fn from_approval_and_profile( approval_policy: Constrained, permission_profile: Constrained, ) -> ConstraintResult { Ok(Self { approval_policy, permission_profile_state: PermissionProfileState::from_constrained_legacy( permission_profile, )?, workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, windows_sandbox_private_desktop: true, }) } 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 this pair directly. pub fn set_permission_profile_from_session_snapshot( &mut self, permission_profile: PermissionProfile, active_permission_profile: Option, ) -> ConstraintResult<()> { self.set_permission_profile_from_session_snapshot_with_profile_workspace_roots( permission_profile, active_permission_profile, Vec::new(), ) } pub fn set_permission_profile_from_session_snapshot_with_profile_workspace_roots( &mut self, permission_profile: PermissionProfile, active_permission_profile: Option, profile_workspace_roots: Vec, ) -> ConstraintResult<()> { self.permission_profile_state.set_active_permission_profile( permission_profile, active_permission_profile, profile_workspace_roots, ) } /// 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, permission_profile: Constrained, active_permission_profile: Option, ) -> ConstraintResult<()> { self.replace_permission_profile_from_session_snapshot_with_profile_workspace_roots( permission_profile, active_permission_profile, Vec::new(), ) } pub fn replace_permission_profile_from_session_snapshot_with_profile_workspace_roots( &mut self, permission_profile: Constrained, active_permission_profile: Option, profile_workspace_roots: Vec, ) -> ConstraintResult<()> { self.permission_profile_state = PermissionProfileState::from_constrained_active_profile( permission_profile, active_permission_profile, profile_workspace_roots, )?; 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<()> { self.permission_profile_state .can_set_legacy_permission_profile(permission_profile) } pub fn set_workspace_roots(&mut self, workspace_roots: Vec) { 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) -> &[AbsolutePathBuf] { self.permission_profile_state.profile_workspace_roots() } fn materialized_permission_profile(&self) -> PermissionProfile { self.permission_profile() .clone() .materialize_project_roots_with_workspace_roots(&self.workspace_roots) } /// Effective runtime permissions after config requirements and runtime /// workspace-root materialization have been applied. pub fn effective_permission_profile(&self) -> PermissionProfile { self.materialized_permission_profile() } /// Named profile selected by config, if the current profile has one. pub fn active_permission_profile(&self) -> Option { 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.materialized_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.materialized_permission_profile(); let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy(); compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, permission_profile.network_sandbox_policy(), 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.permission_profile_state .can_set_legacy_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.permission_profile_state .set_legacy_permission_profile(permission_profile)?; Ok(()) } /// Set permissions from the canonical profile. pub fn set_permission_profile( &mut self, permission_profile: PermissionProfile, ) -> ConstraintResult<()> { self.permission_profile_state .set_legacy_permission_profile(permission_profile) } } // 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, } } /// 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 { /// Provenance for how this [`Config`] was derived (merged layers + enforced /// requirements). pub config_layer_stack: ConfigLayerStack, /// Warnings collected during config load that should be shown on startup. pub startup_warnings: Vec, /// Optional override of model selection. pub model: Option, /// Effective service tier request id preference for new turns. pub service_tier: Option, /// Model used specifically for review sessions. pub review_model: Option, /// Size of the context window for the model, in tokens. pub model_context_window: Option, /// Token usage threshold triggering auto-compaction of conversation history. pub model_auto_compact_token_limit: Option, /// 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, /// Optionally specify the personality of the model pub personality: Option, /// Effective permission configuration for shell tool execution. pub permissions: Permissions, /// 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, /// 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>, /// 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, /// User-provided instructions from AGENTS.md. pub user_instructions: Option, /// Base instructions override. pub base_instructions: Option, /// Developer instructions override injected as a separate message. pub developer_instructions: Option, /// 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, /// Whether to inject the `` developer block. pub include_permissions_instructions: bool, /// Whether to inject the `` developer block. pub include_apps_instructions: bool, /// Whether to inject the `` developer block. pub include_collaboration_mode_instructions: bool, /// Whether to inject the `` developer block. pub include_skill_instructions: bool, /// Whether to inject the `` user block. pub include_environment_context: bool, /// Compact prompt override. pub compact_prompt: Option, /// 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>, /// 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, /// Show startup tooltips in the TUI welcome screen. pub show_tooltips: 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, /// Start the TUI in raw scrollback mode for copy-friendly transcript output. pub tui_raw_output_mode: bool, /// 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>, /// 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>, /// Syntax highlighting theme override (kebab-case name). pub tui_theme: Option, /// Pet id preselected by the terminal pet picker. pub tui_pet: Option, /// 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, /// 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, /// 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, /// 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>, /// 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, /// 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, /// Combined provider map (defaults plus user-defined providers). pub model_providers: HashMap, /// Maximum number of bytes to include from an AGENTS.md project doc file. pub project_doc_max_bytes: usize, /// Additional filenames to try when looking for project-level docs. pub project_doc_fallback_filenames: Vec, /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option, /// Maximum number of agent threads that can be open concurrently. pub agent_max_threads: Option, /// Maximum runtime in seconds for agent job workers before they are failed. pub agent_job_max_runtime_seconds: Option, /// Whether to record a model-visible message when an agent turn is interrupted. pub agent_interrupt_message_enabled: bool, /// Maximum nesting depth allowed for spawned agent threads. pub agent_max_depth: i32, /// User-defined role declarations keyed by role name. pub agent_roles: BTreeMap, /// 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, /// Directory where Codex stores the SQLite state DB. pub sqlite_home: PathBuf, /// Directory where Codex writes log files (defaults to `$CODEX_HOME/log`). pub log_dir: PathBuf, /// Directory where Codex writes effective session config lock files. pub config_lock_export_dir: Option, /// Whether config lock replay ignores Codex version drift between the /// lock metadata and the regenerated lock. pub config_lock_allow_codex_version_mismatch: bool, /// Whether config lock creation saves values resolved from the model /// catalog/session configuration. pub config_lock_save_fields_resolved_from_model_catalog: bool, /// Effective config lock used for strict replay validation. pub config_lock_toml: Option>, /// 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, /// 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, /// 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, /// Path to the `codex-execve-wrapper` executable used for shell /// escalation. This cannot be set in the config file: it must be set in /// code via [`ConfigOverrides`]. pub main_execve_wrapper_exe: Option, /// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution. pub zsh_path: Option, /// Value to use for `reasoning.effort` when making a request using the /// Responses API. pub model_reasoning_effort: Option, /// 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, /// 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, /// Optional override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option, /// Optional full model catalog loaded from `model_catalog_json`. /// When set, this replaces the bundled catalog for the current process. pub model_catalog: Option, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option, /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, /// Optional path override for the host-owned apps MCP server. pub apps_mcp_path_override: Option, /// 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, /// Experimental / do not use. Selects the realtime websocket model/snapshot /// used for the `Op::RealtimeConversation` connection. pub experimental_realtime_ws_model: Option, /// 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, /// 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, /// 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, /// Experimental / do not use. When set, app-server fetches thread-scoped /// config from a remote service at this endpoint. pub experimental_thread_config_endpoint: Option, /// 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>, /// When set, restricts the login mechanism users may use. pub forced_login_method: Option, /// Explicit or feature-derived web search mode. pub web_search_mode: Constrained, /// Additional parameters for the web search tool when it is enabled. pub web_search_config: Option, /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. /// Default: `300000` (5 minutes). pub background_terminal_max_timeout: u64, /// 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, /// Centralized feature flags; source of truth for feature gating. pub features: ManagedFeatures, /// When `true`, suppress warnings about unstable (under development) features. pub suppress_unstable_features_warning: bool, /// The active profile name used to derive this `Config` (if any). pub active_profile: Option, /// 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 analytics across Codex product surfaces in this machine. /// Voluntarily left as Optional because the default value might depend on the client. pub analytics_enabled: Option, /// When `false`, disables feedback collection across Codex product surfaces. /// 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, 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_enabled: bool, pub usage_hint_text: Option, pub root_agent_usage_hint_text: Option, pub subagent_usage_hint_text: Option, pub hide_spawn_agent_metadata: bool, pub non_code_mode_only: bool, } impl Default for MultiAgentV2Config { fn default() -> Self { Self { max_concurrent_threads_per_session: DEFAULT_MULTI_AGENT_V2_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_enabled: true, usage_hint_text: None, root_agent_usage_hint_text: None, subagent_usage_hint_text: None, hide_spawn_agent_metadata: false, non_code_mode_only: false, } } } #[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 forced_chatgpt_workspace_id(&self) -> Option> { self.forced_chatgpt_workspace_id.clone() } fn chatgpt_base_url(&self) -> String { self.chatgpt_base_url.clone() } } #[derive(Clone, Default)] pub struct ConfigBuilder { codex_home: Option, cli_overrides: Option>, harness_overrides: Option, loader_overrides: Option, strict_config: bool, cloud_requirements: CloudRequirementsLoader, thread_config_loader: Option>, fallback_cwd: Option, } 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_requirements(mut self, cloud_requirements: CloudRequirementsLoader) -> Self { self.cloud_requirements = cloud_requirements; self } pub fn thread_config_loader( mut self, thread_config_loader: Arc, ) -> Self { self.thread_config_loader = Some(thread_config_loader); self } pub fn fallback_cwd(mut self, fallback_cwd: Option) -> Self { self.fallback_cwd = fallback_cwd; self } pub async fn build(self) -> std::io::Result { // Keep the large config-loading future off small runtime thread stacks. Box::pin(self.build_inner()).await } async fn build_inner(self) -> std::io::Result { let Self { codex_home, cli_overrides, harness_overrides, loader_overrides, strict_config, cloud_requirements, thread_config_loader, fallback_cwd, } = 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, Some(cwd), &cli_overrides, ConfigLoadOptions { loader_overrides, strict_config, }, cloud_requirements, thread_config_loader .as_deref() .unwrap_or(&codex_config::NoopThreadConfigLoader), ) .await?; let merged_toml = config_layer_stack.effective_config(); // Note that each layer in ConfigLayerStack should have resolved // relative paths to absolute paths based on the parent folder of the // respective config file, so we should be safe to deserialize without // AbsolutePathBufGuard here. let config_toml: ConfigToml = match merged_toml.try_into() { Ok(config_toml) => config_toml, Err(err) => { if let Some(config_error) = codex_config::first_layer_config_error::( &config_layer_stack, codex_config::CONFIG_TOML_FILE, ) .await { return Err(codex_config::io_error_from_config_error( std::io::ErrorKind::InvalidData, config_error, Some(err), )); } return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err)); } }; let config_lock_settings = config_toml .debug .as_ref() .and_then(|debug| debug.config_lockfile.as_ref()); if let Some(config_lock_load_path) = config_lock_settings.and_then(|config_lock| config_lock.load_path.as_ref()) { let allow_codex_version_mismatch = config_lock_settings .and_then(|config_lock| config_lock.allow_codex_version_mismatch) .unwrap_or(false); let save_fields_resolved_from_model_catalog = config_lock_settings .and_then(|config_lock| config_lock.save_fields_resolved_from_model_catalog) .unwrap_or(true); let lockfile_toml = read_config_lock_from_path(config_lock_load_path).await?; let expected_lock_config = lockfile_toml.clone(); let lock_layer = lock_layer_from_config(config_lock_load_path, &lockfile_toml)?; let lock_config_toml = config_without_lock_controls(&lockfile_toml.config); let lock_config_layer_stack = ConfigLayerStack::new( vec![lock_layer], config_layer_stack.requirements().clone(), config_layer_stack.requirements_toml().clone(), )?; let mut config = Config::load_config_with_layer_stack( LOCAL_FS.as_ref(), lock_config_toml, harness_overrides, codex_home, lock_config_layer_stack, ) .await?; config.config_lock_toml = Some(Arc::new(expected_lock_config)); config.config_lock_allow_codex_version_mismatch = allow_codex_version_mismatch; config.config_lock_save_fields_resolved_from_model_catalog = save_fields_resolved_from_model_catalog; return Ok(config); } 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()) } } impl Config { 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(()) } pub fn effective_workspace_roots(&self) -> Vec { let mut workspace_roots = self.workspace_roots.clone(); workspace_roots.extend(self.permissions.profile_workspace_roots().iter().cloned()); dedupe_absolute_paths(&mut workspace_roots); 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(), personality_enabled: self.features.enabled(Feature::Personality), model_supports_reasoning_summaries: self.model_supports_reasoning_summaries, model_catalog: self.model_catalog.clone(), } } /// Build the plugin-manager input from the effective config. pub fn plugins_config_input(&self) -> PluginsConfigInput { PluginsConfigInput::new( self.config_layer_stack.clone(), self.features.enabled(Feature::Plugins), self.features.enabled(Feature::RemotePlugin), self.features.enabled(Feature::PluginHooks), self.chatgpt_base_url.clone(), ) } pub async fn to_mcp_config( &self, plugins_manager: &codex_core_plugins::PluginsManager, ) -> McpConfig { let plugins_input = self.plugins_config_input(); let loaded_plugins = plugins_manager.plugins_for_config(&plugins_input).await; let mut configured_mcp_servers = self.mcp_servers.get().clone(); for plugin in loaded_plugins .plugins() .iter() .filter(|plugin| plugin.is_active()) { let mut plugin_mcp_servers = plugin.mcp_servers.clone(); filter_plugin_mcp_servers_by_requirements( &plugin.config_name, &mut plugin_mcp_servers, self.config_layer_stack.requirements().plugins.as_ref(), ); for (name, plugin_server) in plugin_mcp_servers { configured_mcp_servers.entry(name).or_insert(plugin_server); } } if let Some(mcp_requirements) = self.config_layer_stack.requirements().mcp_servers.as_ref() && mcp_requirements.value.is_empty() { // A present empty allowlist bans configurable MCPs, including plugin MCPs merged // above. filter_mcp_servers_by_requirements(&mut configured_mcp_servers, Some(mcp_requirements)); } McpConfig { chatgpt_base_url: self.chatgpt_base_url.clone(), apps_mcp_path_override: self.apps_mcp_path_override.clone(), codex_home: self.codex_home.to_path_buf(), mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode, mcp_oauth_callback_port: self.mcp_oauth_callback_port, mcp_oauth_callback_url: self.mcp_oauth_callback_url.clone(), skill_mcp_dependency_install_enabled: self .features .enabled(Feature::SkillMcpDependencyInstall), approval_policy: self.permissions.approval_policy.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), client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) { ElicitationCapability { form: Some(FormElicitationCapability::default()), url: Some(UrlElicitationCapability::default()), } } else { // https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities // indicates this should be an empty object. ElicitationCapability::default() }, configured_mcp_servers, plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(), } } pub async fn rebuild_preserving_session_layers( &self, refreshed_config: &Config, ) -> std::io::Result { let mut layers = refreshed_config .config_layer_stack .get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ true, ) .into_iter() .filter(|layer| !is_session_layer(&layer.name)) .cloned() .collect::>(); layers.extend( self.config_layer_stack .get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ true, ) .into_iter() .filter(|layer| is_session_layer(&layer.name)) .cloned(), ); layers.sort_by_key(|layer| layer.name.precedence()); let config_layer_stack = ConfigLayerStack::new( layers, refreshed_config.config_layer_stack.requirements().clone(), refreshed_config .config_layer_stack .requirements_toml() .clone(), )? .with_user_and_project_exec_policy_rules_ignored( refreshed_config .config_layer_stack .ignore_user_and_project_exec_policy_rules(), ); let cfg: ConfigToml = config_layer_stack .effective_config() .try_into() .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; Self::load_config_with_layer_stack( LOCAL_FS.as_ref(), cfg, ConfigOverrides { cwd: Some(self.cwd.to_path_buf()), ..Default::default() }, refreshed_config.codex_home.clone(), config_layer_stack, ) .await } /// 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 { 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 { 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 { 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`, `codex_linux_sandbox_exe`, and /// `main_execve_wrapper_exe`. pub async fn load_with_cli_overrides_and_harness_overrides( cli_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, ) -> std::io::Result { 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 { 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 { 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, ) -> std::io::Result { let config_layer_stack = load_config_layers_state( LOCAL_FS.as_ref(), codex_home, cwd.cloned(), &cli_overrides, options, CloudRequirementsLoader::default(), &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(cfg) } pub fn deserialize_config_toml_with_base( root_value: TomlValue, config_base_dir: &Path, ) -> std::io::Result { // 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); root_value .try_into() .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>, ) -> 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 { let file_contents = std::fs::read_to_string(path)?; let catalog = serde_json::from_str::(&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, ) -> std::io::Result> { model_catalog_json .map(|path| load_catalog_json(&path)) .transpose() } fn filter_mcp_servers_by_requirements( mcp_servers: &mut HashMap, mcp_requirements: Option<&Sourced>>, ) { 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| mcp_server_matches_requirement(requirement, server)); if allowed { server.disabled_reason = None; } else { server.enabled = false; server.disabled_reason = Some(McpServerDisabledReason::Requirements { source: source.clone(), }); } } } fn filter_plugin_mcp_servers_by_requirements( plugin_config_name: &str, mcp_servers: &mut HashMap, plugin_requirements: Option<&Sourced>>, ) { let Some(requirements) = plugin_requirements else { 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| mcp_server_matches_requirement(requirement, server)); if allowed { server.disabled_reason = None; } else { server.enabled = false; server.disabled_reason = Some(McpServerDisabledReason::Requirements { source: source.clone(), }); } } } fn constrain_mcp_servers( mcp_servers: HashMap, mcp_requirements: Option<&Sourced>>, ) -> ConstraintResult>> { 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( field_name: &'static str, configured_value: T, constrained_value: &mut ConstrainedWithSource, startup_warnings: &mut Vec, ) -> std::io::Result 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) } fn mcp_server_matches_requirement( requirement: &McpServerRequirement, server: &McpServerConfig, ) -> bool { match &requirement.identity { McpServerIdentity::Command { command: want_command, } => matches!( &server.transport, McpServerTransportConfig::Stdio { command: got_command, .. } if got_command == want_command ), McpServerIdentity::Url { url: want_url } => matches!( &server.transport, McpServerTransportConfig::StreamableHttp { url: got_url, .. } if got_url == want_url ), } } pub async fn load_global_mcp_servers( codex_home: &Path, ) -> std::io::Result> { // 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 = None; let config_layer_stack = load_config_layers_state( LOCAL_FS.as_ref(), codex_home, cwd, &cli_overrides, LoaderOverrides::default(), CloudRequirementsLoader::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}"))) } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AgentRoleConfig { /// Human-facing role documentation used in spawn tool guidance. /// Required for loaded user-defined roles after deprecated/new metadata precedence resolves. pub description: Option, /// Path to a role-specific config layer. pub config_file: Option, /// Candidate nicknames for agents spawned with this role. pub nickname_candidates: Option>, } 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::().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 layers = config_layer_stack.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ); if layers.is_empty() { 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::().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) -> 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, sandbox_mode: Option, } fn resolve_permission_config_syntax( config_layer_stack: &ConfigLayerStack, cfg: &ConfigToml, sandbox_mode_override: Option, profile_sandbox_mode: Option, ) -> Option { if sandbox_mode_override.is_some() { return Some(PermissionConfigSyntax::Legacy); } let session_flags_select_profiles = config_layer_stack .get_layers( ConfigLayerStackOrdering::HighestPrecedenceFirst, /*include_disabled*/ false, ) .into_iter() .find(|layer| matches!(layer.name, ConfigLayerSource::SessionFlags)) .and_then(|layer| { layer .config .clone() .try_into::() .ok() }) .is_some_and(|selection| selection.default_permissions.is_some()); if session_flags_select_profiles { return Some(PermissionConfigSyntax::Profiles); } if profile_sandbox_mode.is_some() { return Some(PermissionConfigSyntax::Legacy); } let mut selection = None; for layer in config_layer_stack.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ) { let Ok(layer_selection) = layer.config.clone().try_into::() 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 } }) } fn apply_managed_filesystem_constraints( file_system_sandbox_policy: &mut FileSystemSandboxPolicy, filesystem_constraints: &codex_config::FilesystemConstraints, ) { for deny_read in &filesystem_constraints.deny_read { let deny_entry = if deny_read.contains_glob() { codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::GlobPattern { pattern: deny_read.as_str().to_string(), }, access: codex_protocol::permissions::FileSystemAccessMode::None, } } else { let Ok(path) = AbsolutePathBuf::try_from(deny_read.as_str()) else { continue; }; codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path }, access: codex_protocol::permissions::FileSystemAccessMode::None, } }; if !file_system_sandbox_policy .entries .iter() .any(|existing| existing == &deny_entry) { file_system_sandbox_policy.entries.push(deny_entry); } } } /// Optional overrides for user configuration (e.g., from CLI flags). #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { pub model: Option, pub review_model: Option, pub cwd: Option, pub approval_policy: Option, pub approvals_reviewer: Option, pub sandbox_mode: Option, pub permission_profile: Option, pub default_permissions: Option, pub model_provider: Option, pub service_tier: Option>, pub config_profile: Option, pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, pub main_execve_wrapper_exe: Option, pub zsh_path: Option, pub base_instructions: Option, pub developer_instructions: Option, pub personality: Option, pub compact_prompt: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, pub ephemeral: Option, pub bypass_hook_trust: Option, /// Additional directories that should be treated as writable roots for this session. pub additional_writable_roots: Vec, /// Explicit runtime workspace roots for this session. When set, this is /// the full runtime root list rather than an additive override. pub workspace_roots: Option>, } fn dedupe_absolute_paths(paths: &mut Vec) { let mut seen = HashSet::new(); paths.retain(|path| seen.insert(path.clone())); } /// Resolves the OSS provider from CLI override, profile config, 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, config_profile: Option, ) -> Option { if let Some(provider) = explicit_provider { // Explicit provider specified (e.g., via --local-provider) Some(provider.to_string()) } else { // Check profile config first, then global config let profile = config_toml.get_config_profile(config_profile).ok(); if let Some(profile) = &profile { // Check if profile has an oss provider if let Some(profile_oss_provider) = &profile.oss_provider { Some(profile_oss_provider.clone()) } // If not then check if the toml has an oss provider else { config_toml.oss_provider.clone() } } 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, config_profile: &ConfigProfile, features: &Features, ) -> Option { if let Some(mode) = config_profile.web_search.or(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, config_profile: &ConfigProfile, ) -> Option { let base = config_toml .tools .as_ref() .and_then(|tools| tools.web_search.as_ref()); let profile = config_profile .tools .as_ref() .and_then(|tools| tools.web_search.as_ref()); match (base, profile) { (None, None) => None, (Some(base), None) => Some(base.clone().into()), (None, Some(profile)) => Some(profile.clone().into()), (Some(base), Some(profile)) => Some(base.merge(profile).into()), } } fn resolve_multi_agent_v2_config( config_toml: &ConfigToml, config_profile: &ConfigProfile, ) -> MultiAgentV2Config { let base = multi_agent_v2_toml_config(config_toml.features.as_ref()); let profile = multi_agent_v2_toml_config(config_profile.features.as_ref()); let default = MultiAgentV2Config::default(); let max_concurrent_threads_per_session = profile .and_then(|config| config.max_concurrent_threads_per_session) .or_else(|| base.and_then(|config| config.max_concurrent_threads_per_session)) .unwrap_or(default.max_concurrent_threads_per_session); let min_wait_timeout_ms = profile .and_then(|config| config.min_wait_timeout_ms) .or_else(|| base.and_then(|config| config.min_wait_timeout_ms)) .unwrap_or(default.min_wait_timeout_ms); let max_wait_timeout_ms = profile .and_then(|config| config.max_wait_timeout_ms) .or_else(|| base.and_then(|config| config.max_wait_timeout_ms)) .unwrap_or(default.max_wait_timeout_ms); let default_wait_timeout_ms = profile .and_then(|config| config.default_wait_timeout_ms) .or_else(|| base.and_then(|config| config.default_wait_timeout_ms)) .unwrap_or(default.default_wait_timeout_ms); let usage_hint_enabled = profile .and_then(|config| config.usage_hint_enabled) .or_else(|| base.and_then(|config| config.usage_hint_enabled)) .unwrap_or(default.usage_hint_enabled); let usage_hint_text = profile .and_then(|config| config.usage_hint_text.as_ref()) .or_else(|| base.and_then(|config| config.usage_hint_text.as_ref())) .cloned() .or(default.usage_hint_text); let root_agent_usage_hint_text = profile .and_then(|config| config.root_agent_usage_hint_text.as_ref()) .or_else(|| base.and_then(|config| config.root_agent_usage_hint_text.as_ref())) .cloned() .or(default.root_agent_usage_hint_text); let subagent_usage_hint_text = profile .and_then(|config| config.subagent_usage_hint_text.as_ref()) .or_else(|| base.and_then(|config| config.subagent_usage_hint_text.as_ref())) .cloned() .or(default.subagent_usage_hint_text); let hide_spawn_agent_metadata = profile .and_then(|config| config.hide_spawn_agent_metadata) .or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata)) .unwrap_or(default.hide_spawn_agent_metadata); let non_code_mode_only = profile .and_then(|config| config.non_code_mode_only) .or_else(|| 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_enabled, usage_hint_text, root_agent_usage_hint_text, subagent_usage_hint_text, hide_spawn_agent_metadata, non_code_mode_only, } } 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 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 apps_mcp_path_override_toml_config( features: Option<&FeaturesToml>, ) -> Option<&AppsMcpPathOverrideConfigToml> { match features?.apps_mcp_path_override.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), } } pub(crate) fn resolve_web_search_mode_for_turn( web_search_mode: &Constrained, permission_profile: &PermissionProfile, ) -> WebSearchMode { let preferred = web_search_mode.value(); if matches!(permission_profile, PermissionProfile::Disabled) && preferred != WebSearchMode::Disabled { for mode in [ WebSearchMode::Live, WebSearchMode::Cached, WebSearchMode::Disabled, ] { if web_search_mode.can_set(&mode).is_ok() { return mode; } } } else { if web_search_mode.can_set(&preferred).is_ok() { return preferred; } for mode in [ WebSearchMode::Cached, WebSearchMode::Live, WebSearchMode::Disabled, ] { if web_search_mode.can_set(&mode).is_ok() { 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(()) } impl Config { #[cfg(test)] async fn load_from_base_config_with_overrides( cfg: ConfigToml, overrides: ConfigOverrides, codex_home: AbsolutePathBuf, ) -> std::io::Result { // 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, cfg: ConfigToml, overrides: ConfigOverrides, codex_home: AbsolutePathBuf, config_layer_stack: ConfigLayerStack, ) -> std::io::Result { // 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))?; // Ensure that every field of ConfigRequirements is applied to the final // Config. let ConfigRequirements { approval_policy: mut constrained_approval_policy, approvals_reviewer: mut constrained_approvals_reviewer, permission_profile: mut constrained_permission_profile, web_search_mode: mut constrained_web_search_mode, allow_managed_hooks_only: _, feature_requirements, managed_hooks: _, mcp_servers, plugins: _, exec_policy: _, enforce_residency, network: network_requirements, filesystem: filesystem_requirements, guardian_policy_config_source: _, } = config_layer_stack.requirements().clone(); let user_instructions = AgentsMdManager::load_global_instructions(Some(&codex_home)) .map(|loaded| loaded.contents); let mut startup_warnings = config_layer_stack .startup_warnings() .unwrap_or_default() .to_vec(); // 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, model_provider, service_tier: service_tier_override, config_profile: config_profile_key, codex_self_exe, codex_linux_sandbox_exe, main_execve_wrapper_exe, zsh_path: zsh_path_override, 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", )); } let active_profile_name = config_profile_key .as_ref() .or(cfg.profile.as_ref()) .cloned(); let config_profile = match active_profile_name.as_ref() { Some(key) => cfg .profiles .get(key) .ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, format!("config profile `{key}` not found"), ) })? .clone(), None => ConfigProfile::default(), }; let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack); let feature_overrides = FeatureOverrides { web_search_request: override_tools_web_search_request, }; let configured_features = Features::from_sources( FeatureConfigSource { features: cfg.features.as_ref(), experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool, }, FeatureConfigSource { features: config_profile.features.as_ref(), experimental_use_unified_exec_tool: config_profile .experimental_use_unified_exec_tool, }, feature_overrides, ); let features = ManagedFeatures::from_configured_with_warnings( configured_features, feature_requirements, &mut startup_warnings, )?; let enable_network_proxy = features.enabled(Feature::NetworkProxy); let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg, &config_profile); 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 = 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, config_profile.sandbox_mode, ); let has_permission_profiles = cfg .permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); let default_permissions = default_permissions_override .as_deref() .or(cfg.default_permissions.as_deref()); validate_user_permission_profile_names(cfg.permissions.as_ref())?; if has_permission_profiles && !matches!( permission_config_syntax, Some(PermissionConfigSyntax::Legacy) ) && default_permissions.is_none() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "config defines `[permissions]` profiles but does not set `default_permissions`", )); } let windows_sandbox_level = match windows_sandbox_mode { Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated, Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken, None => WindowsSandboxLevel::from_features(&features), }; let memories_root = memory_root(&codex_home); std::fs::create_dir_all(&memories_root)?; let internal_writable_roots = vec![memories_root]; let profiles_are_active = default_permissions_override.is_some() || matches!( permission_config_syntax, Some(PermissionConfigSyntax::Profiles) ) || permission_config_syntax.is_none(); let using_implicit_builtin_profile = permission_config_syntax.is_none() && default_permissions.is_none(); let should_seed_legacy_workspace_roots = default_permissions.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 .into_iter() .map(|path| { AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()) }) .collect(), 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 ( mut configured_network_proxy_config, permission_profile, file_system_sandbox_policy, mut active_permission_profile, mut profile_workspace_roots, ) = if let Some(mut permission_profile) = permission_profile { let (mut 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 = default_permissions.unwrap_or_else(|| { default_builtin_permission_profile_name( &active_project, windows_sandbox_level, ) }); network_proxy_config_for_profile_selection( cfg.permissions.as_ref(), default_permissions, )? } else { NetworkProxyConfig::default() }; let materialized_file_system_sandbox_policy = file_system_sandbox_policy .clone() .materialize_project_roots_with_workspace_roots(&workspace_roots); let materialized_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &materialized_file_system_sandbox_policy, network_sandbox_policy, ); let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &materialized_permission_profile, &materialized_file_system_sandbox_policy, network_sandbox_policy, resolved_cwd.as_path(), ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { file_system_sandbox_policy = file_system_sandbox_policy .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &file_system_sandbox_policy, network_sandbox_policy, ); } ( configured_network_proxy_config, permission_profile, file_system_sandbox_policy, None, Vec::new(), ) } else if profiles_are_active { let default_permissions = default_permissions.unwrap_or_else(|| { default_builtin_permission_profile_name(&active_project, windows_sandbox_level) }); let builtin_workspace_write_settings = if using_implicit_builtin_profile { cfg.sandbox_workspace_write.as_ref() } else { None }; let configured_network_proxy_config = network_proxy_config_for_profile_selection( cfg.permissions.as_ref(), default_permissions, )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile_selection( cfg.permissions.as_ref(), default_permissions, builtin_workspace_write_settings, resolved_cwd.as_path(), &mut startup_warnings, )?; let mut configured_workspace_roots = compile_permission_profile_workspace_roots( cfg.permissions.as_ref(), default_permissions, resolved_cwd.as_path(), )?; if using_implicit_builtin_profile && default_permissions == BUILT_IN_WORKSPACE_PROFILE && let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref() { configured_workspace_roots.extend(sandbox_workspace_write.writable_roots.clone()); } dedupe_absolute_paths(&mut configured_workspace_roots); workspace_roots.extend(configured_workspace_roots.iter().cloned()); dedupe_absolute_paths(&mut workspace_roots); file_system_sandbox_policy = file_system_sandbox_policy .with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots); let mut permission_profile = if let Some(permission_profile) = builtin_permission_profile(default_permissions, builtin_workspace_write_settings) { permission_profile } else { PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, network_sandbox_policy, ) }; let materialized_file_system_sandbox_policy = file_system_sandbox_policy .clone() .materialize_project_roots_with_workspace_roots(&workspace_roots); let materialized_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &materialized_file_system_sandbox_policy, network_sandbox_policy, ); let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &materialized_permission_profile, &materialized_file_system_sandbox_policy, network_sandbox_policy, resolved_cwd.as_path(), ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { file_system_sandbox_policy = file_system_sandbox_policy .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &file_system_sandbox_policy, network_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 { Some(ActivePermissionProfile::new(default_permissions)) }; ( 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, config_profile.sandbox_mode, 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 (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); let materialized_file_system_sandbox_policy = permission_profile .clone() .materialize_project_roots_with_workspace_roots(&workspace_roots) .file_system_sandbox_policy(); if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed) && materialized_file_system_sandbox_policy.can_write_path_with_cwd( resolved_cwd.as_path(), resolved_cwd.as_path(), ) && !materialized_file_system_sandbox_policy.has_full_disk_write_access() { // Keep Codex runtime write access while storing the runtime // workspace roots separately on the thread. file_system_sandbox_policy = file_system_sandbox_policy .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &file_system_sandbox_policy, network_sandbox_policy, ); } ( configured_network_proxy_config, permission_profile, file_system_sandbox_policy, None, Vec::new(), ) }; if enable_network_proxy && permission_profile.network_sandbox_policy().is_enabled() { if let Some(network_proxy) = network_proxy_toml_config(cfg.features.as_ref()) { apply_network_proxy_feature_config( &mut configured_network_proxy_config, network_proxy, ); } if let Some(network_proxy) = network_proxy_toml_config(config_profile.features.as_ref()) { apply_network_proxy_feature_config( &mut configured_network_proxy_config, network_proxy, ); } configured_network_proxy_config.network.enabled = true; } let approval_policy_was_explicit = approval_policy_override.is_some() || config_profile.approval_policy.is_some() || cfg.approval_policy.is_some(); let mut approval_policy = approval_policy_override .or(config_profile.approval_policy) .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() || config_profile.approvals_reviewer.is_some() || cfg.approvals_reviewer.is_some(); let mut approvals_reviewer = approvals_reviewer_override .or(config_profile.approvals_reviewer) .or(cfg.approvals_reviewer) .unwrap_or(ApprovalsReviewer::User); 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, &config_profile, &features) .unwrap_or(WebSearchMode::Cached); let web_search_config = resolve_web_search_config(&cfg, &config_profile); let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile); let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) { let base = apps_mcp_path_override_toml_config(cfg.features.as_ref()); let profile = apps_mcp_path_override_toml_config(config_profile.features.as_ref()); profile .and_then(|config| config.path.as_ref()) .or_else(|| base.and_then(|config| config.path.as_ref())) .cloned() .or_else(|| Some("/ps/mcp".to_string())) } else { None }; let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg); let agent_roles = 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 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))?; let model_provider_id = model_provider .or(config_profile.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 = cfg.shell_environment_policy.into(); 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", )); } let agent_max_threads_from_config = cfg.agents.as_ref().and_then(|agents| agents.max_threads); let agent_max_threads = if features.enabled(Feature::MultiAgentV2) { if agent_max_threads_from_config.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.max_threads cannot be set when multi_agent_v2 is enabled", )); } Some( multi_agent_v2 .max_concurrent_threads_per_session .saturating_sub(1), ) } else { let agent_max_threads = agent_max_threads_from_config.or(DEFAULT_AGENT_MAX_THREADS); if agent_max_threads == Some(0) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.max_threads must be at least 1", )); } agent_max_threads }; let agent_max_depth = cfg .agents .as_ref() .and_then(|agents| agents.max_depth) .unwrap_or(DEFAULT_AGENT_MAX_DEPTH); if agent_max_depth < 1 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.max_depth must be at least 1", )); } let agent_job_max_runtime_seconds = cfg .agents .as_ref() .and_then(|agents| agents.job_max_runtime_seconds) .or(DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS); if agent_job_max_runtime_seconds == Some(0) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.job_max_runtime_seconds must be at least 1", )); } if let Some(max_runtime_seconds) = agent_job_max_runtime_seconds && max_runtime_seconds > i64::MAX as u64 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "agents.job_max_runtime_seconds must fit within a 64-bit signed integer", )); } 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 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 use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec); 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::>() }) .filter(|values| !values.is_empty()); let forced_login_method = cfg.forced_login_method; let model = model.or(config_profile.model).or(cfg.model); let mut notices = cfg.notice.unwrap_or_default(); let service_tier = match service_tier_override { Some(Some(service_tier)) => Some(service_tier), Some(None) => { // Preserve explicit standard/clear intent after the nested override // collapses into `Config.service_tier = None`. notices.fast_default_opt_out = Some(true); None } None => config_profile.service_tier.or(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 => 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 = config_profile .model_instructions_file .as_ref() .or(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 developer_instructions = developer_instructions.or(cfg.developer_instructions); let include_permissions_instructions = config_profile .include_permissions_instructions .or(cfg.include_permissions_instructions) .unwrap_or(true); let include_apps_instructions = config_profile .include_apps_instructions .or(cfg.include_apps_instructions) .unwrap_or(true); let include_collaboration_mode_instructions = config_profile .include_collaboration_mode_instructions .or(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 include_environment_context = config_profile .include_environment_context .or(cfg.include_environment_context) .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 personality = personality .or(config_profile.personality) .or(cfg.personality) .or_else(|| { features .enabled(Feature::Personality) .then_some(Personality::Pragmatic) }); let experimental_compact_prompt_path = config_profile .experimental_compact_prompt_file .as_ref() .or(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 zsh_path = zsh_path_override .or(config_profile.zsh_path.map(Into::into)) .or(cfg.zsh_path.map(Into::into)); 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( config_profile .model_catalog_json .clone() .or(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 = cfg .sqlite_home .as_ref() .map(AbsolutePathBuf::to_path_buf) .or_else(|| resolve_sqlite_home_env(&resolved_cwd)) .unwrap_or_else(|| codex_home.to_path_buf()); 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| { let mode = sandbox_mode_requirement_for_permission_profile(permission_profile); match mode { SandboxModeRequirement::ReadOnly | SandboxModeRequirement::WorkspaceWrite => Ok(()), SandboxModeRequirement::DangerFullAccess | SandboxModeRequirement::ExternalSandbox => { Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: format!("{mode:?}"), allowed: "[read-only, workspace-write]".to_string(), requirement_source: requirement_source.clone(), }) } } }) .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 { // 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_requirements, network_requirements_source) = match network_requirements { Some(Sourced { value, source }) => (Some(value), Some(source)), None => (None, None), }; let has_network_requirements = network_requirements.is_some(); let network_permission_profile = constrained_permission_profile.get().clone(); let network = NetworkProxySpec::from_config_and_constraints( configured_network_proxy_config, network_requirements, &network_permission_profile, ) .map_err(|err| { if let Some(source) = network_requirements_source.as_ref() { std::io::Error::new( err.kind(), format!("failed to build managed network proxy from {source}: {err}"), ) } else { err } })?; let network = if has_network_requirements { Some(network) } else { network.enabled().then_some(network) }; let helper_readable_roots = get_readable_roots_required_for_codex_runtime( &codex_home, zsh_path.as_ref(), main_execve_wrapper_exe.as_ref(), ); 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); } if let Some(Sourced { value: filesystem_requirements, .. }) = filesystem_requirements.as_ref() { apply_managed_filesystem_constraints( &mut effective_file_system_sandbox_policy, filesystem_requirements, ); } 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)?; 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 config = Self { model, service_tier, review_model, model_context_window: cfg.model_context_window, model_auto_compact_token_limit: cfg.model_auto_compact_token_limit, 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, workspace_roots, network, allow_login_shell, shell_environment_policy, windows_sandbox_mode, windows_sandbox_private_desktop, }, approvals_reviewer: constrained_approvals_reviewer.value(), enforce_residency: enforce_residency.value, notify: cfg.notify, user_instructions, base_instructions, personality, developer_instructions, compact_prompt, include_permissions_instructions, include_apps_instructions, include_collaboration_mode_instructions, include_skill_instructions, include_environment_context, // 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: resolve_cli_auth_credentials_store_mode( cfg.cli_auth_credentials_store.unwrap_or_default(), env!("CARGO_PKG_VERSION"), ), mcp_servers, // 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(), 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, agent_max_threads, agent_max_depth, agent_roles, memories: cfg.memories.unwrap_or_default().into(), agent_job_max_runtime_seconds, agent_interrupt_message_enabled, codex_home, sqlite_home, log_dir, config_lock_export_dir: cfg .debug .as_ref() .and_then(|debug| debug.config_lockfile.as_ref()) .and_then(|config_lock| config_lock.export_dir.clone()), config_lock_allow_codex_version_mismatch: cfg .debug .as_ref() .and_then(|debug| debug.config_lockfile.as_ref()) .and_then(|config_lock| config_lock.allow_codex_version_mismatch) .unwrap_or(false), config_lock_save_fields_resolved_from_model_catalog: cfg .debug .as_ref() .and_then(|debug| debug.config_lockfile.as_ref()) .and_then(|config_lock| config_lock.save_fields_resolved_from_model_catalog) .unwrap_or(true), config_lock_toml: None, config_layer_stack, history, ephemeral: ephemeral.unwrap_or_default(), bypass_hook_trust, file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), codex_self_exe, codex_linux_sandbox_exe, main_execve_wrapper_exe, zsh_path, 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, model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort), plan_mode_reasoning_effort: config_profile .plan_mode_reasoning_effort .or(cfg.plan_mode_reasoning_effort), model_reasoning_summary: config_profile .model_reasoning_summary .or(cfg.model_reasoning_summary), model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries, model_catalog, model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity), chatgpt_base_url: config_profile .chatgpt_base_url .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), apps_mcp_path_override, realtime_audio: cfg .audio .map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig { microphone: audio.microphone, speaker: audio.speaker, }), experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_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_config_endpoint: cfg.experimental_thread_config_endpoint, 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, use_experimental_unified_exec_tool, background_terminal_max_timeout, ghost_snapshot, multi_agent_v2, features, suppress_unstable_features_warning: cfg .suppress_unstable_features_warning .unwrap_or(false), active_profile: active_profile_name, active_project, notices, check_for_update_on_startup, disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), analytics_enabled: config_profile .analytics .as_ref() .and_then(|a| a.enabled) .or(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), show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true), model_availability_nux: cfg .tui .as_ref() .map(|t| t.model_availability_nux.clone()) .unwrap_or_default(), 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_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: config_profile .tui .as_ref() .and_then(|t| t.session_picker_view) .or_else(|| cfg.tui.as_ref().and_then(|t| t.session_picker_view)) .unwrap_or_default(), terminal_resize_reflow, tui_keymap: cfg .tui .as_ref() .map(|t| t.keymap.clone()) .unwrap_or_default(), otel, }; 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> { let Some(path) = path else { return Ok(None); }; let contents = fs .read_file_text(path, /*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_toml() .network .is_some() } pub fn bundled_skills_enabled(&self) -> bool { crate::manager::bundled_skills_enabled_from_stack(&self.config_layer_stack) } } pub(crate) fn uses_deprecated_instructions_file(config_layer_stack: &ConfigLayerStack) -> bool { config_layer_stack .layers_high_to_low() .into_iter() .any(|layer| toml_uses_deprecated_instructions_file(&layer.config)) } fn guardian_policy_config_from_requirements( requirements_toml: &ConfigRequirementsToml, ) -> Option { normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref()) } fn normalize_guardian_policy_config(value: Option<&str>) -> Option { value.and_then(|value| { let trimmed = value.trim(); (!trimmed.is_empty()).then(|| trimmed.to_string()) }) } fn toml_uses_deprecated_instructions_file(value: &TomlValue) -> bool { let Some(table) = value.as_table() else { return false; }; if table.contains_key("experimental_instructions_file") { return true; } let Some(profiles) = table.get("profiles").and_then(TomlValue::as_table) else { return false; }; profiles.values().any(|profile| { profile.as_table().is_some_and(|profile_table| { profile_table.contains_key("experimental_instructions_file") }) }) } /// 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 { 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 { Ok(cfg.log_dir.clone()) } #[cfg(test)] #[path = "config_tests.rs"] mod tests; #[cfg(test)] #[path = "config_loader_tests.rs"] mod config_loader_tests;