config: remove legacy profile v1 resolution (#24051)

## Why

[#23883](https://github.com/openai/codex/pull/23883) moved user-facing
`--profile` selection onto profile v2, and
[#23886](https://github.com/openai/codex/pull/23886) removed the old CLI
`config_profile` override path. Core still had a second legacy path:
`profile = "..."` could select `[profiles.*]` values while runtime
config was built. Keeping that resolver alive preserves the old
precedence model and profile-carrying surfaces even though profile
selection now points at `$CODEX_HOME/<name>.config.toml`.

## What

- Reject legacy top-level `profile = "..."` config while loading runtime
config, with an error that points callers at `--profile <name>` and
`<name>.config.toml` in the [core load
path](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/mod.rs#L2524-L2531).
- Remove the remaining profile-v1 merge points from runtime config
resolution, including features, permissions, model/provider selection,
web search, Windows sandbox settings, TUI settings, role reloads, and
OSS provider lookup.
- Drop the leftover profile override surface from
[`ConfigOverrides`](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/mod.rs#L2118-L2148)
and from the MCP server `codex` tool schema.
- Prune profile-precedence tests that only exercised the removed
resolver and replace them with rejection coverage for the legacy
selector.

## Testing

- Not run in this metadata pass.
- Added
[`legacy_profile_selection_is_rejected`](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/config_tests.rs#L7942-L7965)
coverage for the new runtime guard.
This commit is contained in:
jif-oai
2026-05-22 12:13:52 +02:00
committed by GitHub
Unverified
parent ed80e5f558
commit fd72e99384
10 changed files with 110 additions and 1766 deletions
-42
View File
@@ -979,48 +979,6 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn explicit_permission_profile_overrides_active_profile_sandbox_mode()
-> anyhow::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"profile = \"legacy\"\n\
\n\
[profiles.legacy]\n\
sandbox_mode = \"danger-full-access\"\n",
)?;
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
DebugSandboxConfigOptions {
permissions_profile: Some(":workspace".to_string()),
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Ignore,
},
Some(codex_home.path().to_path_buf()),
/*strict_config*/ false,
)
.await?;
let actual = config
.permissions
.permission_profile()
.file_system_sandbox_policy();
let expected = codex_protocol::models::PermissionProfile::workspace_write()
.file_system_sandbox_policy();
assert!(
expected
.entries
.iter()
.all(|entry| actual.entries.contains(entry)),
"explicit workspace profile should preserve the built-in workspace rules"
);
Ok(())
}
#[tokio::test]
async fn debug_sandbox_honors_explicit_named_permission_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
+4 -72
View File
@@ -71,14 +71,12 @@ async fn apply_role_to_config_inner(
{
return Ok(());
}
let (preserve_current_profile, preserve_current_provider) =
preservation_policy(config, &role_layer_toml);
let preserve_current_provider = role_layer_toml.get("model_provider").is_none();
let preserve_current_service_tier = role_layer_toml.get("service_tier").is_none();
*config = reload::build_next_config(
config,
role_layer_toml,
preserve_current_profile,
preserve_current_provider,
preserve_current_service_tier,
)
@@ -130,48 +128,19 @@ pub(crate) fn resolve_role_config<'a>(
.or_else(|| built_in::configs().get(role_name))
}
fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, bool) {
let role_selects_provider = role_layer_toml.get("model_provider").is_some();
let role_selects_profile = role_layer_toml.get("profile").is_some();
let role_updates_active_profile_provider = config
.active_profile
.as_ref()
.and_then(|active_profile| {
role_layer_toml
.get("profiles")
.and_then(TomlValue::as_table)
.and_then(|profiles| profiles.get(active_profile))
.and_then(TomlValue::as_table)
.map(|profile| profile.contains_key("model_provider"))
})
.unwrap_or(false);
let preserve_current_profile = !role_selects_provider && !role_selects_profile;
let preserve_current_provider =
preserve_current_profile && !role_updates_active_profile_provider;
(preserve_current_profile, preserve_current_provider)
}
mod reload {
use super::*;
pub(super) async fn build_next_config(
config: &Config,
role_layer_toml: TomlValue,
preserve_current_profile: bool,
preserve_current_provider: bool,
preserve_current_service_tier: bool,
) -> anyhow::Result<Config> {
let active_profile_name = preserve_current_profile
.then_some(config.active_profile.as_deref())
.flatten();
let config_layer_stack =
build_config_layer_stack(config, &role_layer_toml, active_profile_name)?;
let mut merged_config = deserialize_effective_config(config, &config_layer_stack)?;
if preserve_current_profile {
merged_config.profile = None;
}
let config_layer_stack = build_config_layer_stack(config, &role_layer_toml)?;
let merged_config = deserialize_effective_config(config, &config_layer_stack)?;
let mut next_config = Config::load_config_with_layer_stack(
let next_config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
merged_config,
reload_overrides(
@@ -183,23 +152,14 @@ mod reload {
config_layer_stack,
)
.await?;
if preserve_current_profile {
next_config.active_profile = config.active_profile.clone();
}
Ok(next_config)
}
fn build_config_layer_stack(
config: &Config,
role_layer_toml: &TomlValue,
active_profile_name: Option<&str>,
) -> anyhow::Result<ConfigLayerStack> {
let mut layers = existing_layers(config);
if let Some(resolved_profile_layer) =
resolved_profile_layer(config, &layers, role_layer_toml, active_profile_name)?
{
insert_layer(&mut layers, resolved_profile_layer);
}
insert_layer(&mut layers, role_layer(role_layer_toml.clone()));
Ok(ConfigLayerStack::new(
layers,
@@ -208,34 +168,6 @@ mod reload {
)?)
}
fn resolved_profile_layer(
config: &Config,
existing_layers: &[ConfigLayerEntry],
role_layer_toml: &TomlValue,
active_profile_name: Option<&str>,
) -> anyhow::Result<Option<ConfigLayerEntry>> {
let Some(active_profile_name) = active_profile_name else {
return Ok(None);
};
let mut layers = existing_layers.to_vec();
insert_layer(&mut layers, role_layer(role_layer_toml.clone()));
let merged_config = deserialize_effective_config(
config,
&ConfigLayerStack::new(
layers,
config.config_layer_stack.requirements().clone(),
config.config_layer_stack.requirements_toml().clone(),
)?,
)?;
let resolved_profile =
merged_config.get_config_profile(Some(active_profile_name.to_string()))?;
Ok(Some(ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
TomlValue::try_from(resolved_profile)?,
)))
}
fn deserialize_effective_config(
config: &Config,
config_layer_stack: &ConfigLayerStack,
-298
View File
@@ -1,13 +1,10 @@
use super::*;
use crate::SkillsManager;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigBuilder;
use crate::skills_load_input_from_config;
use codex_config::ConfigLayerStackOrdering;
use codex_core_plugins::PluginsManager;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity;
use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::test_support::PathExt;
use pretty_assertions::assert_eq;
@@ -275,301 +272,6 @@ async fn apply_role_preserves_existing_service_tier_without_override() {
);
}
#[tokio::test]
async fn apply_role_preserves_active_profile_and_model_provider() {
let home = TempDir::new().expect("create temp dir");
tokio::fs::write(
home.path().join(CONFIG_TOML_FILE),
r#"
[model_providers.test-provider]
name = "Test Provider"
base_url = "https://example.com/v1"
env_key = "TEST_PROVIDER_API_KEY"
wire_api = "responses"
[profiles.test-profile]
model_provider = "test-provider"
"#,
)
.await
.expect("write config.toml");
let mut config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
config_profile: Some("test-profile".to_string()),
..Default::default()
})
.fallback_cwd(Some(home.path().to_path_buf()))
.build()
.await
.expect("load config");
let role_path = write_role_config(
&home,
"empty-role.toml",
"developer_instructions = \"Stay focused\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.active_profile.as_deref(), Some("test-profile"));
assert_eq!(config.model_provider_id, "test-provider");
assert_eq!(config.model_provider.name, "Test Provider");
}
#[tokio::test]
async fn apply_role_top_level_profile_settings_override_preserved_profile() {
let home = TempDir::new().expect("create temp dir");
tokio::fs::write(
home.path().join(CONFIG_TOML_FILE),
r#"
[profiles.base-profile]
model = "profile-model"
model_reasoning_effort = "low"
model_reasoning_summary = "concise"
model_verbosity = "low"
"#,
)
.await
.expect("write config.toml");
let mut config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
config_profile: Some("base-profile".to_string()),
..Default::default()
})
.fallback_cwd(Some(home.path().to_path_buf()))
.build()
.await
.expect("load config");
let role_path = write_role_config(
&home,
"top-level-profile-settings-role.toml",
r#"developer_instructions = "Stay focused"
model = "role-model"
model_reasoning_effort = "high"
model_reasoning_summary = "detailed"
model_verbosity = "high"
"#,
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.active_profile.as_deref(), Some("base-profile"));
assert_eq!(config.model.as_deref(), Some("role-model"));
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
assert_eq!(
config.model_reasoning_summary,
Some(ReasoningSummary::Detailed)
);
assert_eq!(config.model_verbosity, Some(Verbosity::High));
}
#[tokio::test]
async fn apply_role_uses_role_profile_instead_of_current_profile() {
let home = TempDir::new().expect("create temp dir");
tokio::fs::write(
home.path().join(CONFIG_TOML_FILE),
r#"
[model_providers.base-provider]
name = "Base Provider"
base_url = "https://base.example.com/v1"
env_key = "BASE_PROVIDER_API_KEY"
wire_api = "responses"
[model_providers.role-provider]
name = "Role Provider"
base_url = "https://role.example.com/v1"
env_key = "ROLE_PROVIDER_API_KEY"
wire_api = "responses"
[profiles.base-profile]
model_provider = "base-provider"
[profiles.role-profile]
model_provider = "role-provider"
"#,
)
.await
.expect("write config.toml");
let mut config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
config_profile: Some("base-profile".to_string()),
..Default::default()
})
.fallback_cwd(Some(home.path().to_path_buf()))
.build()
.await
.expect("load config");
let role_path = write_role_config(
&home,
"profile-role.toml",
"developer_instructions = \"Stay focused\"\nprofile = \"role-profile\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.active_profile.as_deref(), Some("role-profile"));
assert_eq!(config.model_provider_id, "role-provider");
assert_eq!(config.model_provider.name, "Role Provider");
}
#[tokio::test]
async fn apply_role_uses_role_model_provider_instead_of_current_profile_provider() {
let home = TempDir::new().expect("create temp dir");
tokio::fs::write(
home.path().join(CONFIG_TOML_FILE),
r#"
[model_providers.base-provider]
name = "Base Provider"
base_url = "https://base.example.com/v1"
env_key = "BASE_PROVIDER_API_KEY"
wire_api = "responses"
[model_providers.role-provider]
name = "Role Provider"
base_url = "https://role.example.com/v1"
env_key = "ROLE_PROVIDER_API_KEY"
wire_api = "responses"
[profiles.base-profile]
model_provider = "base-provider"
"#,
)
.await
.expect("write config.toml");
let mut config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
config_profile: Some("base-profile".to_string()),
..Default::default()
})
.fallback_cwd(Some(home.path().to_path_buf()))
.build()
.await
.expect("load config");
let role_path = write_role_config(
&home,
"provider-role.toml",
"developer_instructions = \"Stay focused\"\nmodel_provider = \"role-provider\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.active_profile, None);
assert_eq!(config.model_provider_id, "role-provider");
assert_eq!(config.model_provider.name, "Role Provider");
}
#[tokio::test]
async fn apply_role_uses_active_profile_model_provider_update() {
let home = TempDir::new().expect("create temp dir");
tokio::fs::write(
home.path().join(CONFIG_TOML_FILE),
r#"
[model_providers.base-provider]
name = "Base Provider"
base_url = "https://base.example.com/v1"
env_key = "BASE_PROVIDER_API_KEY"
wire_api = "responses"
[model_providers.role-provider]
name = "Role Provider"
base_url = "https://role.example.com/v1"
env_key = "ROLE_PROVIDER_API_KEY"
wire_api = "responses"
[profiles.base-profile]
model_provider = "base-provider"
model_reasoning_effort = "low"
"#,
)
.await
.expect("write config.toml");
let mut config = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
config_profile: Some("base-profile".to_string()),
..Default::default()
})
.fallback_cwd(Some(home.path().to_path_buf()))
.build()
.await
.expect("load config");
let role_path = write_role_config(
&home,
"profile-edit-role.toml",
r#"developer_instructions = "Stay focused"
[profiles.base-profile]
model_provider = "role-provider"
model_reasoning_effort = "high"
"#,
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.active_profile.as_deref(), Some("base-profile"));
assert_eq!(config.model_provider_id, "role-provider");
assert_eq!(config.model_provider.name, "Role Provider");
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
}
#[tokio::test]
#[cfg(not(windows))]
async fn apply_role_does_not_materialize_default_sandbox_workspace_write_fields() {
File diff suppressed because it is too large Load Diff
+61 -179
View File
@@ -34,7 +34,6 @@ 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::profile_toml::ConfigProfile;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::AuthCredentialsStoreMode;
@@ -2033,7 +2032,6 @@ fn resolve_permission_config_syntax(
config_layer_stack: &ConfigLayerStack,
cfg: &ConfigToml,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
) -> Option<PermissionConfigSyntax> {
if sandbox_mode_override.is_some() {
return Some(PermissionConfigSyntax::Legacy);
@@ -2058,10 +2056,6 @@ fn resolve_permission_config_syntax(
return Some(PermissionConfigSyntax::Profiles);
}
if profile_sandbox_mode.is_some() {
return Some(PermissionConfigSyntax::Legacy);
}
let mut selection = None;
for layer in config_layer_stack.get_layers(
ConfigLayerStackOrdering::LowestPrecedenceFirst,
@@ -2134,7 +2128,6 @@ pub struct ConfigOverrides {
pub default_permissions: Option<String>,
pub model_provider: Option<String>,
pub service_tier: Option<Option<String>>,
pub config_profile: Option<String>,
pub codex_self_exe: Option<PathBuf>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
@@ -2159,41 +2152,23 @@ fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
paths.retain(|path| seen.insert(path.clone()));
}
/// Resolves the OSS provider from CLI override, profile config, or global config.
/// 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,
config_profile: Option<String>,
) -> Option<String> {
if let Some(provider) = explicit_provider {
// Explicit provider specified (e.g., via --local-provider)
Some(provider.to_string())
} else {
// Check profile config first, then global config
let profile = config_toml.get_config_profile(config_profile).ok();
if let Some(profile) = &profile {
// Check if profile has an oss provider
if let Some(profile_oss_provider) = &profile.oss_provider {
Some(profile_oss_provider.clone())
}
// If not then check if the toml has an oss provider
else {
config_toml.oss_provider.clone()
}
} else {
config_toml.oss_provider.clone()
}
config_toml.oss_provider.clone()
}
}
/// Resolve the web search mode from explicit config and feature flags.
fn resolve_web_search_mode(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
features: &Features,
) -> Option<WebSearchMode> {
if let Some(mode) = config_profile.web_search.or(config_toml.web_search) {
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) {
@@ -2205,82 +2180,55 @@ fn resolve_web_search_mode(
None
}
fn resolve_web_search_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> Option<WebSearchConfig> {
let base = config_toml
fn resolve_web_search_config(config_toml: &ConfigToml) -> Option<WebSearchConfig> {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.web_search.as_ref());
let profile = config_profile
.tools
.as_ref()
.and_then(|tools| tools.web_search.as_ref());
match (base, profile) {
(None, None) => None,
(Some(base), None) => Some(base.clone().into()),
(None, Some(profile)) => Some(profile.clone().into()),
(Some(base), Some(profile)) => Some(base.merge(profile).into()),
}
.and_then(|tools| tools.web_search.as_ref())
.cloned()
.map(Into::into)
}
fn resolve_multi_agent_v2_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> MultiAgentV2Config {
fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
let profile = multi_agent_v2_toml_config(config_profile.features.as_ref());
let default = MultiAgentV2Config::default();
let max_concurrent_threads_per_session = profile
let max_concurrent_threads_per_session = base
.and_then(|config| config.max_concurrent_threads_per_session)
.or_else(|| base.and_then(|config| config.max_concurrent_threads_per_session))
.unwrap_or(default.max_concurrent_threads_per_session);
let min_wait_timeout_ms = profile
let min_wait_timeout_ms = base
.and_then(|config| config.min_wait_timeout_ms)
.or_else(|| base.and_then(|config| config.min_wait_timeout_ms))
.unwrap_or(default.min_wait_timeout_ms);
let max_wait_timeout_ms = profile
let max_wait_timeout_ms = base
.and_then(|config| config.max_wait_timeout_ms)
.or_else(|| base.and_then(|config| config.max_wait_timeout_ms))
.unwrap_or(default.max_wait_timeout_ms);
let default_wait_timeout_ms = profile
let default_wait_timeout_ms = base
.and_then(|config| config.default_wait_timeout_ms)
.or_else(|| base.and_then(|config| config.default_wait_timeout_ms))
.unwrap_or(default.default_wait_timeout_ms);
let usage_hint_enabled = profile
let usage_hint_enabled = base
.and_then(|config| config.usage_hint_enabled)
.or_else(|| base.and_then(|config| config.usage_hint_enabled))
.unwrap_or(default.usage_hint_enabled);
let usage_hint_text = profile
let usage_hint_text = base
.and_then(|config| config.usage_hint_text.as_ref())
.or_else(|| base.and_then(|config| config.usage_hint_text.as_ref()))
.cloned()
.or(default.usage_hint_text);
let root_agent_usage_hint_text = profile
let root_agent_usage_hint_text = base
.and_then(|config| config.root_agent_usage_hint_text.as_ref())
.or_else(|| base.and_then(|config| config.root_agent_usage_hint_text.as_ref()))
.cloned()
.or(default.root_agent_usage_hint_text);
let subagent_usage_hint_text = profile
let subagent_usage_hint_text = base
.and_then(|config| config.subagent_usage_hint_text.as_ref())
.or_else(|| base.and_then(|config| config.subagent_usage_hint_text.as_ref()))
.cloned()
.or(default.subagent_usage_hint_text);
let tool_namespace = profile
let tool_namespace = base
.and_then(|config| config.tool_namespace.as_ref())
.or_else(|| base.and_then(|config| config.tool_namespace.as_ref()))
.cloned()
.or(default.tool_namespace);
let hide_spawn_agent_metadata = profile
let hide_spawn_agent_metadata = base
.and_then(|config| config.hide_spawn_agent_metadata)
.or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata))
.unwrap_or(default.hide_spawn_agent_metadata);
let non_code_mode_only = profile
let non_code_mode_only = base
.and_then(|config| config.non_code_mode_only)
.or_else(|| base.and_then(|config| config.non_code_mode_only))
.unwrap_or(default.non_code_mode_only);
MultiAgentV2Config {
@@ -2531,7 +2479,6 @@ impl Config {
default_permissions: default_permissions_override,
model_provider,
service_tier: service_tier_override,
config_profile: config_profile_key,
codex_self_exe,
codex_linux_sandbox_exe,
main_execve_wrapper_exe,
@@ -2574,24 +2521,15 @@ impl Config {
"`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 active_profile_name = config_profile_key
.as_ref()
.or(cfg.profile.as_ref())
.cloned();
let config_profile = match active_profile_name.as_ref() {
Some(key) => cfg
.profiles
.get(key)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("config profile `{key}` not found"),
)
})?
.clone(),
None => ConfigProfile::default(),
};
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
let feature_overrides = FeatureOverrides {
web_search_request: override_tools_web_search_request,
@@ -2603,9 +2541,7 @@ impl Config {
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
},
FeatureConfigSource {
features: config_profile.features.as_ref(),
experimental_use_unified_exec_tool: config_profile
.experimental_use_unified_exec_tool,
..Default::default()
},
feature_overrides,
);
@@ -2615,9 +2551,8 @@ impl Config {
&mut startup_warnings,
)?;
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
let windows_sandbox_private_desktop =
resolve_windows_sandbox_private_desktop(&cfg, &config_profile);
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg);
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
use std::env;
@@ -2651,7 +2586,6 @@ impl Config {
&config_layer_stack,
&cfg,
sandbox_mode,
config_profile.sandbox_mode,
);
let requirements_toml = config_layer_stack.requirements_toml();
let effective_permission_selection = resolve_effective_permission_selection(
@@ -2908,7 +2842,7 @@ impl Config {
let mut permission_profile = cfg
.derive_permission_profile(
sandbox_mode,
config_profile.sandbox_mode,
/*profile_sandbox_mode*/ None,
windows_sandbox_level,
Some(&active_project),
Some(&constrained_permission_profile),
@@ -2965,20 +2899,11 @@ impl Config {
network_proxy,
);
}
if let Some(network_proxy) = network_proxy_toml_config(config_profile.features.as_ref())
{
apply_network_proxy_feature_config(
&mut configured_network_proxy_config,
network_proxy,
);
}
configured_network_proxy_config.network.enabled = true;
}
let approval_policy_was_explicit = approval_policy_override.is_some()
|| config_profile.approval_policy.is_some()
|| cfg.approval_policy.is_some();
let approval_policy_was_explicit =
approval_policy_override.is_some() || cfg.approval_policy.is_some();
let mut approval_policy = approval_policy_override
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
.unwrap_or_else(|| {
if active_project.is_trusted() {
@@ -2998,11 +2923,9 @@ impl Config {
);
approval_policy = constrained_approval_policy.value();
}
let approvals_reviewer_was_explicit = approvals_reviewer_override.is_some()
|| config_profile.approvals_reviewer.is_some()
|| cfg.approvals_reviewer.is_some();
let approvals_reviewer_was_explicit =
approvals_reviewer_override.is_some() || cfg.approvals_reviewer.is_some();
let mut approvals_reviewer = approvals_reviewer_override
.or(config_profile.approvals_reviewer)
.or(cfg.approvals_reviewer)
.unwrap_or(ApprovalsReviewer::User);
if !approvals_reviewer_was_explicit
@@ -3014,16 +2937,13 @@ impl Config {
);
approvals_reviewer = constrained_approvals_reviewer.value();
}
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
let web_search_mode =
resolve_web_search_mode(&cfg, &features).unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&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());
let profile = apps_mcp_path_override_toml_config(config_profile.features.as_ref());
profile
.and_then(|config| config.path.as_ref())
.or_else(|| base.and_then(|config| config.path.as_ref()))
base.and_then(|config| config.path.as_ref())
.cloned()
.or_else(|| Some("/ps/mcp".to_string()))
} else {
@@ -3045,7 +2965,6 @@ impl Config {
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidData, message))?;
let model_provider_id = model_provider
.or(config_profile.model_provider)
.or(cfg.model_provider)
.unwrap_or_else(|| "openai".to_string());
let model_provider = model_providers
@@ -3207,12 +3126,12 @@ impl Config {
let forced_login_method = cfg.forced_login_method;
let model = model.or(config_profile.model).or(cfg.model);
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 => config_profile.service_tier.or(cfg.service_tier),
None => cfg.service_tier,
};
let service_tier = service_tier.and_then(|service_tier| {
match ServiceTier::from_request_value(&service_tier) {
@@ -3236,10 +3155,7 @@ impl Config {
// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
let model_instructions_path = config_profile
.model_instructions_file
.as_ref()
.or(cfg.model_instructions_file.as_ref());
let model_instructions_path = cfg.model_instructions_file.as_ref();
let file_base_instructions = Self::try_read_non_empty_file(
fs,
model_instructions_path,
@@ -3250,27 +3166,16 @@ impl Config {
.or(file_base_instructions)
.or(cfg.instructions.clone());
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
let include_permissions_instructions = config_profile
.include_permissions_instructions
.or(cfg.include_permissions_instructions)
.unwrap_or(true);
let include_apps_instructions = config_profile
.include_apps_instructions
.or(cfg.include_apps_instructions)
.unwrap_or(true);
let include_collaboration_mode_instructions = config_profile
.include_collaboration_mode_instructions
.or(cfg.include_collaboration_mode_instructions)
.unwrap_or(true);
let include_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 = config_profile
.include_environment_context
.or(cfg.include_environment_context)
.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(|| {
@@ -3281,7 +3186,6 @@ impl Config {
))
});
let personality = personality
.or(config_profile.personality)
.or(cfg.personality)
.or_else(|| {
features
@@ -3289,10 +3193,7 @@ impl Config {
.then_some(Personality::Pragmatic)
});
let experimental_compact_prompt_path = config_profile
.experimental_compact_prompt_file
.as_ref()
.or(cfg.experimental_compact_prompt_file.as_ref());
let 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,
@@ -3300,19 +3201,12 @@ impl Config {
)
.await?;
let compact_prompt = compact_prompt.or(file_compact_prompt);
let zsh_path = zsh_path_override
.or(config_profile.zsh_path.map(Into::into))
.or(cfg.zsh_path.map(Into::into));
let zsh_path = zsh_path_override.or(cfg.zsh_path.map(Into::into));
let review_model = override_review_model.or(cfg.review_model);
let check_for_update_on_startup = cfg.check_for_update_on_startup.unwrap_or(true);
let model_catalog = load_model_catalog(
config_profile
.model_catalog_json
.clone()
.or(cfg.model_catalog_json.clone()),
)?;
let model_catalog = load_model_catalog(cfg.model_catalog_json.clone())?;
let log_dir = cfg
.log_dir
@@ -3558,21 +3452,14 @@ impl Config {
.or(show_raw_agent_reasoning)
.unwrap_or(false),
guardian_policy_config,
model_reasoning_effort: config_profile
.model_reasoning_effort
.or(cfg.model_reasoning_effort),
plan_mode_reasoning_effort: config_profile
.plan_mode_reasoning_effort
.or(cfg.plan_mode_reasoning_effort),
model_reasoning_summary: config_profile
.model_reasoning_summary
.or(cfg.model_reasoning_summary),
model_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: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile
model_verbosity: cfg.model_verbosity,
chatgpt_base_url: cfg
.chatgpt_base_url
.or(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(),
@@ -3612,16 +3499,12 @@ impl Config {
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
.unwrap_or(false),
active_profile: active_profile_name,
active_profile: None,
active_project,
notices,
check_for_update_on_startup,
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
analytics_enabled: config_profile
.analytics
.as_ref()
.and_then(|a| a.enabled)
.or(cfg.analytics.as_ref().and_then(|a| a.enabled)),
analytics_enabled: cfg.analytics.as_ref().and_then(|a| a.enabled),
feedback_enabled: cfg
.feedback
.as_ref()
@@ -3669,11 +3552,10 @@ impl Config {
.as_ref()
.map(|t| t.pet_anchor)
.unwrap_or_default(),
tui_session_picker_view: config_profile
tui_session_picker_view: cfg
.tui
.as_ref()
.and_then(|t| t.session_picker_view)
.or_else(|| cfg.tui.as_ref().and_then(|t| t.session_picker_view))
.unwrap_or_default(),
terminal_resize_reflow,
tui_keymap: cfg
+4 -32
View File
@@ -1,7 +1,6 @@
use crate::config::Config;
use crate::config::edit::ConfigEditsBuilder;
use codex_config::config_toml::ConfigToml;
use codex_config::profile_toml::ConfigProfile;
use codex_config::types::WindowsSandboxModeToml;
use codex_features::Feature;
use codex_features::Features;
@@ -56,47 +55,20 @@ pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandbo
WindowsSandboxLevel::from_features(features)
}
pub fn resolve_windows_sandbox_mode(
cfg: &ConfigToml,
profile: &ConfigProfile,
) -> Option<WindowsSandboxModeToml> {
if let Some(mode) = legacy_windows_sandbox_mode(profile.features.as_ref()) {
return Some(mode);
}
if legacy_windows_sandbox_keys_present(profile.features.as_ref()) {
return None;
}
profile
.windows
pub fn resolve_windows_sandbox_mode(cfg: &ConfigToml) -> Option<WindowsSandboxModeToml> {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox)
.or_else(|| cfg.windows.as_ref().and_then(|windows| windows.sandbox))
.or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref()))
}
pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &ConfigProfile) -> bool {
profile
.windows
pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml) -> bool {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
.or_else(|| {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
})
.unwrap_or(true)
}
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {
let Some(entries) = features.map(FeaturesToml::entries) else {
return false;
};
entries.contains_key(Feature::WindowsSandboxElevated.key())
|| entries.contains_key(Feature::WindowsSandbox.key())
|| entries.contains_key("enable_experimental_windows_sandbox")
}
pub fn legacy_windows_sandbox_mode(
features: Option<&FeaturesToml>,
) -> Option<WindowsSandboxModeToml> {
+3 -75
View File
@@ -78,29 +78,6 @@ fn legacy_mode_supports_alias_key() {
);
}
#[test]
fn resolve_windows_sandbox_mode_prefers_profile_windows() {
let cfg = ConfigToml {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Unelevated),
..Default::default()
}),
..Default::default()
};
let profile = ConfigProfile {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Elevated),
..Default::default()
}),
..Default::default()
};
assert_eq!(
resolve_windows_sandbox_mode(&cfg, &profile),
Some(WindowsSandboxModeToml::Elevated)
);
}
#[test]
fn resolve_windows_sandbox_mode_falls_back_to_legacy_keys() {
let mut entries = BTreeMap::new();
@@ -114,61 +91,15 @@ fn resolve_windows_sandbox_mode_falls_back_to_legacy_keys() {
};
assert_eq!(
resolve_windows_sandbox_mode(&cfg, &ConfigProfile::default()),
resolve_windows_sandbox_mode(&cfg),
Some(WindowsSandboxModeToml::Unelevated)
);
}
#[test]
fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_true() {
let mut profile_entries = BTreeMap::new();
profile_entries.insert(
"experimental_windows_sandbox".to_string(),
/*value*/ false,
);
let profile = ConfigProfile {
features: Some(FeaturesToml::from(profile_entries)),
..Default::default()
};
let mut cfg_entries = BTreeMap::new();
cfg_entries.insert(
"experimental_windows_sandbox".to_string(),
/*value*/ true,
);
let cfg = ConfigToml {
features: Some(FeaturesToml::from(cfg_entries)),
..Default::default()
};
assert_eq!(resolve_windows_sandbox_mode(&cfg, &profile), None);
}
#[test]
fn resolve_windows_sandbox_private_desktop_prefers_profile_windows() {
let cfg = ConfigToml {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Unelevated),
sandbox_private_desktop: Some(false),
}),
..Default::default()
};
let profile = ConfigProfile {
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Elevated),
sandbox_private_desktop: Some(true),
}),
..Default::default()
};
assert!(resolve_windows_sandbox_private_desktop(&cfg, &profile));
}
#[test]
fn resolve_windows_sandbox_private_desktop_defaults_to_true() {
assert!(resolve_windows_sandbox_private_desktop(
&ConfigToml::default(),
&ConfigProfile::default()
&ConfigToml::default()
));
}
@@ -182,8 +113,5 @@ fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() {
..Default::default()
};
assert!(!resolve_windows_sandbox_private_desktop(
&cfg,
&ConfigProfile::default()
));
assert!(!resolve_windows_sandbox_private_desktop(&cfg));
}
+1 -6
View File
@@ -375,11 +375,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
let run_cloud_requirements = cloud_requirements.clone();
let model_provider = if oss {
let resolved = resolve_oss_provider(
oss_provider.as_deref(),
&config_toml,
/*config_profile*/ None,
);
let resolved = resolve_oss_provider(oss_provider.as_deref(), &config_toml);
if let Some(provider) = resolved {
Some(provider)
@@ -418,7 +414,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
workspace_roots: None,
model_provider: model_provider.clone(),
service_tier: None,
config_profile: None,
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
@@ -29,10 +29,6 @@ pub struct CodexToolCallParam {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Configuration profile from config.toml to specify default options.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
/// Working directory for the session. If relative, it is resolved against
/// the server process's current working directory.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -160,7 +156,6 @@ impl CodexToolCallParam {
let Self {
prompt,
model,
profile,
cwd,
approval_policy,
sandbox,
@@ -173,7 +168,6 @@ impl CodexToolCallParam {
// Build the `ConfigOverrides` recognized by codex-core.
let overrides = ConfigOverrides {
model,
config_profile: profile,
cwd: cwd.map(PathBuf::from),
approval_policy: approval_policy.map(Into::into),
sandbox_mode: sandbox.map(Into::into),
@@ -345,10 +339,6 @@ mod tests {
"description": "Optional override for the model name (e.g. 'gpt-5.2', 'gpt-5.2-codex').",
"type": "string"
},
"profile": {
"description": "Configuration profile from config.toml to specify default options.",
"type": "string"
},
"prompt": {
"description": "The *initial user prompt* to start the Codex conversation.",
"type": "string"
+1 -5
View File
@@ -994,11 +994,7 @@ pub async fn run_main(
.await;
let model_provider_override = if cli.oss {
let resolved = resolve_oss_provider(
cli.oss_provider.as_deref(),
&config_toml,
/*config_profile*/ None,
);
let resolved = resolve_oss_provider(cli.oss_provider.as_deref(), &config_toml);
if let Some(provider) = resolved {
Some(provider)