mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
11bceb8f8b
## Why Model metadata can now select multi-agent v2 even when a user has not enabled `features.multi_agent_v2` in their config. Some existing configs still set the legacy `agents.max_threads` knob for v1 multi-agent behavior, so treating every v2 runtime as incompatible with `agents.max_threads` would break users whose only v2 signal came from the model catalog. The incompatible configuration is specifically enabling `features.multi_agent_v2` while also setting `agents.max_threads`. Catalog-forced v2 should use the v2 concurrency setting and ignore the legacy v1 cap instead of rejecting the config. ## What changed - Split config validation from runtime concurrency calculation: `effective_agent_max_threads` now just returns the effective cap for the resolved multi-agent runtime. - Added explicit validation for `features.multi_agent_v2` + `agents.max_threads` at session startup. - Preserved catalog-selected v2 behavior when `features.multi_agent_v2` is disabled, so existing configs with `agents.max_threads` keep starting. - Updated model-runtime selector coverage so a catalog v2 model still exposes v2 tools even when `agents.max_threads` is set and the config flag is disabled. ## Validation - `cargo check -p codex-core --lib` - `just test -p codex-core --lib -E "test(multi_agent_v2_feature_rejects_agents_max_threads) | test(catalog_v2_allows_agents_max_threads_when_feature_disabled)"`
3913 lines
155 KiB
Rust
3913 lines
155 KiB
Rust
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::CloudConfigBundleLoader;
|
|
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::permissions_toml::PermissionsToml;
|
|
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
|
use codex_config::types::ApprovalsReviewer;
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_config::types::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_install_context::InstallContext;
|
|
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::AutoCompactTokenLimitScope;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use codex_protocol::config_types::Personality;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
|
use codex_protocol::config_types::SandboxMode;
|
|
use codex_protocol::config_types::ServiceTier;
|
|
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
|
use codex_protocol::config_types::TrustLevel;
|
|
use codex_protocol::config_types::Verbosity;
|
|
use codex_protocol::config_types::WebSearchConfig;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::models::ActivePermissionProfile;
|
|
use codex_protocol::models::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::MultiAgentVersion;
|
|
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::collections::hash_map::Entry;
|
|
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::is_builtin_permission_profile_name;
|
|
pub(crate) use permissions::reject_unknown_builtin_permission_profile;
|
|
pub(crate) use permissions::resolve_permission_profile;
|
|
pub use resolved_permission_profile::PermissionProfileSnapshot;
|
|
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<i64>,
|
|
pub ignore_large_untracked_dirs: Option<i64>,
|
|
pub disable_warnings: bool,
|
|
}
|
|
|
|
impl Default for GhostSnapshotConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
ignore_large_untracked_files: Some(DEFAULT_IGNORE_LARGE_UNTRACKED_FILES),
|
|
ignore_large_untracked_dirs: Some(DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS),
|
|
disable_warnings: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
|
/// files are *silently truncated* to this size so we do not take up too much of
|
|
/// the context window.
|
|
pub(crate) const AGENTS_MD_MAX_BYTES: usize = DEFAULT_PROJECT_DOC_MAX_BYTES; // 32 KiB
|
|
pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6);
|
|
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usize = 4;
|
|
pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
|
|
pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000;
|
|
pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
|
|
const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
|
|
|
At the start of your turn, you are the active agent.
|
|
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
|
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
|
|
|
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
|
|
Child agents can also spawn their own sub-agents.
|
|
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
|
|
|
You will receive messages in the analysis channel in the form:
|
|
```
|
|
Message Type: MESSAGE | FINAL_ANSWER
|
|
Sender: <author>
|
|
Payload:
|
|
<payload text>
|
|
```
|
|
They may be addressed as to=/root
|
|
"#;
|
|
const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task.
|
|
|
|
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
|
|
|
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
|
|
Child agents can also spawn their own sub-agents.
|
|
|
|
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
|
|
|
|
You will receive messages in the analysis channel in the form:
|
|
```
|
|
Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER
|
|
Task name: <recipient> # only for NEW_TASK -- this determines your identity
|
|
Sender: <author>
|
|
Payload:
|
|
<payload text>
|
|
```
|
|
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
|
|
"#;
|
|
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<u64> = 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<PathBuf> {
|
|
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<AskForApproval>,
|
|
/// Constrained permission profile plus its selected profile identity, if
|
|
/// the profile came from a built-in or named config profile.
|
|
permission_profile_state: PermissionProfileState,
|
|
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
|
|
/// entries in the permission profile are materialized against these roots.
|
|
workspace_roots: Vec<AbsolutePathBuf>,
|
|
/// Effective network configuration applied to all spawned processes.
|
|
pub network: Option<NetworkProxySpec>,
|
|
/// Whether the model may request a login shell for shell-based tools.
|
|
/// Default to `true`
|
|
///
|
|
/// If `true`, the model may request a login shell (`login = true`), and
|
|
/// omitting `login` defaults to using a login shell.
|
|
/// If `false`, the model can never use a login shell: `login = true`
|
|
/// requests are rejected, and omitting `login` defaults to a non-login
|
|
/// shell.
|
|
pub allow_login_shell: bool,
|
|
/// Policy used to build process environments for shell/unified exec.
|
|
pub shell_environment_policy: ShellEnvironmentPolicy,
|
|
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
|
|
/// legacy feature keys.
|
|
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
|
|
/// 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<AskForApproval>,
|
|
permission_profile: Constrained<PermissionProfile>,
|
|
) -> ConstraintResult<Self> {
|
|
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 a snapshot directly.
|
|
pub fn set_permission_profile_from_session_snapshot(
|
|
&mut self,
|
|
snapshot: PermissionProfileSnapshot,
|
|
) -> ConstraintResult<()> {
|
|
self.permission_profile_state
|
|
.set_permission_profile_snapshot(snapshot)
|
|
}
|
|
|
|
/// Replace the current permission constraints with a trusted session
|
|
/// snapshot. This is only for clients that must mirror core session state
|
|
/// after their local config constraints reject the snapshot.
|
|
pub fn replace_permission_profile_from_session_snapshot(
|
|
&mut self,
|
|
snapshot: PermissionProfileSnapshot,
|
|
) -> ConstraintResult<()> {
|
|
let permission_profile = Constrained::allow_only(snapshot.permission_profile().clone());
|
|
self.permission_profile_state = PermissionProfileState::from_constrained_resolved(
|
|
permission_profile,
|
|
snapshot.into_resolved_permission_profile(),
|
|
)?;
|
|
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<AbsolutePathBuf>) {
|
|
self.workspace_roots = workspace_roots;
|
|
}
|
|
|
|
pub fn workspace_roots(&self) -> &[AbsolutePathBuf] {
|
|
&self.workspace_roots
|
|
}
|
|
|
|
/// Workspace roots that came from user-visible configuration or runtime
|
|
/// selection. Internal Codex-only writable roots are intentionally excluded.
|
|
pub fn user_visible_workspace_roots(&self) -> &[AbsolutePathBuf] {
|
|
&self.workspace_roots
|
|
}
|
|
|
|
pub fn profile_workspace_roots(&self) -> &[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<ActivePermissionProfile> {
|
|
self.permission_profile_state.active_permission_profile()
|
|
}
|
|
|
|
/// Effective filesystem sandbox policy derived from the canonical profile.
|
|
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
|
self.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();
|
|
compatibility_sandbox_policy_for_permission_profile(&permission_profile, cwd)
|
|
}
|
|
|
|
/// Check whether a legacy sandbox policy can be applied to this permission
|
|
/// set after projecting it into the canonical permission profile.
|
|
pub fn can_set_legacy_sandbox_policy(
|
|
&self,
|
|
sandbox_policy: &SandboxPolicy,
|
|
cwd: &Path,
|
|
) -> ConstraintResult<()> {
|
|
let file_system_sandbox_policy =
|
|
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd);
|
|
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
|
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
|
|
&file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
);
|
|
self.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,
|
|
}
|
|
}
|
|
|
|
fn build_network_proxy_spec(
|
|
configured_network_proxy_config: NetworkProxyConfig,
|
|
network_requirements: Option<Sourced<codex_config::NetworkConstraints>>,
|
|
permission_profile: &PermissionProfile,
|
|
) -> std::io::Result<Option<NetworkProxySpec>> {
|
|
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 = NetworkProxySpec::from_config_and_constraints(
|
|
configured_network_proxy_config,
|
|
network_requirements,
|
|
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
|
|
}
|
|
})?;
|
|
|
|
Ok(if has_network_requirements {
|
|
Some(network)
|
|
} else {
|
|
network.enabled().then_some(network)
|
|
})
|
|
}
|
|
|
|
/// 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<String>,
|
|
|
|
/// Optional override of model selection.
|
|
pub model: Option<String>,
|
|
|
|
/// Effective service tier request id preference for new turns.
|
|
/// `default` means the user explicitly selected standard routing.
|
|
pub service_tier: Option<String>,
|
|
|
|
/// Model used specifically for review sessions.
|
|
pub review_model: Option<String>,
|
|
|
|
/// Size of the context window for the model, in tokens.
|
|
pub model_context_window: Option<i64>,
|
|
|
|
/// Token usage threshold triggering auto-compaction of conversation history.
|
|
pub model_auto_compact_token_limit: Option<i64>,
|
|
|
|
/// Controls whether `model_auto_compact_token_limit` applies to the full
|
|
/// active context or only tokens after the carried compaction-window prefix.
|
|
pub model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope,
|
|
|
|
/// 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<Personality>,
|
|
|
|
/// Effective permission configuration for shell tool execution.
|
|
pub permissions: Permissions,
|
|
|
|
/// Whether config explicitly selected named permissions profiles instead
|
|
/// of the legacy `sandbox_mode` syntax.
|
|
pub explicit_permission_profile_mode: bool,
|
|
|
|
/// User-defined permission profiles available from effective config.
|
|
pub custom_permission_profiles: Vec<CustomPermissionProfileSummary>,
|
|
|
|
/// 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<Option<ResidencyRequirement>>,
|
|
|
|
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
|
/// suppressed from the frontend output. This can reduce visual noise when
|
|
/// users are only interested in the final agent responses.
|
|
pub hide_agent_reasoning: bool,
|
|
|
|
/// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
|
|
/// Defaults to `false`.
|
|
pub show_raw_agent_reasoning: bool,
|
|
|
|
/// User-provided instructions from AGENTS.md.
|
|
pub user_instructions: Option<String>,
|
|
|
|
/// Base instructions override.
|
|
pub base_instructions: Option<String>,
|
|
|
|
/// Developer instructions override injected as a separate message.
|
|
pub developer_instructions: Option<String>,
|
|
|
|
/// Guardian-specific policy config override from requirements.toml or config.toml.
|
|
/// This is inserted into the fixed guardian prompt template under the
|
|
/// `# Policy Configuration` section rather than replacing the whole
|
|
/// guardian developer prompt.
|
|
pub guardian_policy_config: Option<String>,
|
|
|
|
/// Whether to inject the `<permissions instructions>` developer block.
|
|
pub include_permissions_instructions: bool,
|
|
|
|
/// Whether to inject the `<apps_instructions>` developer block.
|
|
pub include_apps_instructions: bool,
|
|
|
|
/// Whether to inject the `<collaboration_mode>` developer block.
|
|
pub include_collaboration_mode_instructions: bool,
|
|
|
|
/// Whether to inject the `<skills_instructions>` developer block.
|
|
pub include_skill_instructions: bool,
|
|
|
|
/// Whether to inject the `<environment_context>` user block.
|
|
pub include_environment_context: bool,
|
|
|
|
/// Compact prompt override.
|
|
pub compact_prompt: Option<String>,
|
|
|
|
/// Optional external notifier command. When set, Codex will spawn this
|
|
/// program after each completed *turn* (i.e. when the agent finishes
|
|
/// processing a user submission). The value must be the full command
|
|
/// broken into argv tokens **without** the trailing JSON argument - Codex
|
|
/// appends one extra argument containing a JSON payload describing the
|
|
/// event.
|
|
///
|
|
/// Example `~/.codex/config.toml` snippet:
|
|
///
|
|
/// ```toml
|
|
/// notify = ["notify-send", "Codex"]
|
|
/// ```
|
|
///
|
|
/// which will be invoked as:
|
|
///
|
|
/// ```shell
|
|
/// notify-send Codex '{"type":"agent-turn-complete","turn-id":"12345"}'
|
|
/// ```
|
|
///
|
|
/// If unset the feature is disabled.
|
|
pub notify: Option<Vec<String>>,
|
|
|
|
/// TUI notification settings, including enabled events, delivery method, and focus condition.
|
|
pub tui_notifications: TuiNotificationSettings,
|
|
|
|
/// Enable ASCII animations and shimmer effects in the TUI.
|
|
pub animations: bool,
|
|
|
|
/// 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<Vec<String>>,
|
|
|
|
/// Whether to color status line items with colors from the active syntax theme.
|
|
pub tui_status_line_use_colors: bool,
|
|
|
|
/// Ordered list of terminal title item identifiers for the TUI.
|
|
///
|
|
/// When unset, the TUI defaults to: `activity` and `project`.
|
|
/// The `activity` item spins while working and shows an action-required
|
|
/// message when blocked on the user.
|
|
pub tui_terminal_title: Option<Vec<String>>,
|
|
|
|
/// Syntax highlighting theme override (kebab-case name).
|
|
pub tui_theme: Option<String>,
|
|
|
|
/// Pet id preselected by the terminal pet picker.
|
|
pub tui_pet: Option<String>,
|
|
|
|
/// Vertical anchor used by terminal pet rendering.
|
|
pub tui_pet_anchor: TuiPetAnchor,
|
|
|
|
/// Preferred layout for resume/fork session picker results.
|
|
pub tui_session_picker_view: SessionPickerViewMode,
|
|
|
|
/// 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<AbsolutePathBuf>,
|
|
/// Whether runtime workspace roots were supplied explicitly by the caller
|
|
/// or legacy config, rather than defaulting to `cwd`.
|
|
pub workspace_roots_explicit: bool,
|
|
|
|
/// Preferred store for CLI auth credentials.
|
|
/// file (default): Use a file in the Codex home directory.
|
|
/// keyring: Use an OS-specific keyring service.
|
|
/// auto: Use the OS-specific keyring service if available, otherwise use a file.
|
|
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
|
|
/// Definition for MCP servers that Codex can reach out to for tool calls.
|
|
pub mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
|
|
|
|
/// Preferred store for MCP OAuth credentials.
|
|
/// keyring: Use an OS-specific keyring service.
|
|
/// Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access.
|
|
/// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2
|
|
/// file: CODEX_HOME/.credentials.json
|
|
/// This file will be readable to Codex and other applications running as the same user.
|
|
/// auto (default): keyring if available, otherwise file.
|
|
pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode,
|
|
|
|
/// Optional fixed port to use for the local HTTP callback server used during MCP OAuth login.
|
|
///
|
|
/// When unset, Codex will bind to an ephemeral port chosen by the OS.
|
|
pub mcp_oauth_callback_port: Option<u16>,
|
|
|
|
/// Optional redirect URI to use during MCP OAuth login.
|
|
///
|
|
/// When set, this URI is used in the OAuth authorization request instead
|
|
/// of the local listener address. The local callback listener still binds
|
|
/// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided).
|
|
pub mcp_oauth_callback_url: Option<String>,
|
|
|
|
/// Combined provider map (defaults plus user-defined providers).
|
|
pub model_providers: HashMap<String, ModelProviderInfo>,
|
|
|
|
/// 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<String>,
|
|
|
|
/// Token budget applied when storing tool/function outputs in the context manager.
|
|
pub tool_output_token_limit: Option<usize>,
|
|
|
|
/// User-configured maximum number of agent threads that can be open concurrently.
|
|
pub agent_max_threads: Option<usize>,
|
|
/// Maximum runtime in seconds for agent job workers before they are failed.
|
|
pub agent_job_max_runtime_seconds: Option<u64>,
|
|
|
|
/// 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<String, AgentRoleConfig>,
|
|
|
|
/// 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<AbsolutePathBuf>,
|
|
|
|
/// 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<Arc<ConfigLockfileToml>>,
|
|
|
|
/// 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<PathBuf>,
|
|
|
|
/// Path to the `codex-linux-sandbox` executable. This must be set if
|
|
/// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this
|
|
/// cannot be set in the config file: it must be set in code via
|
|
/// [`ConfigOverrides`].
|
|
///
|
|
/// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
|
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
|
|
|
/// 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<PathBuf>,
|
|
|
|
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
|
|
pub zsh_path: Option<PathBuf>,
|
|
|
|
/// Value to use for `reasoning.effort` when making a request using the
|
|
/// Responses API.
|
|
pub model_reasoning_effort: Option<ReasoningEffort>,
|
|
/// Optional Plan-mode-specific reasoning effort override used by the TUI.
|
|
///
|
|
/// When unset, Plan mode uses the built-in Plan preset default (currently
|
|
/// `medium`). When explicitly set (including `none`), this overrides the
|
|
/// Plan preset. The `none` value means "no reasoning" (not "inherit the
|
|
/// global default").
|
|
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
|
|
|
|
/// Optional value to use for `reasoning.summary` when making a request
|
|
/// using the Responses API. When unset, the model catalog default is used.
|
|
pub model_reasoning_summary: Option<ReasoningSummary>,
|
|
|
|
/// Optional override to force-enable reasoning summaries for the configured model.
|
|
pub model_supports_reasoning_summaries: Option<bool>,
|
|
|
|
/// Optional full model catalog loaded from `model_catalog_json`.
|
|
/// When set, this replaces the bundled catalog for the current process.
|
|
pub model_catalog: Option<ModelsResponse>,
|
|
|
|
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
|
|
pub model_verbosity: Option<Verbosity>,
|
|
|
|
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
|
pub chatgpt_base_url: String,
|
|
|
|
/// Optional path override for the host-owned apps MCP server.
|
|
pub apps_mcp_path_override: Option<String>,
|
|
|
|
/// Optional product SKU forwarded to the host-owned apps MCP server.
|
|
pub apps_mcp_product_sku: Option<String>,
|
|
|
|
/// Machine-local realtime audio device preferences used by realtime voice.
|
|
pub realtime_audio: RealtimeAudioConfig,
|
|
|
|
/// Experimental / do not use. Overrides only the realtime conversation
|
|
/// websocket transport base URL (the `Op::RealtimeConversation`
|
|
/// `/v1/realtime`
|
|
/// connection) without changing normal provider HTTP requests.
|
|
pub experimental_realtime_ws_base_url: Option<String>,
|
|
/// Experimental / do not use. Selects the realtime websocket model/snapshot
|
|
/// used for the `Op::RealtimeConversation` connection.
|
|
pub experimental_realtime_ws_model: Option<String>,
|
|
/// Experimental / do not use. Realtime websocket session selection.
|
|
/// `version` controls v1/v2 and `type` controls conversational/transcription.
|
|
pub realtime: RealtimeConfig,
|
|
/// Experimental / do not use. Overrides only the realtime conversation
|
|
/// websocket transport instructions (the `Op::RealtimeConversation`
|
|
/// `/ws` session.update instructions) without changing normal prompts.
|
|
pub experimental_realtime_ws_backend_prompt: Option<String>,
|
|
/// Experimental / do not use. Replaces the synthesized realtime startup
|
|
/// context appended to websocket session instructions. An empty string
|
|
/// disables startup context injection entirely.
|
|
pub experimental_realtime_ws_startup_context: Option<String>,
|
|
/// Experimental / do not use. Replaces the built-in realtime start
|
|
/// instructions inserted into developer messages when realtime becomes
|
|
/// active.
|
|
pub experimental_realtime_start_instructions: Option<String>,
|
|
/// Experimental / do not use. When set, app-server fetches thread-scoped
|
|
/// config from a remote service at this endpoint.
|
|
pub experimental_thread_config_endpoint: Option<String>,
|
|
|
|
/// Experimental / do not use. Selects the thread persistence backend.
|
|
pub experimental_thread_store: ThreadStoreConfig,
|
|
/// When set, restricts ChatGPT login to one or more workspace identifiers.
|
|
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
|
|
|
/// When set, restricts the login mechanism users may use.
|
|
pub forced_login_method: Option<ForcedLoginMethod>,
|
|
|
|
/// Explicit or feature-derived web search mode.
|
|
pub web_search_mode: Constrained<WebSearchMode>,
|
|
|
|
/// Additional parameters for the web search tool when it is enabled.
|
|
pub web_search_config: Option<WebSearchConfig>,
|
|
|
|
/// Whether to register the experimental request_user_input tool.
|
|
pub experimental_request_user_input_enabled: bool,
|
|
|
|
/// 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 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<bool>,
|
|
|
|
/// 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<String>,
|
|
pub root_agent_usage_hint_text: Option<String>,
|
|
pub subagent_usage_hint_text: Option<String>,
|
|
pub tool_namespace: Option<String>,
|
|
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: Some(
|
|
DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT.to_string(),
|
|
),
|
|
subagent_usage_hint_text: Some(
|
|
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT.to_string(),
|
|
),
|
|
tool_namespace: None,
|
|
hide_spawn_agent_metadata: true,
|
|
non_code_mode_only: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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<Vec<String>> {
|
|
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<PathBuf>,
|
|
cli_overrides: Option<Vec<(String, TomlValue)>>,
|
|
harness_overrides: Option<ConfigOverrides>,
|
|
loader_overrides: Option<LoaderOverrides>,
|
|
strict_config: bool,
|
|
cloud_config_bundle: CloudConfigBundleLoader,
|
|
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
|
|
fallback_cwd: Option<PathBuf>,
|
|
}
|
|
|
|
impl ConfigBuilder {
|
|
pub fn codex_home(mut self, codex_home: PathBuf) -> Self {
|
|
self.codex_home = Some(codex_home);
|
|
self
|
|
}
|
|
|
|
pub fn cli_overrides(mut self, cli_overrides: Vec<(String, TomlValue)>) -> Self {
|
|
self.cli_overrides = Some(cli_overrides);
|
|
self
|
|
}
|
|
|
|
pub fn harness_overrides(mut self, harness_overrides: ConfigOverrides) -> Self {
|
|
self.harness_overrides = Some(harness_overrides);
|
|
self
|
|
}
|
|
|
|
pub fn loader_overrides(mut self, loader_overrides: LoaderOverrides) -> Self {
|
|
self.loader_overrides = Some(loader_overrides);
|
|
self
|
|
}
|
|
|
|
pub fn strict_config(mut self, strict_config: bool) -> Self {
|
|
self.strict_config = strict_config;
|
|
self
|
|
}
|
|
|
|
pub fn cloud_config_bundle(mut self, cloud_config_bundle: CloudConfigBundleLoader) -> Self {
|
|
self.cloud_config_bundle = cloud_config_bundle;
|
|
self
|
|
}
|
|
|
|
pub fn thread_config_loader(
|
|
mut self,
|
|
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
|
) -> Self {
|
|
self.thread_config_loader = Some(thread_config_loader);
|
|
self
|
|
}
|
|
|
|
pub fn fallback_cwd(mut self, fallback_cwd: Option<PathBuf>) -> Self {
|
|
self.fallback_cwd = fallback_cwd;
|
|
self
|
|
}
|
|
|
|
pub async fn build(self) -> std::io::Result<Config> {
|
|
// 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<Config> {
|
|
let Self {
|
|
codex_home,
|
|
cli_overrides,
|
|
harness_overrides,
|
|
loader_overrides,
|
|
strict_config,
|
|
cloud_config_bundle,
|
|
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_config_bundle,
|
|
},
|
|
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::<ConfigToml>(
|
|
&config_layer_stack,
|
|
codex_config::CONFIG_TOML_FILE,
|
|
)
|
|
.await
|
|
{
|
|
return Err(codex_config::io_error_from_config_error(
|
|
std::io::ErrorKind::InvalidData,
|
|
config_error,
|
|
Some(err),
|
|
));
|
|
}
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err));
|
|
}
|
|
};
|
|
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(crate) fn multi_agent_version_from_features(&self) -> MultiAgentVersion {
|
|
if self.features.enabled(Feature::MultiAgentV2) {
|
|
MultiAgentVersion::V2
|
|
} else if self.features.enabled(Feature::Collab) {
|
|
MultiAgentVersion::V1
|
|
} else {
|
|
MultiAgentVersion::Disabled
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_multi_agent_v2_config(&self) -> std::io::Result<()> {
|
|
if self.features.enabled(Feature::MultiAgentV2) && self.agent_max_threads.is_some() {
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"agents.max_threads cannot be set when features.multi_agent_v2 is enabled",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn effective_agent_max_threads(
|
|
&self,
|
|
multi_agent_version: MultiAgentVersion,
|
|
) -> Option<usize> {
|
|
match multi_agent_version {
|
|
MultiAgentVersion::V2 => Some(
|
|
self.multi_agent_v2
|
|
.max_concurrent_threads_per_session
|
|
.saturating_sub(1),
|
|
),
|
|
MultiAgentVersion::Disabled | MultiAgentVersion::V1 => {
|
|
self.agent_max_threads.or(DEFAULT_AGENT_MAX_THREADS)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn legacy_sandbox_policy(&self) -> SandboxPolicy {
|
|
self.permissions.legacy_sandbox_policy(self.cwd.as_path())
|
|
}
|
|
|
|
pub fn set_legacy_sandbox_policy(
|
|
&mut self,
|
|
sandbox_policy: SandboxPolicy,
|
|
) -> ConstraintResult<()> {
|
|
self.workspace_roots_explicit = matches!(
|
|
&sandbox_policy,
|
|
SandboxPolicy::WorkspaceWrite { writable_roots, .. } if !writable_roots.is_empty()
|
|
);
|
|
self.permissions
|
|
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())?;
|
|
self.workspace_roots = self.permissions.workspace_roots().to_vec();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn effective_workspace_roots(&self) -> Vec<AbsolutePathBuf> {
|
|
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.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();
|
|
let mut plugin_ids_by_mcp_server_name = HashMap::new();
|
|
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 {
|
|
if let Entry::Vacant(entry) = configured_mcp_servers.entry(name.clone()) {
|
|
entry.insert(plugin_server);
|
|
plugin_ids_by_mcp_server_name.insert(name, plugin.config_name.clone());
|
|
}
|
|
}
|
|
}
|
|
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));
|
|
}
|
|
plugin_ids_by_mcp_server_name
|
|
.retain(|server_name, _| configured_mcp_servers.contains_key(server_name));
|
|
|
|
McpConfig {
|
|
chatgpt_base_url: self.chatgpt_base_url.clone(),
|
|
apps_mcp_path_override: self.apps_mcp_path_override.clone(),
|
|
apps_mcp_product_sku: self.apps_mcp_product_sku.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),
|
|
prefix_mcp_tool_names: self.prefix_mcp_tool_names(),
|
|
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_ids_by_mcp_server_name,
|
|
plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn prefix_mcp_tool_names(&self) -> bool {
|
|
!self.features.enabled(Feature::NonPrefixedMcpToolNames)
|
|
}
|
|
|
|
pub async fn rebuild_preserving_session_layers(
|
|
&self,
|
|
refreshed_config: &Config,
|
|
) -> std::io::Result<Self> {
|
|
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::<Vec<_>>();
|
|
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))?;
|
|
let default_zsh_path = refreshed_config
|
|
.zsh_path
|
|
.clone()
|
|
.map(AbsolutePathBuf::try_from)
|
|
.transpose()?;
|
|
|
|
Self::load_config_with_layer_stack(
|
|
LOCAL_FS.as_ref(),
|
|
cfg,
|
|
ConfigOverrides {
|
|
cwd: Some(self.cwd.to_path_buf()),
|
|
default_zsh_path,
|
|
..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<Self> {
|
|
ConfigBuilder::default()
|
|
.cli_overrides(cli_overrides)
|
|
.build()
|
|
.await
|
|
}
|
|
|
|
/// Load a default configuration when user config files are invalid.
|
|
pub async fn load_default_with_cli_overrides(
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
) -> std::io::Result<Self> {
|
|
let codex_home = find_codex_home()?;
|
|
Self::load_default_with_cli_overrides_for_codex_home(
|
|
codex_home.to_path_buf(),
|
|
cli_overrides,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Load a default configuration for a specific Codex home without reading
|
|
/// user, project, or system config layers.
|
|
pub async fn load_default_with_cli_overrides_for_codex_home(
|
|
codex_home: PathBuf,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
) -> std::io::Result<Self> {
|
|
let mut merged = toml::Value::try_from(ConfigToml::default()).map_err(|e| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
format!("failed to serialize default config: {e}"),
|
|
)
|
|
})?;
|
|
let cli_layer = codex_config::build_cli_overrides_layer(&cli_overrides);
|
|
codex_config::merge_toml_values(&mut merged, &cli_layer);
|
|
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)?;
|
|
let config_toml = deserialize_config_toml_with_base(merged, &codex_home)?;
|
|
Self::load_config_with_layer_stack(
|
|
LOCAL_FS.as_ref(),
|
|
config_toml,
|
|
ConfigOverrides::default(),
|
|
codex_home,
|
|
ConfigLayerStack::default(),
|
|
)
|
|
.await
|
|
}
|
|
/// This is a secondary way of creating [Config], which is appropriate when
|
|
/// the harness is meant to be used with a specific configuration that
|
|
/// ignores user settings. For example, the `codex exec` subcommand is
|
|
/// designed to use [AskForApproval::Never] exclusively.
|
|
///
|
|
/// Further, [ConfigOverrides] contains some options that are not supported
|
|
/// in [ConfigToml], such as `cwd`, `codex_self_exe`, `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<Self> {
|
|
ConfigBuilder::default()
|
|
.cli_overrides(cli_overrides)
|
|
.harness_overrides(harness_overrides)
|
|
.build()
|
|
.await
|
|
}
|
|
}
|
|
|
|
pub fn resolve_profile_v2_config_path(
|
|
codex_home: &Path,
|
|
profile_name: &ProfileV2Name,
|
|
) -> AbsolutePathBuf {
|
|
AbsolutePathBuf::resolve_path_against_base(
|
|
format!("{profile_name}{CONFIG_PROFILE_V2_SUFFIX}"),
|
|
codex_home,
|
|
)
|
|
}
|
|
|
|
/// DEPRECATED: Use [Config::load_with_cli_overrides()] instead because working
|
|
/// with [ConfigToml] directly means that [ConfigRequirements] have not been
|
|
/// applied yet, which risks failing to enforce required constraints.
|
|
pub async fn load_config_as_toml_with_cli_overrides(
|
|
codex_home: &Path,
|
|
cwd: Option<&AbsolutePathBuf>,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> std::io::Result<ConfigToml> {
|
|
load_config_as_toml_with_cli_and_loader_overrides(
|
|
codex_home,
|
|
cwd,
|
|
cli_overrides,
|
|
loader_overrides,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// DEPRECATED for most callers: prefer [Config::load_with_cli_overrides()] or
|
|
/// [ConfigBuilder] because working with [ConfigToml] directly means
|
|
/// [ConfigRequirements] have not been applied yet, which risks skipping
|
|
/// required constraints.
|
|
pub async fn load_config_as_toml_with_cli_and_loader_overrides(
|
|
codex_home: &Path,
|
|
cwd: Option<&AbsolutePathBuf>,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> std::io::Result<ConfigToml> {
|
|
load_config_as_toml_with_cli_and_load_options(codex_home, cwd, cli_overrides, loader_overrides)
|
|
.await
|
|
}
|
|
|
|
/// DEPRECATED for most callers: prefer [Config::load_with_cli_overrides()] or
|
|
/// [ConfigBuilder] because working with [ConfigToml] directly means
|
|
/// [ConfigRequirements] have not been applied yet, which risks skipping
|
|
/// required constraints.
|
|
pub async fn load_config_as_toml_with_cli_and_load_options(
|
|
codex_home: &Path,
|
|
cwd: Option<&AbsolutePathBuf>,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
options: impl Into<ConfigLoadOptions>,
|
|
) -> std::io::Result<ConfigToml> {
|
|
let config_layer_stack = load_config_layers_state(
|
|
LOCAL_FS.as_ref(),
|
|
codex_home,
|
|
cwd.cloned(),
|
|
&cli_overrides,
|
|
options,
|
|
&codex_config::NoopThreadConfigLoader,
|
|
)
|
|
.await?;
|
|
|
|
let merged_toml = config_layer_stack.effective_config();
|
|
let cfg = deserialize_config_toml_with_base(merged_toml, codex_home).map_err(|e| {
|
|
tracing::error!("Failed to deserialize overridden config: {e}");
|
|
e
|
|
})?;
|
|
|
|
Ok(cfg)
|
|
}
|
|
|
|
pub fn deserialize_config_toml_with_base(
|
|
root_value: TomlValue,
|
|
config_base_dir: &Path,
|
|
) -> std::io::Result<ConfigToml> {
|
|
// This guard ensures that any relative paths that is deserialized into an
|
|
// [AbsolutePathBuf] is resolved against `config_base_dir`.
|
|
let _guard = AbsolutePathBufGuard::new(config_base_dir);
|
|
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<FeatureRequirementsToml>>,
|
|
) -> std::io::Result<()> {
|
|
managed_features::validate_explicit_feature_settings_in_config_toml(cfg, feature_requirements)?;
|
|
managed_features::validate_feature_requirements_in_config_toml(cfg, feature_requirements)
|
|
}
|
|
|
|
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
|
|
let file_contents = std::fs::read_to_string(path)?;
|
|
let catalog = serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
|
|
std::io::Error::new(
|
|
ErrorKind::InvalidData,
|
|
format!(
|
|
"failed to parse model_catalog_json path `{}` as JSON: {err}",
|
|
path.display()
|
|
),
|
|
)
|
|
})?;
|
|
if catalog.models.is_empty() {
|
|
return Err(std::io::Error::new(
|
|
ErrorKind::InvalidData,
|
|
format!(
|
|
"model_catalog_json path `{}` must contain at least one model",
|
|
path.display()
|
|
),
|
|
));
|
|
}
|
|
Ok(catalog)
|
|
}
|
|
|
|
fn load_model_catalog(
|
|
model_catalog_json: Option<AbsolutePathBuf>,
|
|
) -> std::io::Result<Option<ModelsResponse>> {
|
|
model_catalog_json
|
|
.map(|path| load_catalog_json(&path))
|
|
.transpose()
|
|
}
|
|
|
|
fn filter_mcp_servers_by_requirements(
|
|
mcp_servers: &mut HashMap<String, McpServerConfig>,
|
|
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
|
|
) {
|
|
let Some(allowlist) = mcp_requirements else {
|
|
return;
|
|
};
|
|
|
|
let source = allowlist.source.clone();
|
|
for (name, server) in mcp_servers.iter_mut() {
|
|
let allowed = allowlist
|
|
.value
|
|
.get(name)
|
|
.is_some_and(|requirement| 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<String, McpServerConfig>,
|
|
plugin_requirements: Option<&Sourced<BTreeMap<String, PluginRequirementsToml>>>,
|
|
) {
|
|
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<String, McpServerConfig>,
|
|
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
|
|
) -> ConstraintResult<Constrained<HashMap<String, McpServerConfig>>> {
|
|
if mcp_requirements.is_none() {
|
|
return Ok(Constrained::allow_any(mcp_servers));
|
|
}
|
|
|
|
let mcp_requirements = mcp_requirements.cloned();
|
|
Constrained::normalized(mcp_servers, move |mut servers| {
|
|
filter_mcp_servers_by_requirements(&mut servers, mcp_requirements.as_ref());
|
|
servers
|
|
})
|
|
}
|
|
|
|
fn apply_requirement_constrained_value<T>(
|
|
field_name: &'static str,
|
|
configured_value: T,
|
|
constrained_value: &mut ConstrainedWithSource<T>,
|
|
startup_warnings: &mut Vec<String>,
|
|
) -> std::io::Result<bool>
|
|
where
|
|
T: Clone + std::fmt::Debug + Send + Sync,
|
|
{
|
|
if let Err(err) = constrained_value.set(configured_value) {
|
|
let fallback_value = constrained_value.get().clone();
|
|
tracing::warn!(
|
|
error = %err,
|
|
?fallback_value,
|
|
requirement_source = ?constrained_value.source,
|
|
"configured value is disallowed by requirements; falling back to required value for {field_name}"
|
|
);
|
|
let message = format!(
|
|
"Configured value for `{field_name}` is disallowed by requirements; falling back to required value {fallback_value:?}. Details: {err}"
|
|
);
|
|
startup_warnings.push(message);
|
|
|
|
constrained_value.set(fallback_value).map_err(|fallback_err| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"configured value for `{field_name}` is disallowed by requirements ({err}); fallback to a requirement-compliant value also failed ({fallback_err})"
|
|
),
|
|
)
|
|
})?;
|
|
return Ok(true);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
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<BTreeMap<String, McpServerConfig>> {
|
|
// In general, Config::load_with_cli_overrides() should be used to load the
|
|
// full config with requirements.toml applied, but in this case, we need
|
|
// access to the raw TOML in order to warn the user about deprecated fields.
|
|
//
|
|
// Note that a more precise way to do this would be to audit the individual
|
|
// config layers for deprecated fields rather than reporting on the merged
|
|
// result.
|
|
let cli_overrides = Vec::<(String, TomlValue)>::new();
|
|
// There is no cwd/project context for this query, so this will not include
|
|
// MCP servers defined in in-repo .codex/ folders.
|
|
let cwd: Option<AbsolutePathBuf> = None;
|
|
let config_layer_stack = load_config_layers_state(
|
|
LOCAL_FS.as_ref(),
|
|
codex_home,
|
|
cwd,
|
|
&cli_overrides,
|
|
LoaderOverrides::default(),
|
|
&codex_config::NoopThreadConfigLoader,
|
|
)
|
|
.await?;
|
|
let merged_toml = config_layer_stack.effective_config();
|
|
let Some(servers_value) = merged_toml.get("mcp_servers") else {
|
|
return Ok(BTreeMap::new());
|
|
};
|
|
|
|
ensure_no_inline_bearer_tokens(servers_value)?;
|
|
|
|
servers_value
|
|
.clone()
|
|
.try_into()
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
|
}
|
|
|
|
/// We briefly allowed plain text bearer_token fields in MCP server configs.
|
|
/// We want to warn people who recently added these fields but can remove this after a few months.
|
|
fn ensure_no_inline_bearer_tokens(value: &TomlValue) -> std::io::Result<()> {
|
|
let Some(servers_table) = value.as_table() else {
|
|
return Ok(());
|
|
};
|
|
|
|
for (server_name, server_value) in servers_table {
|
|
if let Some(server_table) = server_value.as_table()
|
|
&& server_table.contains_key("bearer_token")
|
|
{
|
|
let message = format!(
|
|
"mcp_servers.{server_name} uses unsupported `bearer_token`; set `bearer_token_env_var`."
|
|
);
|
|
return Err(std::io::Error::new(ErrorKind::InvalidData, message));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn set_project_trust_level_inner(
|
|
doc: &mut DocumentMut,
|
|
project_path: &Path,
|
|
trust_level: TrustLevel,
|
|
) -> anyhow::Result<()> {
|
|
// Ensure we render a human-friendly structure:
|
|
//
|
|
// [projects]
|
|
// [projects."/path/to/project"]
|
|
// trust_level = "trusted" or "untrusted"
|
|
//
|
|
// rather than inline tables like:
|
|
//
|
|
// [projects]
|
|
// "/path/to/project" = { trust_level = "trusted" }
|
|
let project_key = project_trust_key(project_path);
|
|
|
|
// Ensure top-level `projects` exists as a non-inline, explicit table. If it
|
|
// exists but was previously represented as a non-table (e.g., inline),
|
|
// replace it with an explicit table.
|
|
{
|
|
let root = doc.as_table_mut();
|
|
// If `projects` exists but isn't a standard table (e.g., it's an inline table),
|
|
// convert it to an explicit table while preserving existing entries.
|
|
let existing_projects = root.get("projects").cloned();
|
|
if existing_projects.as_ref().is_none_or(|i| !i.is_table()) {
|
|
let mut projects_tbl = toml_edit::Table::new();
|
|
projects_tbl.set_implicit(true);
|
|
|
|
// If there was an existing inline table, migrate its entries to explicit tables.
|
|
if let Some(inline_tbl) = existing_projects.as_ref().and_then(|i| i.as_inline_table()) {
|
|
for (k, v) in inline_tbl.iter() {
|
|
if let Some(inner_tbl) = v.as_inline_table() {
|
|
let new_tbl = inner_tbl.clone().into_table();
|
|
projects_tbl.insert(k, toml_edit::Item::Table(new_tbl));
|
|
}
|
|
}
|
|
}
|
|
|
|
root.insert("projects", toml_edit::Item::Table(projects_tbl));
|
|
}
|
|
}
|
|
let Some(projects_tbl) = doc["projects"].as_table_mut() else {
|
|
return Err(anyhow::anyhow!(
|
|
"projects table missing after initialization"
|
|
));
|
|
};
|
|
|
|
// Ensure the per-project entry is its own explicit table. If it exists but
|
|
// is not a table (e.g., an inline table), replace it with an explicit table.
|
|
let needs_proj_table = !projects_tbl.contains_key(project_key.as_str())
|
|
|| projects_tbl
|
|
.get(project_key.as_str())
|
|
.and_then(|i| i.as_table())
|
|
.is_none();
|
|
if needs_proj_table {
|
|
projects_tbl.insert(project_key.as_str(), toml_edit::table());
|
|
}
|
|
let Some(proj_tbl) = projects_tbl
|
|
.get_mut(project_key.as_str())
|
|
.and_then(|i| i.as_table_mut())
|
|
else {
|
|
return Err(anyhow::anyhow!("project table missing for {project_key}"));
|
|
};
|
|
proj_tbl.set_implicit(false);
|
|
proj_tbl["trust_level"] = toml_edit::value(trust_level.to_string());
|
|
Ok(())
|
|
}
|
|
|
|
/// Patch `CODEX_HOME/config.toml` project state to set trust level.
|
|
/// Use with caution.
|
|
pub fn set_project_trust_level(
|
|
codex_home: &Path,
|
|
project_path: &Path,
|
|
trust_level: TrustLevel,
|
|
) -> anyhow::Result<()> {
|
|
use crate::config::edit::ConfigEditsBuilder;
|
|
|
|
ConfigEditsBuilder::new(codex_home)
|
|
.set_project_trust_level(project_path, trust_level)
|
|
.apply_blocking()
|
|
}
|
|
|
|
/// Save the default OSS provider preference to config.toml
|
|
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
|
|
codex_config::config_toml::validate_oss_provider(provider)?;
|
|
use toml_edit::value;
|
|
|
|
let edits = [ConfigEdit::SetPath {
|
|
segments: vec!["oss_provider".to_string()],
|
|
value: value(provider),
|
|
}];
|
|
|
|
ConfigEditsBuilder::new(codex_home)
|
|
.with_edits(edits)
|
|
.apply_blocking()
|
|
.map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}")))
|
|
}
|
|
|
|
#[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<String>,
|
|
/// Path to a role-specific config layer.
|
|
pub config_file: Option<PathBuf>,
|
|
/// Candidate nicknames for agents spawned with this role.
|
|
pub nickname_candidates: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct CustomPermissionProfileSummary {
|
|
pub id: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
fn resolve_tool_suggest_config(
|
|
config_toml: &ConfigToml,
|
|
config_layer_stack: &ConfigLayerStack,
|
|
) -> ToolSuggestConfig {
|
|
resolve_tool_suggest_config_from_config(config_toml.tool_suggest.as_ref(), config_layer_stack)
|
|
}
|
|
|
|
pub(crate) fn resolve_tool_suggest_config_from_layer_stack(
|
|
config_layer_stack: &ConfigLayerStack,
|
|
) -> ToolSuggestConfig {
|
|
let tool_suggest = config_layer_stack
|
|
.effective_config()
|
|
.get("tool_suggest")
|
|
.cloned()
|
|
.and_then(|value| value.try_into::<ToolSuggestConfig>().ok());
|
|
resolve_tool_suggest_config_from_config(tool_suggest.as_ref(), config_layer_stack)
|
|
}
|
|
|
|
fn resolve_tool_suggest_config_from_config(
|
|
tool_suggest: Option<&ToolSuggestConfig>,
|
|
config_layer_stack: &ConfigLayerStack,
|
|
) -> ToolSuggestConfig {
|
|
let discoverables = tool_suggest
|
|
.into_iter()
|
|
.flat_map(|tool_suggest| tool_suggest.discoverables.iter())
|
|
.filter_map(|discoverable| {
|
|
let trimmed = discoverable.id.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(ToolSuggestDiscoverable {
|
|
kind: discoverable.kind,
|
|
id: trimmed.to_string(),
|
|
})
|
|
}
|
|
})
|
|
.collect();
|
|
let mut seen_disabled_tools = HashSet::new();
|
|
let mut disabled_tools = Vec::new();
|
|
let mut add_disabled_tool = |disabled_tool: ToolSuggestDisabledTool| {
|
|
if let Some(disabled_tool) = disabled_tool.normalized()
|
|
&& seen_disabled_tools.insert(disabled_tool.clone())
|
|
{
|
|
disabled_tools.push(disabled_tool);
|
|
}
|
|
};
|
|
|
|
let 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::<ToolSuggestConfig>().ok())
|
|
else {
|
|
continue;
|
|
};
|
|
for disabled_tool in tool_suggest.disabled_tools {
|
|
add_disabled_tool(disabled_tool);
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolSuggestConfig {
|
|
discoverables,
|
|
disabled_tools,
|
|
}
|
|
}
|
|
|
|
fn thread_store_config(thread_store: Option<ThreadStoreToml>) -> ThreadStoreConfig {
|
|
match thread_store {
|
|
Some(ThreadStoreToml::Local {}) => ThreadStoreConfig::Local,
|
|
Some(ThreadStoreToml::InMemory { id }) => ThreadStoreConfig::InMemory { id },
|
|
None => ThreadStoreConfig::Local,
|
|
}
|
|
}
|
|
|
|
fn is_session_layer(source: &ConfigLayerSource) -> bool {
|
|
matches!(source, ConfigLayerSource::SessionFlags)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum PermissionConfigSyntax {
|
|
Legacy,
|
|
Profiles,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default)]
|
|
struct PermissionSelectionToml {
|
|
default_permissions: Option<String>,
|
|
sandbox_mode: Option<SandboxMode>,
|
|
}
|
|
|
|
// Resolve the named-profile catalog and selected profile id together. Runtime
|
|
// profile constraints are applied later after this selection compiles into a
|
|
// concrete `PermissionProfile`.
|
|
#[derive(Debug)]
|
|
struct EffectivePermissionSelection<'a> {
|
|
profiles: Option<PermissionsToml>,
|
|
selected_profile_id: Option<&'a str>,
|
|
requirements_force_profile_selection: bool,
|
|
}
|
|
|
|
impl EffectivePermissionSelection<'_> {
|
|
fn has_profiles(&self) -> bool {
|
|
self.profiles
|
|
.as_ref()
|
|
.is_some_and(|profiles| !profiles.is_empty())
|
|
}
|
|
|
|
fn profiles_are_active(
|
|
&self,
|
|
default_permissions_override: Option<&str>,
|
|
permission_config_syntax: Option<PermissionConfigSyntax>,
|
|
) -> bool {
|
|
self.requirements_force_profile_selection
|
|
|| default_permissions_override.is_some()
|
|
|| matches!(
|
|
permission_config_syntax,
|
|
Some(PermissionConfigSyntax::Profiles)
|
|
)
|
|
|| permission_config_syntax.is_none()
|
|
}
|
|
}
|
|
|
|
fn resolve_permission_config_syntax(
|
|
config_layer_stack: &ConfigLayerStack,
|
|
cfg: &ConfigToml,
|
|
sandbox_mode_override: Option<SandboxMode>,
|
|
) -> Option<PermissionConfigSyntax> {
|
|
if sandbox_mode_override.is_some() {
|
|
return Some(PermissionConfigSyntax::Legacy);
|
|
}
|
|
|
|
let session_flags_select_profiles = config_layer_stack
|
|
.get_layers(
|
|
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
)
|
|
.into_iter()
|
|
.find(|layer| matches!(layer.name, ConfigLayerSource::SessionFlags))
|
|
.and_then(|layer| {
|
|
layer
|
|
.config
|
|
.clone()
|
|
.try_into::<PermissionSelectionToml>()
|
|
.ok()
|
|
})
|
|
.is_some_and(|selection| selection.default_permissions.is_some());
|
|
if session_flags_select_profiles {
|
|
return Some(PermissionConfigSyntax::Profiles);
|
|
}
|
|
|
|
let mut selection = None;
|
|
for layer in config_layer_stack.get_layers(
|
|
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
) {
|
|
let Ok(layer_selection) = layer.config.clone().try_into::<PermissionSelectionToml>() else {
|
|
continue;
|
|
};
|
|
|
|
if layer_selection.sandbox_mode.is_some() {
|
|
selection = Some(PermissionConfigSyntax::Legacy);
|
|
}
|
|
if layer_selection.default_permissions.is_some() {
|
|
selection = Some(PermissionConfigSyntax::Profiles);
|
|
}
|
|
}
|
|
|
|
selection.or_else(|| {
|
|
if cfg.default_permissions.is_some() {
|
|
Some(PermissionConfigSyntax::Profiles)
|
|
} else if cfg.sandbox_mode.is_some() {
|
|
Some(PermissionConfigSyntax::Legacy)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
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::Deny,
|
|
}
|
|
} 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::Deny,
|
|
}
|
|
};
|
|
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<String>,
|
|
pub review_model: Option<String>,
|
|
pub cwd: Option<PathBuf>,
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
|
pub sandbox_mode: Option<SandboxMode>,
|
|
pub permission_profile: Option<PermissionProfile>,
|
|
pub default_permissions: Option<String>,
|
|
pub model_provider: Option<String>,
|
|
pub service_tier: Option<Option<String>>,
|
|
pub codex_self_exe: Option<PathBuf>,
|
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
|
pub main_execve_wrapper_exe: Option<PathBuf>,
|
|
pub default_zsh_path: Option<AbsolutePathBuf>,
|
|
pub base_instructions: Option<String>,
|
|
pub developer_instructions: Option<String>,
|
|
pub personality: Option<Personality>,
|
|
pub compact_prompt: Option<String>,
|
|
pub show_raw_agent_reasoning: Option<bool>,
|
|
pub tools_web_search_request: Option<bool>,
|
|
pub ephemeral: Option<bool>,
|
|
pub bypass_hook_trust: Option<bool>,
|
|
/// Additional directories that should be treated as writable roots for this session.
|
|
pub additional_writable_roots: Vec<PathBuf>,
|
|
/// Explicit runtime workspace roots for this session. When set, this is
|
|
/// the full runtime root list rather than an additive override.
|
|
pub workspace_roots: Option<Vec<PathBuf>>,
|
|
}
|
|
|
|
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
|
|
let mut seen = HashSet::new();
|
|
paths.retain(|path| seen.insert(path.clone()));
|
|
}
|
|
|
|
/// Resolves the OSS provider from CLI override or global config.
|
|
/// Returns `None` if no provider is configured at any level.
|
|
pub fn resolve_oss_provider(
|
|
explicit_provider: Option<&str>,
|
|
config_toml: &ConfigToml,
|
|
) -> Option<String> {
|
|
if let Some(provider) = explicit_provider {
|
|
// Explicit provider specified (e.g., via --local-provider)
|
|
Some(provider.to_string())
|
|
} else {
|
|
config_toml.oss_provider.clone()
|
|
}
|
|
}
|
|
|
|
/// Resolve the web search mode from explicit config and feature flags.
|
|
fn resolve_web_search_mode(config_toml: &ConfigToml, features: &Features) -> Option<WebSearchMode> {
|
|
if let Some(mode) = config_toml.web_search {
|
|
return Some(mode);
|
|
}
|
|
if features.enabled(Feature::WebSearchCached) {
|
|
return Some(WebSearchMode::Cached);
|
|
}
|
|
if features.enabled(Feature::WebSearchRequest) {
|
|
return Some(WebSearchMode::Live);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn resolve_web_search_config(config_toml: &ConfigToml) -> Option<WebSearchConfig> {
|
|
config_toml
|
|
.tools
|
|
.as_ref()
|
|
.and_then(|tools| tools.web_search.as_ref())
|
|
.cloned()
|
|
.map(Into::into)
|
|
}
|
|
|
|
fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) -> bool {
|
|
config_toml
|
|
.tools
|
|
.as_ref()
|
|
.and_then(|tools| tools.experimental_request_user_input.as_ref())
|
|
.is_none_or(|config| config.enabled)
|
|
}
|
|
|
|
fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
|
|
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
|
|
let default = MultiAgentV2Config::default();
|
|
|
|
let max_concurrent_threads_per_session = base
|
|
.and_then(|config| config.max_concurrent_threads_per_session)
|
|
.unwrap_or(default.max_concurrent_threads_per_session);
|
|
let min_wait_timeout_ms = base
|
|
.and_then(|config| config.min_wait_timeout_ms)
|
|
.unwrap_or(default.min_wait_timeout_ms);
|
|
let max_wait_timeout_ms = base
|
|
.and_then(|config| config.max_wait_timeout_ms)
|
|
.unwrap_or(default.max_wait_timeout_ms);
|
|
let default_wait_timeout_ms = base
|
|
.and_then(|config| config.default_wait_timeout_ms)
|
|
.unwrap_or(default.default_wait_timeout_ms);
|
|
let usage_hint_enabled = base
|
|
.and_then(|config| config.usage_hint_enabled)
|
|
.unwrap_or(default.usage_hint_enabled);
|
|
let usage_hint_text = base
|
|
.and_then(|config| config.usage_hint_text.as_ref())
|
|
.cloned()
|
|
.or(default.usage_hint_text);
|
|
let root_agent_usage_hint_text = resolve_optional_prompt_text(
|
|
base.map(|config| &config.root_agent_usage_hint_text),
|
|
default.root_agent_usage_hint_text,
|
|
);
|
|
let subagent_usage_hint_text = resolve_optional_prompt_text(
|
|
base.map(|config| &config.subagent_usage_hint_text),
|
|
default.subagent_usage_hint_text,
|
|
);
|
|
let tool_namespace = base
|
|
.and_then(|config| config.tool_namespace.as_ref())
|
|
.cloned()
|
|
.or(default.tool_namespace);
|
|
let hide_spawn_agent_metadata = base
|
|
.and_then(|config| config.hide_spawn_agent_metadata)
|
|
.unwrap_or(default.hide_spawn_agent_metadata);
|
|
let non_code_mode_only = base
|
|
.and_then(|config| config.non_code_mode_only)
|
|
.unwrap_or(default.non_code_mode_only);
|
|
|
|
MultiAgentV2Config {
|
|
max_concurrent_threads_per_session,
|
|
min_wait_timeout_ms,
|
|
max_wait_timeout_ms,
|
|
default_wait_timeout_ms,
|
|
usage_hint_enabled,
|
|
usage_hint_text,
|
|
root_agent_usage_hint_text,
|
|
subagent_usage_hint_text,
|
|
tool_namespace,
|
|
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 resolve_optional_prompt_text(
|
|
configured: Option<&Option<String>>,
|
|
default: Option<String>,
|
|
) -> Option<String> {
|
|
match configured {
|
|
Some(Some(value)) if value.is_empty() => None,
|
|
Some(Some(value)) => Some(value.clone()),
|
|
Some(None) | None => default,
|
|
}
|
|
}
|
|
|
|
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<WebSearchMode>,
|
|
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(())
|
|
}
|
|
|
|
fn validate_multi_agent_v2_tool_namespace(namespace: Option<&str>) -> std::io::Result<()> {
|
|
const LABEL: &str = "features.multi_agent_v2.tool_namespace";
|
|
const MAX_LEN: usize = 64;
|
|
const RESERVED_RESPONSES_NAMESPACES: &[&str] = &[
|
|
"api_tool",
|
|
"browser",
|
|
"computer",
|
|
"container",
|
|
"file_search",
|
|
"functions",
|
|
"image_gen",
|
|
"multi_tool_use",
|
|
"python",
|
|
"python_user_visible",
|
|
"submodel_delegator",
|
|
"terminal",
|
|
"tool_search",
|
|
"web",
|
|
];
|
|
|
|
let Some(namespace) = namespace else {
|
|
return Ok(());
|
|
};
|
|
if namespace.is_empty() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("{LABEL} must not be empty"),
|
|
));
|
|
}
|
|
if namespace.trim() != namespace {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("{LABEL} must not have leading or trailing whitespace"),
|
|
));
|
|
}
|
|
if !namespace
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("{LABEL} must match ^[a-zA-Z0-9_-]+$"),
|
|
));
|
|
}
|
|
if namespace.chars().count() > MAX_LEN {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("{LABEL} must be at most {MAX_LEN} characters"),
|
|
));
|
|
}
|
|
if namespace == "mcp"
|
|
|| namespace.starts_with("mcp__")
|
|
|| RESERVED_RESPONSES_NAMESPACES.contains(&namespace)
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!("{LABEL} uses a reserved namespace: {namespace}"),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
impl Config {
|
|
#[cfg(test)]
|
|
async fn load_from_base_config_with_overrides(
|
|
cfg: ConfigToml,
|
|
overrides: ConfigOverrides,
|
|
codex_home: AbsolutePathBuf,
|
|
) -> std::io::Result<Self> {
|
|
// Note this ignores requirements.toml enforcement for tests.
|
|
let config_layer_stack = ConfigLayerStack::default();
|
|
Self::load_config_with_layer_stack(
|
|
LOCAL_FS.as_ref(),
|
|
cfg,
|
|
overrides,
|
|
codex_home,
|
|
config_layer_stack,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn load_config_with_layer_stack(
|
|
fs: &dyn ExecutorFileSystem,
|
|
cfg: ConfigToml,
|
|
overrides: ConfigOverrides,
|
|
codex_home: AbsolutePathBuf,
|
|
config_layer_stack: ConfigLayerStack,
|
|
) -> std::io::Result<Self> {
|
|
// 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,
|
|
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
|
|
web_search_mode: mut constrained_web_search_mode,
|
|
allow_managed_hooks_only: _,
|
|
allow_appshots: _,
|
|
computer_use: _,
|
|
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 mut startup_warnings = config_layer_stack
|
|
.startup_warnings()
|
|
.unwrap_or_default()
|
|
.to_vec();
|
|
let user_instructions = AgentsMdManager::load_global_instructions(
|
|
LOCAL_FS.as_ref(),
|
|
Some(&codex_home),
|
|
&mut startup_warnings,
|
|
)
|
|
.await
|
|
.map(|loaded| loaded.contents);
|
|
|
|
// 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,
|
|
codex_self_exe,
|
|
codex_linux_sandbox_exe,
|
|
main_execve_wrapper_exe,
|
|
default_zsh_path,
|
|
base_instructions,
|
|
developer_instructions,
|
|
personality,
|
|
compact_prompt,
|
|
show_raw_agent_reasoning,
|
|
tools_web_search_request: override_tools_web_search_request,
|
|
ephemeral,
|
|
bypass_hook_trust,
|
|
additional_writable_roots,
|
|
workspace_roots: workspace_roots_override,
|
|
} = overrides;
|
|
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
|
|
|
|
if bypass_hook_trust {
|
|
startup_warnings.push(
|
|
"`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
if sandbox_mode.is_some() && permission_profile.is_some() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"`sandbox_mode` and `permission_profile` overrides cannot both be set",
|
|
));
|
|
}
|
|
if sandbox_mode.is_some() && default_permissions_override.is_some() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"`sandbox_mode` and `default_permissions` overrides cannot both be set",
|
|
));
|
|
}
|
|
if permission_profile.is_some() && default_permissions_override.is_some() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"`permission_profile` and `default_permissions` overrides cannot both be set",
|
|
));
|
|
}
|
|
if let Some(profile) = cfg.profile.as_deref() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
format!(
|
|
"legacy `profile = \"{profile}\"` config is no longer supported; use `--profile {profile}` with `{profile}.config.toml` instead"
|
|
),
|
|
));
|
|
}
|
|
|
|
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
|
|
let feature_overrides = FeatureOverrides {
|
|
web_search_request: override_tools_web_search_request,
|
|
};
|
|
|
|
let configured_features = Features::from_sources(
|
|
FeatureConfigSource {
|
|
features: cfg.features.as_ref(),
|
|
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
|
|
},
|
|
FeatureConfigSource {
|
|
..Default::default()
|
|
},
|
|
feature_overrides,
|
|
);
|
|
let features = ManagedFeatures::from_configured_with_warnings(
|
|
configured_features,
|
|
feature_requirements,
|
|
&mut startup_warnings,
|
|
)?;
|
|
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
|
|
let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
|
|
// Keep the configured mode separate so a requirement-constrained mode
|
|
// does not look like it was explicitly selected in config.
|
|
let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| {
|
|
match WindowsSandboxLevel::from_features(&features) {
|
|
WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated),
|
|
WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated),
|
|
WindowsSandboxLevel::Disabled => None,
|
|
}
|
|
});
|
|
apply_requirement_constrained_value(
|
|
"windows.sandbox",
|
|
selected_windows_sandbox_mode,
|
|
&mut constrained_windows_sandbox_mode,
|
|
&mut startup_warnings,
|
|
)?;
|
|
let effective_windows_sandbox_mode = *constrained_windows_sandbox_mode.get();
|
|
let windows_sandbox_mode = if constrained_windows_sandbox_mode.source.is_some() {
|
|
effective_windows_sandbox_mode
|
|
} else {
|
|
configured_windows_sandbox_mode
|
|
};
|
|
let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg);
|
|
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
|
|
use std::env;
|
|
|
|
match cwd {
|
|
None => {
|
|
tracing::info!("cwd not set, using current dir");
|
|
env::current_dir()?
|
|
}
|
|
Some(p) if p.is_absolute() => p,
|
|
Some(p) => {
|
|
// Resolve relative path against the current working directory.
|
|
tracing::info!("cwd is relative, resolving against current dir");
|
|
let mut current = env::current_dir()?;
|
|
current.push(p);
|
|
current
|
|
}
|
|
}
|
|
}))?;
|
|
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
|
.into_iter()
|
|
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
|
|
.collect();
|
|
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
|
|
let active_project = cfg
|
|
.get_active_project(
|
|
resolved_cwd.as_path(),
|
|
repo_root.as_ref().map(AbsolutePathBuf::as_path),
|
|
)
|
|
.unwrap_or(ProjectConfig { trust_level: None });
|
|
let permission_config_syntax = resolve_permission_config_syntax(
|
|
&config_layer_stack,
|
|
&cfg,
|
|
sandbox_mode,
|
|
);
|
|
let requirements_toml = config_layer_stack.requirements_toml();
|
|
let effective_permission_selection = resolve_effective_permission_selection(
|
|
cfg.permissions.as_ref(),
|
|
default_permissions_override.as_deref(),
|
|
cfg.default_permissions.as_deref(),
|
|
requirements_toml,
|
|
&mut startup_warnings,
|
|
)?;
|
|
if effective_permission_selection.has_profiles()
|
|
&& !matches!(
|
|
permission_config_syntax,
|
|
Some(PermissionConfigSyntax::Legacy)
|
|
)
|
|
&& effective_permission_selection.selected_profile_id.is_none()
|
|
&& !effective_permission_selection.requirements_force_profile_selection
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"config defines `[permissions]` profiles but does not set `default_permissions`",
|
|
));
|
|
}
|
|
|
|
let windows_sandbox_level = match effective_windows_sandbox_mode {
|
|
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
|
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
|
|
None => WindowsSandboxLevel::Disabled,
|
|
};
|
|
let memories_config: MemoriesConfig = cfg.memories.clone().unwrap_or_default().into();
|
|
let memories_root = memory_root(&codex_home);
|
|
|
|
let profiles_are_active = effective_permission_selection.profiles_are_active(
|
|
default_permissions_override.as_deref(),
|
|
permission_config_syntax,
|
|
);
|
|
let explicit_permission_profile_mode = default_permissions_override.is_some()
|
|
|| matches!(
|
|
permission_config_syntax,
|
|
Some(PermissionConfigSyntax::Profiles)
|
|
);
|
|
let custom_permission_profiles = cfg
|
|
.permissions
|
|
.as_ref()
|
|
.map_or_else(Vec::new, |permissions| {
|
|
permissions
|
|
.entries
|
|
.iter()
|
|
.map(|(id, profile)| CustomPermissionProfileSummary {
|
|
id: id.clone(),
|
|
description: profile.description.clone(),
|
|
})
|
|
.collect()
|
|
});
|
|
let using_implicit_builtin_profile = permission_config_syntax.is_none()
|
|
&& effective_permission_selection.selected_profile_id.is_none();
|
|
let should_seed_legacy_workspace_roots = effective_permission_selection
|
|
.selected_profile_id
|
|
.is_none()
|
|
&& matches!(
|
|
permission_config_syntax,
|
|
None | Some(PermissionConfigSyntax::Legacy)
|
|
);
|
|
let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots
|
|
&& cfg
|
|
.sandbox_workspace_write
|
|
.as_ref()
|
|
.is_some_and(|sandbox_workspace_write| {
|
|
!sandbox_workspace_write.writable_roots.is_empty()
|
|
});
|
|
let workspace_roots_explicit = workspace_roots_override.is_some()
|
|
|| !requested_additional_writable_roots.is_empty()
|
|
|| legacy_workspace_roots_explicit;
|
|
let mut workspace_roots = match workspace_roots_override {
|
|
Some(workspace_roots) => workspace_roots
|
|
.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(permission_profile) = permission_profile {
|
|
let (file_system_sandbox_policy, _network_sandbox_policy) =
|
|
permission_profile.to_runtime_permissions();
|
|
let configured_network_proxy_config =
|
|
if profile_allows_configured_network_proxy(&permission_profile)
|
|
&& profiles_are_active
|
|
{
|
|
// PermissionProfile carries the active network sandbox bit, not the configured
|
|
// proxy/allowlist policy. Keep that config so active profiles can round-trip
|
|
// without broadening network behavior.
|
|
let default_permissions = effective_permission_selection
|
|
.selected_profile_id
|
|
.unwrap_or_else(|| {
|
|
default_builtin_permission_profile_name(
|
|
&active_project,
|
|
windows_sandbox_level,
|
|
)
|
|
});
|
|
network_proxy_config_for_profile_selection(
|
|
effective_permission_selection.profiles.as_ref(),
|
|
default_permissions,
|
|
)?
|
|
} else {
|
|
NetworkProxyConfig::default()
|
|
};
|
|
(
|
|
configured_network_proxy_config,
|
|
permission_profile,
|
|
file_system_sandbox_policy,
|
|
None,
|
|
Vec::new(),
|
|
)
|
|
} else if profiles_are_active {
|
|
let default_permissions = effective_permission_selection
|
|
.selected_profile_id
|
|
.unwrap_or_else(|| {
|
|
default_builtin_permission_profile_name(&active_project, 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(
|
|
effective_permission_selection.profiles.as_ref(),
|
|
default_permissions,
|
|
)?;
|
|
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
|
compile_permission_profile_selection(
|
|
effective_permission_selection.profiles.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(
|
|
effective_permission_selection.profiles.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);
|
|
file_system_sandbox_policy = file_system_sandbox_policy
|
|
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
|
|
let 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 active_permission_profile = if using_implicit_builtin_profile
|
|
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
|
|
&& cfg.sandbox_workspace_write.is_some()
|
|
{
|
|
// The implicit built-in profile preserves legacy
|
|
// `[sandbox_workspace_write]` customizations, but explicitly
|
|
// selecting `:workspace` intentionally ignores those legacy
|
|
// settings. Do not advertise a re-selectable active profile
|
|
// when doing so would lose roots, network, or tmp settings.
|
|
None
|
|
} else {
|
|
let selected_profile_extends = cfg
|
|
.permissions
|
|
.as_ref()
|
|
.and_then(|permissions| permissions.entries.get(default_permissions))
|
|
.and_then(|profile| profile.extends.clone());
|
|
Some(ActivePermissionProfile {
|
|
id: default_permissions.to_string(),
|
|
extends: selected_profile_extends,
|
|
})
|
|
};
|
|
(
|
|
configured_network_proxy_config,
|
|
permission_profile,
|
|
file_system_sandbox_policy,
|
|
active_permission_profile,
|
|
configured_workspace_roots,
|
|
)
|
|
} else {
|
|
let configured_network_proxy_config = NetworkProxyConfig::default();
|
|
// No named `[permissions]` profile is active, but permissions
|
|
// should still flow through the canonical profile representation.
|
|
// Derive the old `sandbox_mode` defaults as a profile first, then
|
|
// keep a legacy-compatible projection only for the remaining code
|
|
// paths that still speak `SandboxPolicy`.
|
|
let mut permission_profile = cfg
|
|
.derive_permission_profile(
|
|
sandbox_mode,
|
|
windows_sandbox_level,
|
|
Some(&active_project),
|
|
Some(&constrained_permission_profile),
|
|
)
|
|
.await;
|
|
// The legacy-derived profiles above are expected to be
|
|
// representable as `SandboxPolicy`. This guard keeps the old safe
|
|
// fallback behavior if future changes make this branch derive a
|
|
// profile with split-only filesystem semantics, such as root write
|
|
// with carveouts or writes that are not expressible as
|
|
// workspace-write roots.
|
|
if let Err(err) = permission_profile.to_legacy_sandbox_policy(resolved_cwd.as_path()) {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"derived permission profile cannot be represented as a legacy sandbox policy; falling back to read-only"
|
|
);
|
|
permission_profile = PermissionProfile::read_only();
|
|
}
|
|
let (file_system_sandbox_policy, _network_sandbox_policy) =
|
|
permission_profile.to_runtime_permissions();
|
|
(
|
|
configured_network_proxy_config,
|
|
permission_profile,
|
|
file_system_sandbox_policy,
|
|
None,
|
|
Vec::new(),
|
|
)
|
|
};
|
|
if 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,
|
|
);
|
|
}
|
|
configured_network_proxy_config.network.enabled = true;
|
|
}
|
|
let approval_policy_was_explicit =
|
|
approval_policy_override.is_some() || cfg.approval_policy.is_some();
|
|
let mut approval_policy = approval_policy_override
|
|
.or(cfg.approval_policy)
|
|
.unwrap_or_else(|| {
|
|
if active_project.is_trusted() {
|
|
AskForApproval::OnRequest
|
|
} else if active_project.is_untrusted() {
|
|
AskForApproval::UnlessTrusted
|
|
} else {
|
|
AskForApproval::default()
|
|
}
|
|
});
|
|
if !approval_policy_was_explicit
|
|
&& let Err(err) = constrained_approval_policy.can_set(&approval_policy)
|
|
{
|
|
tracing::warn!(
|
|
error = %err,
|
|
"default approval policy is disallowed by requirements; falling back to required default"
|
|
);
|
|
approval_policy = constrained_approval_policy.value();
|
|
}
|
|
let approvals_reviewer_was_explicit =
|
|
approvals_reviewer_override.is_some() || cfg.approvals_reviewer.is_some();
|
|
let mut approvals_reviewer = approvals_reviewer_override
|
|
.or(cfg.approvals_reviewer)
|
|
.unwrap_or(ApprovalsReviewer::User);
|
|
if !approvals_reviewer_was_explicit
|
|
&& let Err(err) = constrained_approvals_reviewer.can_set(&approvals_reviewer)
|
|
{
|
|
tracing::warn!(
|
|
error = %err,
|
|
"default approvals reviewer is disallowed by requirements; falling back to required default"
|
|
);
|
|
approvals_reviewer = constrained_approvals_reviewer.value();
|
|
}
|
|
let web_search_mode =
|
|
resolve_web_search_mode(&cfg, &features).unwrap_or(WebSearchMode::Cached);
|
|
let web_search_config = resolve_web_search_config(&cfg);
|
|
let experimental_request_user_input_enabled =
|
|
resolve_experimental_request_user_input_enabled(&cfg);
|
|
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
|
|
let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) {
|
|
let base = apps_mcp_path_override_toml_config(cfg.features.as_ref());
|
|
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(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",
|
|
));
|
|
}
|
|
validate_multi_agent_v2_tool_namespace(multi_agent_v2.tool_namespace.as_deref())?;
|
|
let agent_max_threads = cfg.agents.as_ref().and_then(|agents| agents.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",
|
|
));
|
|
}
|
|
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::<Vec<_>>()
|
|
})
|
|
.filter(|values| !values.is_empty());
|
|
|
|
let forced_login_method = cfg.forced_login_method;
|
|
|
|
let model = model.or(cfg.model);
|
|
let notices = cfg.notice.unwrap_or_default();
|
|
let service_tier = match service_tier_override {
|
|
Some(Some(service_tier)) => Some(service_tier),
|
|
Some(None) => Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
|
None => cfg.service_tier,
|
|
};
|
|
let service_tier = service_tier.and_then(|service_tier| {
|
|
match ServiceTier::from_request_value(&service_tier) {
|
|
Some(ServiceTier::Fast) => features
|
|
.enabled(Feature::FastMode)
|
|
.then(|| ServiceTier::Fast.request_value().to_string()),
|
|
Some(ServiceTier::Flex) => Some(ServiceTier::Flex.request_value().to_string()),
|
|
None => Some(service_tier),
|
|
}
|
|
});
|
|
|
|
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
});
|
|
|
|
// Load base instructions override from a file if specified. If the
|
|
// path is relative, resolve it against the effective cwd so the
|
|
// behaviour matches other path-like config values.
|
|
let model_instructions_path = cfg.model_instructions_file.as_ref();
|
|
let file_base_instructions = Self::try_read_non_empty_file(
|
|
fs,
|
|
model_instructions_path,
|
|
"model instructions file",
|
|
)
|
|
.await?;
|
|
let base_instructions = base_instructions
|
|
.or(file_base_instructions)
|
|
.or(cfg.instructions.clone());
|
|
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
|
let include_permissions_instructions = cfg.include_permissions_instructions.unwrap_or(true);
|
|
let include_apps_instructions = cfg.include_apps_instructions.unwrap_or(true);
|
|
let include_collaboration_mode_instructions =
|
|
cfg.include_collaboration_mode_instructions.unwrap_or(true);
|
|
let include_skill_instructions = cfg
|
|
.skills
|
|
.as_ref()
|
|
.and_then(|skills| skills.include_instructions)
|
|
.unwrap_or(true);
|
|
let include_environment_context = 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(cfg.personality)
|
|
.or_else(|| {
|
|
features
|
|
.enabled(Feature::Personality)
|
|
.then_some(Personality::Pragmatic)
|
|
});
|
|
|
|
let experimental_compact_prompt_path = cfg.experimental_compact_prompt_file.as_ref();
|
|
let file_compact_prompt = Self::try_read_non_empty_file(
|
|
fs,
|
|
experimental_compact_prompt_path,
|
|
"experimental compact prompt file",
|
|
)
|
|
.await?;
|
|
let compact_prompt = compact_prompt.or(file_compact_prompt);
|
|
let zsh_path = default_zsh_path
|
|
.or_else(|| InstallContext::current().bundled_zsh_path())
|
|
.map(AbsolutePathBuf::into_path_buf);
|
|
|
|
let review_model = override_review_model.or(cfg.review_model);
|
|
|
|
let check_for_update_on_startup = cfg.check_for_update_on_startup.unwrap_or(true);
|
|
let model_catalog = load_model_catalog(cfg.model_catalog_json.clone())?;
|
|
|
|
let log_dir = cfg
|
|
.log_dir
|
|
.as_ref()
|
|
.map(AbsolutePathBuf::to_path_buf)
|
|
.unwrap_or_else(|| codex_home.join("log").to_path_buf());
|
|
let sqlite_home = 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
|
|
&& sandbox_mode_requirement_for_permission_profile(&original_permission_profile)
|
|
== SandboxModeRequirement::DangerFullAccess
|
|
&& constrained_permission_profile.get() == &PermissionProfile::read_only()
|
|
&& constrained_approval_policy.value() == AskForApproval::Never
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode.",
|
|
));
|
|
}
|
|
if permission_profile_was_constrained {
|
|
// The selected profile no longer describes the effective
|
|
// permissions after requirements forced a fallback.
|
|
active_permission_profile = None;
|
|
profile_workspace_roots.clear();
|
|
}
|
|
apply_requirement_constrained_value(
|
|
"web_search_mode",
|
|
web_search_mode,
|
|
&mut constrained_web_search_mode,
|
|
&mut startup_warnings,
|
|
)?;
|
|
|
|
let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
|
|
|
|
let network_permission_profile = constrained_permission_profile.get().clone();
|
|
let network = build_network_proxy_spec(
|
|
configured_network_proxy_config,
|
|
network_requirements,
|
|
&network_permission_profile,
|
|
)?;
|
|
let mut helper_readable_roots = get_readable_roots_required_for_codex_runtime(
|
|
&codex_home,
|
|
zsh_path.as_ref(),
|
|
main_execve_wrapper_exe.as_ref(),
|
|
);
|
|
if features.enabled(Feature::MemoryTool) && memories_config.use_memories {
|
|
helper_readable_roots.push(memories_root);
|
|
}
|
|
let effective_permission_profile = constrained_permission_profile.value.get().clone();
|
|
let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) =
|
|
effective_permission_profile.to_runtime_permissions();
|
|
if effective_permission_profile != original_permission_profile {
|
|
effective_file_system_sandbox_policy
|
|
.preserve_deny_read_restrictions_from(&file_system_sandbox_policy);
|
|
}
|
|
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_auto_compact_token_limit_scope: cfg
|
|
.model_auto_compact_token_limit_scope
|
|
.unwrap_or_default(),
|
|
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,
|
|
},
|
|
explicit_permission_profile_mode,
|
|
custom_permission_profiles,
|
|
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: memories_config,
|
|
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: cfg.model_reasoning_effort,
|
|
plan_mode_reasoning_effort: cfg.plan_mode_reasoning_effort,
|
|
model_reasoning_summary: cfg.model_reasoning_summary,
|
|
model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries,
|
|
model_catalog,
|
|
model_verbosity: cfg.model_verbosity,
|
|
chatgpt_base_url: cfg
|
|
.chatgpt_base_url
|
|
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
|
apps_mcp_path_override,
|
|
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
|
|
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,
|
|
experimental_request_user_input_enabled,
|
|
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_project,
|
|
notices,
|
|
check_for_update_on_startup,
|
|
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
|
analytics_enabled: cfg.analytics.as_ref().and_then(|a| a.enabled),
|
|
feedback_enabled: cfg
|
|
.feedback
|
|
.as_ref()
|
|
.and_then(|feedback| feedback.enabled)
|
|
.unwrap_or(true),
|
|
tool_suggest,
|
|
tui_notifications: cfg
|
|
.tui
|
|
.as_ref()
|
|
.map(|t| t.notification_settings.clone())
|
|
.unwrap_or_default(),
|
|
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
|
|
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: 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<Option<String>> {
|
|
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(crate) fn network_proxy_spec_for_active_permission_profile(
|
|
&self,
|
|
active_permission_profile: &ActivePermissionProfile,
|
|
permission_profile: &PermissionProfile,
|
|
) -> std::io::Result<Option<NetworkProxySpec>> {
|
|
let profile_allows_network_proxy =
|
|
profile_allows_configured_network_proxy(permission_profile);
|
|
let configured_network_proxy_config = if profile_allows_network_proxy {
|
|
let cfg: ConfigToml = self
|
|
.config_layer_stack
|
|
.effective_config()
|
|
.try_into()
|
|
.map_err(|err| {
|
|
std::io::Error::new(
|
|
ErrorKind::InvalidInput,
|
|
format!(
|
|
"failed to read effective config for selected permission profile: {err}"
|
|
),
|
|
)
|
|
})?;
|
|
let mut configured_network_proxy_config = network_proxy_config_for_profile_selection(
|
|
cfg.permissions.as_ref(),
|
|
active_permission_profile.id.as_str(),
|
|
)?;
|
|
if self.features.enabled(Feature::NetworkProxy)
|
|
&& 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,
|
|
);
|
|
}
|
|
configured_network_proxy_config.network.enabled = true;
|
|
}
|
|
configured_network_proxy_config
|
|
} else {
|
|
NetworkProxyConfig::default()
|
|
};
|
|
|
|
build_network_proxy_spec(
|
|
configured_network_proxy_config,
|
|
self.config_layer_stack.requirements().network.clone(),
|
|
permission_profile,
|
|
)
|
|
}
|
|
|
|
pub fn bundled_skills_enabled(&self) -> bool {
|
|
crate::manager::bundled_skills_enabled_from_stack(&self.config_layer_stack)
|
|
}
|
|
}
|
|
|
|
fn guardian_policy_config_from_requirements(
|
|
requirements_toml: &ConfigRequirementsToml,
|
|
) -> Option<String> {
|
|
normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref())
|
|
}
|
|
|
|
fn merge_managed_permission_profiles(
|
|
configured_permissions: Option<&PermissionsToml>,
|
|
requirements_toml: &ConfigRequirementsToml,
|
|
) -> std::io::Result<Option<PermissionsToml>> {
|
|
let managed_profiles = requirements_toml
|
|
.permissions
|
|
.as_ref()
|
|
.map(|permissions| &permissions.profiles)
|
|
.filter(|profiles| !profiles.is_empty());
|
|
let Some(managed_profiles) = managed_profiles else {
|
|
return Ok(configured_permissions.cloned());
|
|
};
|
|
|
|
let mut merged_permissions = configured_permissions.cloned().unwrap_or_default();
|
|
for (profile_id, managed_profile) in managed_profiles {
|
|
if merged_permissions.entries.contains_key(profile_id) {
|
|
return Err(std::io::Error::new(
|
|
ErrorKind::InvalidInput,
|
|
format!(
|
|
"requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name"
|
|
),
|
|
));
|
|
}
|
|
merged_permissions
|
|
.entries
|
|
.insert(profile_id.clone(), managed_profile.clone());
|
|
}
|
|
|
|
Ok(Some(merged_permissions))
|
|
}
|
|
|
|
fn resolve_effective_permission_selection<'a>(
|
|
configured_permissions: Option<&PermissionsToml>,
|
|
default_permissions_override: Option<&'a str>,
|
|
configured_default_permissions: Option<&'a str>,
|
|
requirements_toml: &'a ConfigRequirementsToml,
|
|
startup_warnings: &mut Vec<String>,
|
|
) -> std::io::Result<EffectivePermissionSelection<'a>> {
|
|
let profiles = merge_managed_permission_profiles(configured_permissions, requirements_toml)?;
|
|
validate_user_permission_profile_names(profiles.as_ref())?;
|
|
validate_required_permission_profile_catalog(requirements_toml, profiles.as_ref())?;
|
|
let selected_profile_id = resolve_default_permissions(
|
|
default_permissions_override,
|
|
configured_default_permissions,
|
|
requirements_toml,
|
|
startup_warnings,
|
|
)?;
|
|
|
|
Ok(EffectivePermissionSelection {
|
|
profiles,
|
|
selected_profile_id,
|
|
requirements_force_profile_selection: requirements_toml.allowed_permissions.is_some(),
|
|
})
|
|
}
|
|
|
|
fn resolve_default_permissions<'a>(
|
|
default_permissions_override: Option<&'a str>,
|
|
configured_default_permissions: Option<&'a str>,
|
|
requirements_toml: &'a ConfigRequirementsToml,
|
|
startup_warnings: &mut Vec<String>,
|
|
) -> std::io::Result<Option<&'a str>> {
|
|
let allowed_permissions = requirements_toml.allowed_permissions.as_ref();
|
|
let mut default_permissions = default_permissions_override.or(configured_default_permissions);
|
|
if let (Some(selected_permissions), Some(allowed_permissions)) =
|
|
(default_permissions, allowed_permissions)
|
|
&& !is_builtin_permission_profile_name(selected_permissions)
|
|
&& !allowed_permissions
|
|
.iter()
|
|
.any(|allowed_permission| allowed_permission == selected_permissions)
|
|
{
|
|
let Some(fallback_permissions) = allowed_permissions.first().map(String::as_str) else {
|
|
return Err(std::io::Error::new(
|
|
ErrorKind::InvalidInput,
|
|
"requirements.toml allowed_permissions must include at least one profile",
|
|
));
|
|
};
|
|
startup_warnings.push(format!(
|
|
"Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`."
|
|
));
|
|
default_permissions = Some(fallback_permissions);
|
|
}
|
|
|
|
Ok(default_permissions)
|
|
}
|
|
|
|
fn validate_required_permission_profile_catalog(
|
|
requirements_toml: &ConfigRequirementsToml,
|
|
available_permissions: Option<&PermissionsToml>,
|
|
) -> std::io::Result<()> {
|
|
let is_known_profile = |profile_id: &str| {
|
|
is_builtin_permission_profile_name(profile_id)
|
|
|| available_permissions
|
|
.as_ref()
|
|
.is_some_and(|permissions| permissions.entries.contains_key(profile_id))
|
|
};
|
|
|
|
let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else {
|
|
return Ok(());
|
|
};
|
|
if allowed_permissions.is_empty() {
|
|
return Err(std::io::Error::new(
|
|
ErrorKind::InvalidInput,
|
|
"requirements.toml allowed_permissions must include at least one profile",
|
|
));
|
|
}
|
|
|
|
for profile_id in allowed_permissions {
|
|
if !is_known_profile(profile_id) {
|
|
return Err(std::io::Error::new(
|
|
ErrorKind::InvalidInput,
|
|
format!(
|
|
"requirements.toml allowed_permissions refers to undefined profile `{profile_id}`"
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn normalize_guardian_policy_config(value: Option<&str>) -> Option<String> {
|
|
value.and_then(|value| {
|
|
let trimmed = value.trim();
|
|
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
|
})
|
|
}
|
|
|
|
/// Returns the path to the Codex configuration directory, which can be
|
|
/// specified by the `CODEX_HOME` environment variable. If not set, defaults to
|
|
/// `~/.codex`.
|
|
///
|
|
/// - If `CODEX_HOME` is set, the value must exist and be a directory. The
|
|
/// value will be canonicalized and this function will Err otherwise.
|
|
/// - If `CODEX_HOME` is not set, this function does not verify that the
|
|
/// directory exists.
|
|
pub fn find_codex_home() -> std::io::Result<AbsolutePathBuf> {
|
|
codex_utils_home_dir::find_codex_home()
|
|
}
|
|
|
|
/// Returns the path to the folder where Codex logs are stored. Does not verify
|
|
/// that the directory exists.
|
|
pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
|
|
Ok(cfg.log_dir.clone())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "config_tests.rs"]
|
|
mod tests;
|
|
|
|
#[cfg(test)]
|
|
#[path = "config_loader_tests.rs"]
|
|
mod config_loader_tests;
|