diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 2392cc80784..0b639b7d5c0 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -83,7 +83,6 @@ impl WindowsSandboxRequestProcessor { command_cwd, env_map: std::env::vars().collect(), codex_home: config.codex_home.to_path_buf(), - active_profile: config.active_profile.clone(), }; codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 016a835e2f5..89310277ab6 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4798,7 +4798,6 @@ model = "gpt-project-local" .build() .await?; - assert_eq!(config.active_profile, None); assert_eq!(config.model, None); assert!( config.startup_warnings.iter().any(|warning| { @@ -5032,7 +5031,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5063,7 +5061,6 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> { let empty = BTreeMap::new(); apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(empty.clone())], )?; let loaded = load_global_mcp_servers(codex_home.path()).await?; @@ -5379,7 +5376,6 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5456,7 +5452,6 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5518,7 +5513,6 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()> apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5570,7 +5564,6 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5625,7 +5618,6 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5695,7 +5687,6 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh )]); apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5779,7 +5770,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; let serialized_with_optional = std::fs::read_to_string(&config_path)?; @@ -5814,7 +5804,6 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh ); apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -5913,7 +5902,6 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers() apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -6001,7 +5989,6 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -6052,7 +6039,6 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -6103,7 +6089,6 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> { apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -6160,7 +6145,6 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh apply_blocking( codex_home.path(), - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], )?; @@ -6283,196 +6267,6 @@ model = "gpt-4.1" Ok(()) } -#[tokio::test] -async fn set_model_updates_profile() -> anyhow::Result<()> { - let codex_home = TempDir::new()?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_model(Some("gpt-5.4"), Some(ReasoningEffort::Medium)) - .apply() - .await?; - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - let parsed: ConfigToml = toml::from_str(&serialized)?; - let profile = parsed - .profiles - .get("dev") - .expect("profile should be created"); - - assert_eq!(profile.model.as_deref(), Some("gpt-5.4")); - assert_eq!( - profile.model_reasoning_effort, - Some(ReasoningEffort::Medium) - ); - - Ok(()) -} - -#[tokio::test] -async fn set_model_updates_existing_profile() -> anyhow::Result<()> { - let codex_home = TempDir::new()?; - let config_path = codex_home.path().join(CONFIG_TOML_FILE); - - tokio::fs::write( - &config_path, - r#" -[profiles.dev] -model = "gpt-4" -model_reasoning_effort = "medium" - -[profiles.prod] -model = "gpt-5.4" -"#, - ) - .await?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_model(Some("o4-high"), Some(ReasoningEffort::Medium)) - .apply() - .await?; - - let serialized = tokio::fs::read_to_string(config_path).await?; - let parsed: ConfigToml = toml::from_str(&serialized)?; - - let dev_profile = parsed - .profiles - .get("dev") - .expect("dev profile should survive updates"); - assert_eq!(dev_profile.model.as_deref(), Some("o4-high")); - assert_eq!( - dev_profile.model_reasoning_effort, - Some(ReasoningEffort::Medium) - ); - - assert_eq!( - parsed - .profiles - .get("prod") - .and_then(|profile| profile.model.as_deref()), - Some("gpt-5.4"), - ); - - Ok(()) -} - -#[tokio::test] -async fn set_feature_enabled_updates_profile() -> anyhow::Result<()> { - let codex_home = TempDir::new()?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_feature_enabled("guardian_approval", /*enabled*/ true) - .apply() - .await?; - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - let parsed: ConfigToml = toml::from_str(&serialized)?; - let profile = parsed - .profiles - .get("dev") - .expect("profile should be created"); - - assert_eq!( - profile - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - Some(true), - ); - assert_eq!( - parsed - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - None, - ); - - Ok(()) -} - -#[tokio::test] -async fn set_feature_enabled_persists_feature_disable_in_profile() -> anyhow::Result<()> { - let codex_home = TempDir::new()?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_feature_enabled("guardian_approval", /*enabled*/ true) - .apply() - .await?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_feature_enabled("guardian_approval", /*enabled*/ false) - .apply() - .await?; - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - let parsed: ConfigToml = toml::from_str(&serialized)?; - let profile = parsed - .profiles - .get("dev") - .expect("profile should be created"); - - assert_eq!( - profile - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - Some(false), - ); - assert_eq!( - parsed - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - None, - ); - - Ok(()) -} - -#[tokio::test] -async fn set_feature_enabled_profile_disable_overrides_root_enable() -> anyhow::Result<()> { - let codex_home = TempDir::new()?; - - ConfigEditsBuilder::new(codex_home.path()) - .set_feature_enabled("guardian_approval", /*enabled*/ true) - .apply() - .await?; - - ConfigEditsBuilder::new(codex_home.path()) - .with_profile(Some("dev")) - .set_feature_enabled("guardian_approval", /*enabled*/ false) - .apply() - .await?; - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - let parsed: ConfigToml = toml::from_str(&serialized)?; - let profile = parsed - .profiles - .get("dev") - .expect("profile should be created"); - - assert_eq!( - parsed - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - Some(true), - ); - assert_eq!( - profile - .features - .as_ref() - .and_then(|features| features.entries().get("guardian_approval").copied()), - Some(false), - ); - - Ok(()) -} - struct PrecedenceTestFixture { cwd: TempDir, codex_home: TempDir, diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 1b5cd879c45..ca54a7c3f57 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -513,13 +513,6 @@ mod document_helpers { struct ConfigDocument { doc: DocumentMut, - profile: Option, -} - -#[derive(Copy, Clone)] -enum Scope { - Global, - Profile, } #[derive(Copy, Clone)] @@ -529,25 +522,25 @@ enum TraversalMode { } impl ConfigDocument { - fn new(doc: DocumentMut, profile: Option) -> Self { - Self { doc, profile } + fn new(doc: DocumentMut) -> Self { + Self { doc } } fn apply(&mut self, edit: &ConfigEdit) -> anyhow::Result { match edit { ConfigEdit::SetModel { model, effort } => Ok({ let mut mutated = false; - mutated |= self.write_profile_value( + mutated |= self.write_optional_value( &["model"], model.as_ref().map(|model_value| value(model_value.clone())), ); - mutated |= self.write_profile_value( + mutated |= self.write_optional_value( &["model_reasoning_effort"], effort.map(|effort| value(effort.to_string())), ); mutated }), - ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value( + ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_optional_value( &["service_tier"], service_tier.as_ref().map(|service_tier| { // Keep the legacy config spelling stable. Runtime values use @@ -560,35 +553,30 @@ impl ConfigDocument { value(config_value) }), )), - ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value( + ConfigEdit::SetModelPersonality { personality } => Ok(self.write_optional_value( &["personality"], personality.map(|personality| value(personality.to_string())), )), ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value( - Scope::Global, &[NOTICE_TABLE_KEY, "hide_full_access_warning"], value(*acknowledged), )), ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value( - Scope::Global, &[NOTICE_TABLE_KEY, "hide_world_writable_warning"], value(*acknowledged), )), ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value( - Scope::Global, &[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"], value(*acknowledged), )), ConfigEdit::SetNoticeHideModelMigrationPrompt(migration_config, acknowledged) => { Ok(self.write_value( - Scope::Global, &[NOTICE_TABLE_KEY, migration_config.as_str()], value(*acknowledged), )) } ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(acknowledged) => Ok(self .write_value( - Scope::Global, &[ NOTICE_TABLE_KEY, "external_config_migration_prompts", @@ -598,7 +586,6 @@ impl ConfigDocument { )), ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(timestamp) => { Ok(self.write_value( - Scope::Global, &[ NOTICE_TABLE_KEY, "external_config_migration_prompts", @@ -611,7 +598,6 @@ impl ConfigDocument { project, acknowledged, ) => Ok(self.write_value( - Scope::Global, &[ NOTICE_TABLE_KEY, "external_config_migration_prompts", @@ -624,7 +610,6 @@ impl ConfigDocument { project, timestamp, ) => Ok(self.write_value( - Scope::Global, &[ NOTICE_TABLE_KEY, "external_config_migration_prompts", @@ -634,7 +619,6 @@ impl ConfigDocument { value(*timestamp), )), ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value( - Scope::Global, &[NOTICE_TABLE_KEY, "model_migrations", from.as_str()], value(to.clone()), )), @@ -663,20 +647,26 @@ impl ConfigDocument { } } - fn write_profile_value(&mut self, segments: &[&str], value: Option) -> bool { + fn write_optional_value(&mut self, segments: &[&str], value: Option) -> bool { match value { - Some(item) => self.write_value(Scope::Profile, segments, item), - None => self.clear(Scope::Profile, segments), + Some(item) => self.write_value(segments, item), + None => self.clear(segments), } } - fn write_value(&mut self, scope: Scope, segments: &[&str], value: TomlItem) -> bool { - let resolved = self.scoped_segments(scope, segments); + fn write_value(&mut self, segments: &[&str], value: TomlItem) -> bool { + let resolved = segments + .iter() + .map(|segment| (*segment).to_string()) + .collect::>(); self.insert(&resolved, value) } - fn clear(&mut self, scope: Scope, segments: &[&str]) -> bool { - let resolved = self.scoped_segments(scope, segments); + fn clear(&mut self, segments: &[&str]) -> bool { + let resolved = segments + .iter() + .map(|segment| (*segment).to_string()) + .collect::>(); self.remove(&resolved) } @@ -709,7 +699,6 @@ impl ConfigDocument { .filter(|disabled_tool| seen.insert(disabled_tool.clone())) .collect::>(); self.write_value( - Scope::Global, &["tool_suggest", "disabled_tools"], document_helpers::tool_suggest_disabled_tools_value(&disabled_tools), ) @@ -721,7 +710,7 @@ impl ConfigDocument { fn replace_mcp_servers(&mut self, servers: &BTreeMap) -> bool { if servers.is_empty() { - return self.clear(Scope::Global, &["mcp_servers"]); + return self.clear(&["mcp_servers"]); } let root = self.doc.as_table_mut(); @@ -883,26 +872,6 @@ impl ConfigDocument { mutated } - fn scoped_segments(&self, scope: Scope, segments: &[&str]) -> Vec { - let resolved: Vec = segments - .iter() - .map(|segment| (*segment).to_string()) - .collect(); - - if matches!(scope, Scope::Profile) - && resolved.first().is_none_or(|segment| segment != "profiles") - && let Some(profile) = self.profile.as_deref() - { - let mut scoped = Vec::with_capacity(resolved.len() + 2); - scoped.push("profiles".to_string()); - scoped.push(profile.to_string()); - scoped.extend(resolved); - return scoped; - } - - resolved - } - fn insert(&mut self, segments: &[String], value: TomlItem) -> bool { let Some((last, parents)) = segments.split_last() else { return false; @@ -1030,18 +999,13 @@ fn write_skill_config_selector(table: &mut TomlTable, selector: &SkillConfigSele } /// Persist edits using a blocking strategy. -pub fn apply_blocking( - codex_home: &Path, - profile: Option<&str>, - edits: &[ConfigEdit], -) -> anyhow::Result<()> { +pub fn apply_blocking(codex_home: &Path, edits: &[ConfigEdit]) -> anyhow::Result<()> { let config_path = codex_home.join(CONFIG_TOML_FILE); - apply_blocking_to_resolved_file(&config_path, profile, edits) + apply_blocking_to_resolved_file(&config_path, edits) } fn apply_blocking_to_resolved_file( resolved_config_file: &Path, - legacy_profile: Option<&str>, edits: &[ConfigEdit], ) -> anyhow::Result<()> { if edits.is_empty() { @@ -1064,13 +1028,7 @@ fn apply_blocking_to_resolved_file( serialized.parse::()? }; - let profile = legacy_profile.map(ToOwned::to_owned).or_else(|| { - doc.get("profile") - .and_then(|item| item.as_str()) - .map(ToOwned::to_owned) - }); - - let mut document = ConfigDocument::new(doc, profile); + let mut document = ConfigDocument::new(doc); let mut mutated = false; for edit in edits { @@ -1093,29 +1051,18 @@ fn apply_blocking_to_resolved_file( /// Persist edits asynchronously by offloading the blocking writer. /// -/// `profile` selects a legacy `[profiles.]` section inside -/// `$CODEX_HOME/config.toml`; profile-v2 callers should resolve their target -/// file before constructing a [ConfigEditsBuilder]. -pub async fn apply( - codex_home: &Path, - profile: Option<&str>, - edits: Vec, -) -> anyhow::Result<()> { +pub async fn apply(codex_home: &Path, edits: Vec) -> anyhow::Result<()> { let codex_home = codex_home.to_path_buf(); let config_path = codex_home.join(CONFIG_TOML_FILE); - let profile = profile.map(ToOwned::to_owned); - task::spawn_blocking(move || { - apply_blocking_to_resolved_file(&config_path, profile.as_deref(), &edits) - }) - .await - .context("config persistence task panicked")? + task::spawn_blocking(move || apply_blocking_to_resolved_file(&config_path, &edits)) + .await + .context("config persistence task panicked")? } /// Fluent builder to batch config edits and apply them atomically. #[derive(Default)] pub struct ConfigEditsBuilder { config_path: PathBuf, - profile: Option, edits: Vec, } @@ -1136,16 +1083,10 @@ impl ConfigEditsBuilder { pub fn for_config_path(config_path: &Path) -> Self { Self { config_path: config_path.to_path_buf(), - profile: None, edits: Vec::new(), } } - pub fn with_profile(mut self, profile: Option<&str>) -> Self { - self.profile = profile.map(ToOwned::to_owned); - self - } - pub fn set_model(mut self, model: Option<&str>, effort: Option) -> Self { self.edits.push(ConfigEdit::SetModel { model: model.map(ToOwned::to_owned), @@ -1248,27 +1189,16 @@ impl ConfigEditsBuilder { /// Enable or disable a feature flag by key under the `[features]` table. /// - /// Disabling a default-false feature clears the root-scoped key instead of + /// Disabling a default-false feature clears the key instead of /// persisting `false`, so the config does not pin the feature once it - /// graduates to globally enabled. Profile-scoped disables still persist - /// `false` so they can override an inherited root enable. + /// graduates to globally enabled. pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self { - let profile_scoped = self.profile.is_some(); - let segments = if let Some(profile) = self.profile.as_ref() { - vec![ - "profiles".to_string(), - profile.clone(), - "features".to_string(), - key.to_string(), - ] - } else { - vec!["features".to_string(), key.to_string()] - }; + let segments = vec!["features".to_string(), key.to_string()]; let is_default_false_feature = FEATURES .iter() .find(|spec| spec.key == key) .is_some_and(|spec| !spec.default_enabled); - if enabled || profile_scoped || !is_default_false_feature { + if enabled || !is_default_false_feature { self.edits.push(ConfigEdit::SetPath { segments, value: value(enabled), @@ -1280,18 +1210,8 @@ impl ConfigEditsBuilder { } pub fn set_windows_sandbox_mode(mut self, mode: &str) -> Self { - let segments = if let Some(profile) = self.profile.as_ref() { - vec![ - "profiles".to_string(), - profile.clone(), - "windows".to_string(), - "sandbox".to_string(), - ] - } else { - vec!["windows".to_string(), "sandbox".to_string()] - }; self.edits.push(ConfigEdit::SetPath { - segments, + segments: vec!["windows".to_string(), "sandbox".to_string()], value: value(mode), }); self @@ -1339,34 +1259,15 @@ impl ConfigEditsBuilder { "elevated_windows_sandbox", "enable_experimental_windows_sandbox", ] { - let mut segments = vec!["features".to_string(), key.to_string()]; - if let Some(profile) = self.profile.as_ref() { - segments = vec![ - "profiles".to_string(), - profile.clone(), - "features".to_string(), - key.to_string(), - ]; - } + let segments = vec!["features".to_string(), key.to_string()]; self.edits.push(ConfigEdit::ClearPath { segments }); } self } pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self { - let segments = if let Some(profile) = self.profile.as_ref() { - vec![ - "profiles".to_string(), - profile.clone(), - "tui".to_string(), - "session_picker_view".to_string(), - ] - } else { - vec!["tui".to_string(), "session_picker_view".to_string()] - }; - self.edits.push(ConfigEdit::SetPath { - segments, + segments: vec!["tui".to_string(), "session_picker_view".to_string()], value: value(mode.to_string()), }); self @@ -1382,13 +1283,13 @@ impl ConfigEditsBuilder { /// Apply edits on a blocking thread. pub fn apply_blocking(self) -> anyhow::Result<()> { - apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits) + apply_blocking_to_resolved_file(&self.config_path, &self.edits) } /// Apply edits asynchronously via a blocking offload. pub async fn apply(self) -> anyhow::Result<()> { task::spawn_blocking(move || { - apply_blocking_to_resolved_file(&self.config_path, self.profile.as_deref(), &self.edits) + apply_blocking_to_resolved_file(&self.config_path, &self.edits) }) .await .context("config persistence task panicked")? diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index 740d819aed2..dce192831b6 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -20,7 +20,6 @@ fn blocking_set_model_top_level() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: Some(ReasoningEffort::High), @@ -111,24 +110,6 @@ session_picker_view = "dense" assert_eq!(contents, expected); } -#[test] -fn session_picker_view_builder_respects_active_profile() { - let tmp = tempdir().expect("tmpdir"); - let codex_home = tmp.path(); - - ConfigEditsBuilder::new(codex_home) - .with_profile(Some("work")) - .set_session_picker_view(SessionPickerViewMode::Dense) - .apply_blocking() - .expect("persist"); - - let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); - let expected = r#"[profiles.work.tui] -session_picker_view = "dense" -"#; - assert_eq!(contents, expected); -} - #[test] fn keymap_binding_edit_writes_root_action_binding() { let tmp = tempdir().expect("tmpdir"); @@ -380,7 +361,7 @@ enabled = false } #[test] -fn blocking_set_model_preserves_inline_table_contents() { +fn blocking_set_model_ignores_inline_legacy_profile_contents() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); @@ -396,7 +377,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: Some("o4-mini".to_string()), effort: None, @@ -407,7 +387,12 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let value: TomlValue = toml::from_str(&raw).expect("parse config"); - // Ensure sandbox_mode is preserved under profiles.fast and model updated. + assert_eq!( + value.get("model").and_then(TomlValue::as_str), + Some("o4-mini") + ); + + // Legacy profile values stay untouched when root settings are updated. let profiles_tbl = value .get("profiles") .and_then(|v| v.as_table()) @@ -422,7 +407,7 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } ); assert_eq!( fast_tbl.get("model").and_then(|v| v.as_str()), - Some("o4-mini") + Some("gpt-4o") ); } @@ -441,7 +426,6 @@ fn blocking_set_model_writes_through_symlink_chain() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: Some(ReasoningEffort::High), @@ -474,7 +458,6 @@ fn blocking_set_model_replaces_symlink_on_cycle() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: None, @@ -513,7 +496,6 @@ network_access = false apply_blocking( codex_home, - /*profile*/ None, &[ ConfigEdit::SetPath { segments: vec![ @@ -553,7 +535,7 @@ network_access = true } #[test] -fn blocking_clear_model_removes_inline_table_entry() { +fn blocking_clear_model_does_not_follow_legacy_active_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); @@ -568,7 +550,6 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: None, effort: Some(ReasoningEffort::High), @@ -579,15 +560,14 @@ profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"profile = "fast" -[profiles.fast] -sandbox_mode = "strict" +profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } } model_reasoning_effort = "high" "#; assert_eq!(contents, expected); } #[test] -fn blocking_set_model_scopes_to_active_profile() { +fn blocking_set_model_does_not_follow_legacy_active_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( @@ -602,7 +582,6 @@ model_reasoning_effort = "low" apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetModel { model: Some("o5-preview".to_string()), effort: Some(ReasoningEffort::Minimal), @@ -612,39 +591,11 @@ model_reasoning_effort = "low" let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"profile = "team" - -[profiles.team] -model_reasoning_effort = "minimal" model = "o5-preview" -"#; - assert_eq!(contents, expected); -} - -#[test] -fn blocking_set_model_with_explicit_profile() { - let tmp = tempdir().expect("tmpdir"); - let codex_home = tmp.path(); - std::fs::write( - codex_home.join(CONFIG_TOML_FILE), - r#"[profiles."team a"] -model = "gpt-5.4" -"#, - ) - .expect("seed"); - - apply_blocking( - codex_home, - Some("team a"), - &[ConfigEdit::SetModel { - model: Some("o4-mini".to_string()), - effort: None, - }], - ) - .expect("persist"); +model_reasoning_effort = "minimal" - let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); - let expected = r#"[profiles."team a"] -model = "o4-mini" +[profiles.team] +model_reasoning_effort = "low" "#; assert_eq!(contents, expected); } @@ -666,7 +617,6 @@ existing = "value" apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeHideFullAccessWarning(true)], ) .expect("persist"); @@ -696,7 +646,6 @@ existing = "value" apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)], ) .expect("persist"); @@ -722,7 +671,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeHideModelMigrationPrompt( "hide_gpt5_1_migration_prompt".to_string(), true, @@ -751,7 +699,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeHideModelMigrationPrompt( "hide_gpt-5.1-codex-max_migration_prompt".to_string(), true, @@ -780,7 +727,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::RecordModelMigrationSeen { from: "gpt-5.2".to_string(), to: "gpt-5.4".to_string(), @@ -811,7 +757,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( true, )], @@ -841,7 +786,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( "/Users/alexsong/code/skills".to_string(), @@ -874,7 +818,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)], ) .expect("persist"); @@ -902,7 +845,6 @@ existing = "value" .expect("seed"); apply_blocking( codex_home, - /*profile*/ None, &[ ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( "/Users/alexsong/code/skills".to_string(), @@ -996,7 +938,6 @@ fn blocking_replace_mcp_servers_round_trips() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(servers.clone())], ) .expect("persist"); @@ -1069,12 +1010,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() { }, ); - apply_blocking( - codex_home, - /*profile*/ None, - &[ConfigEdit::ReplaceMcpServers(servers)], - ) - .expect("persist"); + apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = "\ @@ -1129,12 +1065,7 @@ foo = { command = "cmd" } }, ); - apply_blocking( - codex_home, - /*profile*/ None, - &[ConfigEdit::ReplaceMcpServers(servers)], - ) - .expect("persist"); + apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers] @@ -1184,12 +1115,7 @@ foo = { command = "cmd" } # keep me }, ); - apply_blocking( - codex_home, - /*profile*/ None, - &[ConfigEdit::ReplaceMcpServers(servers)], - ) - .expect("persist"); + apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers] @@ -1238,12 +1164,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me }, ); - apply_blocking( - codex_home, - /*profile*/ None, - &[ConfigEdit::ReplaceMcpServers(servers)], - ) - .expect("persist"); + apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers] @@ -1293,12 +1214,7 @@ foo = { command = "cmd" } }, ); - apply_blocking( - codex_home, - /*profile*/ None, - &[ConfigEdit::ReplaceMcpServers(servers)], - ) - .expect("persist"); + apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers] @@ -1315,7 +1231,6 @@ fn blocking_clear_path_noop_when_missing() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::ClearPath { segments: vec!["missing".to_string()], }], @@ -1336,7 +1251,6 @@ fn blocking_set_path_updates_notifications() { let item = value(false); apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::SetPath { segments: vec!["tui".to_string(), "notifications".to_string()], value: item, @@ -1518,7 +1432,6 @@ fn replace_mcp_servers_blocking_clears_table_when_empty() { apply_blocking( codex_home, - /*profile*/ None, &[ConfigEdit::ReplaceMcpServers(BTreeMap::new())], ) .expect("persist"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 3b65ceddcdb..a70492ef05f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -958,9 +958,6 @@ pub struct Config { /// When `true`, suppress warnings about unstable (under development) features. pub suppress_unstable_features_warning: bool, - /// The active profile name used to derive this `Config` (if any). - pub active_profile: Option, - /// The currently active project config, resolved by checking if cwd: /// is (1) part of a git repo, (2) a git worktree, or (3) just using the cwd pub active_project: ProjectConfig, @@ -3499,7 +3496,6 @@ impl Config { suppress_unstable_features_warning: cfg .suppress_unstable_features_warning .unwrap_or(false), - active_profile: None, active_project, notices, check_for_update_on_startup, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 8ed010172e7..a16b0855ece 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -816,7 +816,7 @@ impl Session { .permissions .legacy_sandbox_policy(session_configuration.cwd.as_path()), mcp_servers.keys().map(String::as_str).collect(), - config.active_profile.clone(), + /*active_profile*/ None, ); let use_zsh_fork_shell = config.features.enabled(Feature::ShellZshFork); diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 4166868ff7a..1494ce262ee 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -252,7 +252,6 @@ pub struct WindowsSandboxSetupRequest { pub command_cwd: PathBuf, pub env_map: HashMap, pub codex_home: PathBuf, - pub active_profile: Option, } pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> anyhow::Result<()> { @@ -291,7 +290,6 @@ async fn run_windows_sandbox_setup_and_persist( let command_cwd = request.command_cwd; let env_map = request.env_map; let codex_home = request.codex_home; - let active_profile = request.active_profile; let setup_codex_home = codex_home.clone(); let setup_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { @@ -325,7 +323,6 @@ async fn run_windows_sandbox_setup_and_persist( setup_result?; ConfigEditsBuilder::new(codex_home.as_path()) - .with_profile(active_profile.as_deref()) .set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode)) .clear_legacy_windows_sandbox_keys() .apply() diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 25b2b0e1439..52590b56cca 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -134,7 +134,6 @@ pub use exec_events::TurnStartedEvent; pub use exec_events::Usage; pub use exec_events::WebSearchItem; use serde_json::Value; -use std::collections::HashMap; use std::io::IsTerminal; use std::io::Read; use std::path::Path; @@ -967,7 +966,7 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - config: config_request_overrides_from_config(config), + config: None, ephemeral: Some(config.ephemeral), thread_source: Some(ThreadSource::User), ..ThreadStartParams::default() @@ -998,7 +997,7 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - config: config_request_overrides_from_config(config), + config: None, ..ThreadResumeParams::default() } } @@ -1039,13 +1038,6 @@ fn sandbox_mode_from_permission_profile( } } -fn config_request_overrides_from_config(config: &Config) -> Option> { - config - .active_profile - .as_ref() - .map(|profile| HashMap::from([("profile".to_string(), Value::String(profile.clone()))])) -} - fn approvals_reviewer_override_from_config( config: &Config, ) -> Option { diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 9fdd6db90fb..634327d007d 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -271,7 +271,6 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R multi_agent_v2: MultiAgentV2Config::default(), features: Default::default(), suppress_unstable_features_warning: false, - active_profile: None, active_project: ProjectConfig { trust_level: None }, notices: Notice::default(), check_for_update_on_startup: false, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c0097496782..3c3d15086f8 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -486,7 +486,6 @@ pub(crate) struct App { /// Config is stored here so we can recreate ChatWidgets as needed. pub(crate) config: Config, pub(crate) state_db: Option, - pub(crate) active_profile: Option, cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, @@ -693,7 +692,6 @@ impl App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, - active_profile: Option, initial_prompt: Option, initial_images: Vec, session_selection: SessionSelection, @@ -971,7 +969,6 @@ See the Codex keymap documentation for supported actions and examples." workspace_command_runner: Some(workspace_command_runner), config, state_db, - active_profile, cli_kv_overrides, harness_overrides, loader_overrides, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 1cba3c85a0c..e83662c2a25 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -344,7 +344,6 @@ impl App { let auto_review_preset = auto_review_mode(); let mut next_config = self.config.clone(); - let active_profile = self.active_profile.clone(); let windows_sandbox_changed = updates.iter().any(|(feature, _)| { matches!( feature, @@ -356,42 +355,12 @@ impl App { let mut permission_profile_override = None; let mut active_permission_profile_override = None; let mut feature_updates_to_apply = Vec::with_capacity(updates.len()); - // Auto-Review owns `approvals_reviewer`, but disabling the feature - // from inside a profile should not silently clear a value configured at - // the root scope. - let (root_approvals_reviewer_blocks_profile_disable, profile_approvals_reviewer_configured) = { - let effective_config = next_config.config_layer_stack.effective_config(); - let root_blocks_disable = effective_config - .as_table() - .and_then(|table| table.get("approvals_reviewer")) - .is_some_and(|value| value != &TomlValue::String("user".to_string())); - let profile_configured = active_profile.as_deref().is_some_and(|profile| { - effective_config - .as_table() - .and_then(|table| table.get("profiles")) - .and_then(TomlValue::as_table) - .and_then(|profiles| profiles.get(profile)) - .and_then(TomlValue::as_table) - .is_some_and(|profile_config| profile_config.contains_key("approvals_reviewer")) - }); - (root_blocks_disable, profile_configured) - }; let mut permissions_history_label: Option<&'static str> = None; let mut config_edits = Vec::new(); for (feature, enabled) in updates { let feature_key = feature.key(); let mut feature_edits = Vec::new(); - if feature == Feature::GuardianApproval - && !enabled - && self.active_profile.is_some() - && root_approvals_reviewer_blocks_profile_disable - { - self.chat_widget.add_error_message( - "Cannot disable Auto-review in this profile because `approvals_reviewer` is configured outside the active profile.".to_string(), - ); - continue; - } let mut feature_config = next_config.clone(); if let Err(err) = feature_config.features.set_enabled(feature, enabled) { tracing::error!( @@ -413,24 +382,16 @@ impl App { // changes it explicitly. feature_config.approvals_reviewer = auto_review_preset.approvals_reviewer; feature_edits.push(crate::config_update::replace_config_value( - crate::config_update::profile_scoped_key_path( - active_profile.as_deref(), - "approvals_reviewer", - ), + "approvals_reviewer", serde_json::json!(auto_review_preset.approvals_reviewer.to_string()), )); if previous_approvals_reviewer != auto_review_preset.approvals_reviewer { permissions_history_label = Some("Auto-review"); } } else if !effective_enabled { - if profile_approvals_reviewer_configured || self.active_profile.is_none() { - feature_edits.push(crate::config_update::clear_config_value( - crate::config_update::profile_scoped_key_path( - active_profile.as_deref(), - "approvals_reviewer", - ), - )); - } + feature_edits.push(crate::config_update::clear_config_value( + "approvals_reviewer", + )); feature_config.approvals_reviewer = ApprovalsReviewer::User; if previous_approvals_reviewer != ApprovalsReviewer::User { permissions_history_label = Some("Default"); @@ -463,17 +424,11 @@ impl App { }; feature_edits.extend([ crate::config_update::replace_config_value( - crate::config_update::profile_scoped_key_path( - active_profile.as_deref(), - "approval_policy", - ), + "approval_policy", serde_json::json!("on-request"), ), crate::config_update::replace_config_value( - crate::config_update::profile_scoped_key_path( - active_profile.as_deref(), - "sandbox_mode", - ), + "sandbox_mode", serde_json::json!("workspace-write"), ), ]); @@ -486,7 +441,6 @@ impl App { feature_updates_to_apply.push((feature, effective_enabled)); config_edits.extend(feature_edits); config_edits.push(crate::config_update::build_feature_enabled_edit( - active_profile.as_deref(), feature_key, effective_enabled, )); @@ -815,11 +769,8 @@ impl App { effective_config: &ConfigReadResponse, feature_updates: &[(Feature, bool)], ) { - let active_profile = self.active_profile.clone(); - let active_profile = active_profile.as_deref(); for (feature, _) in feature_updates { - let enabled = - feature_enabled_from_effective_config(effective_config, active_profile, *feature); + let enabled = feature_enabled_from_effective_config(effective_config, *feature); if let Err(err) = self.config.features.set_enabled(*feature, enabled) { tracing::warn!( error = %err, @@ -840,14 +791,10 @@ impl App { return; } - if let Some(reviewer) = - approvals_reviewer_from_effective_config(effective_config, active_profile) - { + if let Some(reviewer) = approvals_reviewer_from_effective_config(effective_config) { self.set_approvals_reviewer_in_app_and_widget(reviewer); } - if let Some(policy) = - approval_policy_from_effective_config(effective_config, active_profile) - { + if let Some(policy) = approval_policy_from_effective_config(effective_config) { if let Err(err) = self .config .permissions @@ -876,7 +823,7 @@ impl App { .iter() .any(|(feature, _)| *feature == Feature::GuardianApproval) || !self.config.features.enabled(Feature::GuardianApproval) - || sandbox_mode_from_effective_config(effective_config, self.active_profile.as_deref()) + || sandbox_mode_from_effective_config(effective_config) != Some(AppServerSandboxMode::WorkspaceWrite) { return; @@ -982,10 +929,7 @@ impl App { else { return; }; - let Some(mode) = windows_sandbox_mode_from_effective_config( - &effective_config, - self.active_profile.as_deref(), - ) else { + let Some(mode) = windows_sandbox_mode_from_effective_config(&effective_config) else { return; }; self.config.permissions.windows_sandbox_mode = Some(mode); @@ -1026,59 +970,38 @@ fn overridden_write_message(write_response: &ConfigWriteResponse) -> &str { fn feature_enabled_from_effective_config( effective_config: &ConfigReadResponse, - active_profile: Option<&str>, feature: Feature, ) -> bool { - let profile_features = active_profile - .and_then(|profile| effective_config.config.profiles.get(profile)) - .and_then(|profile| profile.additional.get("features")) - .and_then(features_toml_from_json); let root_features = effective_config .config .additional .get("features") .and_then(features_toml_from_json); - profile_features + root_features .as_ref() .and_then(|features| features.entries().get(feature.key()).copied()) - .or_else(|| { - root_features - .as_ref() - .and_then(|features| features.entries().get(feature.key()).copied()) - }) .unwrap_or_else(|| feature.default_enabled()) } fn approvals_reviewer_from_effective_config( effective_config: &ConfigReadResponse, - active_profile: Option<&str>, ) -> Option { - active_profile - .and_then(|profile| effective_config.config.profiles.get(profile)) - .and_then(|profile| profile.approvals_reviewer) - .or(effective_config.config.approvals_reviewer) + effective_config + .config + .approvals_reviewer .map(codex_app_server_protocol::ApprovalsReviewer::to_core) } fn approval_policy_from_effective_config( effective_config: &ConfigReadResponse, - active_profile: Option<&str>, ) -> Option { - active_profile - .and_then(|profile| effective_config.config.profiles.get(profile)) - .and_then(|profile| profile.approval_policy) - .or(effective_config.config.approval_policy) + effective_config.config.approval_policy } fn sandbox_mode_from_effective_config( effective_config: &ConfigReadResponse, - active_profile: Option<&str>, ) -> Option { - active_profile - .and_then(|profile| effective_config.config.profiles.get(profile)) - .and_then(|profile| profile.additional.get("sandbox_mode")) - .and_then(|mode| serde_json::from_value(mode.clone()).ok()) - .or(effective_config.config.sandbox_mode) + effective_config.config.sandbox_mode } fn memories_from_effective_config(effective_config: &ConfigReadResponse) -> Option { @@ -1096,20 +1019,13 @@ fn features_toml_from_json(value: &serde_json::Value) -> Option { #[cfg(target_os = "windows")] fn windows_sandbox_mode_from_effective_config( effective_config: &ConfigReadResponse, - active_profile: Option<&str>, ) -> Option { - let profile_windows = active_profile - .and_then(|profile| effective_config.config.profiles.get(profile)) - .and_then(|profile| profile.additional.get("windows")) - .and_then(windows_toml_from_json); let root_windows = effective_config .config .additional .get("windows") .and_then(windows_toml_from_json); - profile_windows - .and_then(|windows| windows.sandbox) - .or_else(|| root_windows.and_then(|windows| windows.sandbox)) + root_windows.and_then(|windows| windows.sandbox) } #[cfg(target_os = "windows")] diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index d103bb738d7..e150dbccd21 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1158,12 +1158,9 @@ impl App { &[("result", "success")], ); } - let profile = self.active_profile.as_deref(); let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated); - let edits = crate::config_update::build_windows_sandbox_mode_edits( - profile, - elevated_enabled, - ); + let edits = + crate::config_update::build_windows_sandbox_mode_edits(elevated_enabled); match crate::config_update::write_config_batch( app_server.request_handle(), edits, @@ -1299,14 +1296,9 @@ impl App { } } AppEvent::PersistModelSelection { model, effort } => { - let profile = self.active_profile.as_deref(); match crate::config_update::write_config_batch( app_server.request_handle(), - crate::config_update::build_model_selection_edits( - profile, - model.as_str(), - effort, - ), + crate::config_update::build_model_selection_edits(model.as_str(), effort), ) .await { @@ -1320,11 +1312,6 @@ impl App { message.push(' '); message.push_str(label); } - if let Some(profile) = profile { - message.push_str(" for "); - message.push_str(profile); - message.push_str(" profile"); - } self.chat_widget.add_info_message(message, /*hint*/ None); } Err(err) => { @@ -1332,14 +1319,8 @@ impl App { error = %err, "failed to persist model selection" ); - if let Some(profile) = profile { - self.chat_widget.add_error_message(format!( - "Failed to save model for profile `{profile}`: {err}" - )); - } else { - self.chat_widget - .add_error_message(format!("Failed to save default model: {err}")); - } + self.chat_widget + .add_error_message(format!("Failed to save default model: {err}")); } } } @@ -1381,11 +1362,10 @@ impl App { self.chat_widget.on_plugin_mentions_loaded(plugins); } AppEvent::PersistPersonalitySelection { personality } => { - let profile = self.active_profile.as_deref(); match crate::config_update::write_config_batch( app_server.request_handle(), vec![crate::config_update::replace_config_value( - crate::config_update::profile_scoped_key_path(profile, "personality"), + "personality", serde_json::json!(personality.to_string()), )], ) @@ -1393,12 +1373,7 @@ impl App { { Ok(_) => { let label = Self::personality_label(personality); - let mut message = format!("Personality set to {label}"); - if let Some(profile) = profile { - message.push_str(" for "); - message.push_str(profile); - message.push_str(" profile"); - } + let message = format!("Personality set to {label}"); self.chat_widget.add_info_message(message, /*hint*/ None); } Err(err) => { @@ -1406,15 +1381,9 @@ impl App { error = %err, "failed to persist personality selection" ); - if let Some(profile) = profile { - self.chat_widget.add_error_message(format!( - "Failed to save personality for profile `{profile}`: {err}" - )); - } else { - self.chat_widget.add_error_message(format!( - "Failed to save default personality: {err}" - )); - } + self.chat_widget.add_error_message(format!( + "Failed to save default personality: {err}" + )); } } } @@ -1423,38 +1392,25 @@ impl App { self.config.service_tier = service_tier.clone(); self.sync_active_thread_service_tier_to_cached_session() .await; - let profile = self.active_profile.as_deref(); let edits = crate::config_update::build_service_tier_selection_edits( - profile, service_tier.as_deref(), ); match crate::config_update::write_config_batch(app_server.request_handle(), edits) .await { Ok(_) => { - let mut message = if let Some(service_tier) = service_tier { + let message = if let Some(service_tier) = service_tier { format!("Service tier set to {service_tier}") } else { "Service tier cleared".to_string() }; - if let Some(profile) = profile { - message.push_str(" for "); - message.push_str(profile); - message.push_str(" profile"); - } self.chat_widget.add_info_message(message, /*hint*/ None); } Err(err) => { tracing::error!(error = %err, "failed to persist service tier selection"); - if let Some(profile) = profile { - self.chat_widget.add_error_message(format!( - "Failed to save service tier for profile `{profile}`: {err}" - )); - } else { - self.chat_widget.add_error_message(format!( - "Failed to save default service tier: {err}" - )); - } + self.chat_widget.add_error_message(format!( + "Failed to save default service tier: {err}" + )); } } } @@ -1603,14 +1559,10 @@ impl App { self.chat_widget.set_approvals_reviewer(policy); self.sync_active_thread_permission_settings_to_cached_session() .await; - let profile = self.active_profile.as_deref(); if let Err(err) = crate::config_update::write_config_batch( app_server.request_handle(), vec![crate::config_update::replace_config_value( - crate::config_update::profile_scoped_key_path( - profile, - "approvals_reviewer", - ), + "approvals_reviewer", serde_json::json!(policy.to_string()), )], ) @@ -1706,11 +1658,7 @@ impl App { } } AppEvent::PersistPlanModeReasoningEffort(effort) => { - let profile = self.active_profile.as_deref(); - let key_path = crate::config_update::profile_scoped_key_path( - profile, - "plan_mode_reasoning_effort", - ); + let key_path = "plan_mode_reasoning_effort"; let edit = if let Some(effort) = effort { crate::config_update::replace_config_value( key_path, @@ -1729,15 +1677,9 @@ impl App { error = %err, "failed to persist plan mode reasoning effort" ); - if let Some(profile) = profile { - self.chat_widget.add_error_message(format!( - "Failed to save Plan mode reasoning effort for profile `{profile}`: {err}" - )); - } else { - self.chat_widget.add_error_message(format!( - "Failed to save Plan mode reasoning effort: {err}" - )); - } + self.chat_widget.add_error_message(format!( + "Failed to save Plan mode reasoning effort: {err}" + )); } } AppEvent::PersistModelMigrationPromptAcknowledged { diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index 10c234a903a..9bbe0c60b47 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -22,7 +22,6 @@ pub(super) async fn make_test_app() -> App { workspace_command_runner: None, config, state_db: None, - active_profile: None, cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index c12d44dd991..04c8bbf9a06 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2041,239 +2041,6 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit Ok(()) } -#[tokio::test] -async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_review_policy() --> Result<()> { - let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; - let codex_home = tempdir()?; - app.config.codex_home = codex_home.path().to_path_buf().abs(); - let auto_review = auto_review_mode(); - app.active_profile = Some("guardian".to_string()); - let config_toml_path = codex_home.path().join("config.toml").abs(); - let config_toml = "profile = \"guardian\"\napprovals_reviewer = \"user\"\n"; - std::fs::write(config_toml_path.as_path(), config_toml)?; - let user_config = toml::from_str::(config_toml)?; - app.config.config_layer_stack = app - .config - .config_layer_stack - .with_user_config(&config_toml_path, user_config); - app.config.approvals_reviewer = ApprovalsReviewer::User; - app.chat_widget - .set_approvals_reviewer(ApprovalsReviewer::User); - let mut app_server = start_config_write_test_app_server(&app).await?; - - app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, true)]) - .await; - - assert!(app.config.features.enabled(Feature::GuardianApproval)); - assert_eq!( - app.config.approvals_reviewer, - auto_review.approvals_reviewer - ); - assert_eq!( - app.chat_widget.config_ref().approvals_reviewer, - auto_review.approvals_reviewer - ); - assert_eq!( - op_rx.try_recv(), - Ok(Op::OverrideTurnContext { - cwd: None, - approval_policy: Some(auto_review.approval_policy), - approvals_reviewer: Some(auto_review.approvals_reviewer), - permission_profile: Some(auto_review.permission_profile()), - active_permission_profile: Some(auto_review.active_permission_profile.clone()), - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - ); - - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - let config_value = toml::from_str::(&config)?; - let profile_config = config_value - .as_table() - .and_then(|table| table.get("profiles")) - .and_then(TomlValue::as_table) - .and_then(|profiles| profiles.get("guardian")) - .and_then(TomlValue::as_table) - .expect("guardian profile should exist"); - assert_eq!( - config_value - .as_table() - .and_then(|table| table.get("approvals_reviewer")), - Some(&TomlValue::String("user".to_string())) - ); - assert_eq!( - profile_config.get("approvals_reviewer"), - Some(&TomlValue::String("guardian_subagent".to_string())) - ); - app_server.shutdown().await?; - Ok(()) -} - -#[tokio::test] -async fn update_feature_flags_disabling_guardian_in_profile_allows_inherited_user_reviewer() --> Result<()> { - let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; - let codex_home = tempdir()?; - app.config.codex_home = codex_home.path().to_path_buf().abs(); - app.active_profile = Some("guardian".to_string()); - let config_toml_path = codex_home.path().join("config.toml").abs(); - let config_toml = r#" -profile = "guardian" -approvals_reviewer = "user" - -[profiles.guardian] -approvals_reviewer = "guardian_subagent" - -[profiles.guardian.features] -guardian_approval = true -"#; - std::fs::write(config_toml_path.as_path(), config_toml)?; - let user_config = toml::from_str::(config_toml)?; - app.config.config_layer_stack = app - .config - .config_layer_stack - .with_user_config(&config_toml_path, user_config); - app.config - .features - .set_enabled(Feature::GuardianApproval, /*enabled*/ true)?; - app.chat_widget - .set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); - app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; - app.chat_widget - .set_approvals_reviewer(ApprovalsReviewer::AutoReview); - let mut app_server = start_config_write_test_app_server(&app).await?; - - app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, false)]) - .await; - - assert!(!app.config.features.enabled(Feature::GuardianApproval)); - assert!( - !app.chat_widget - .config_ref() - .features - .enabled(Feature::GuardianApproval) - ); - assert_eq!(app.config.approvals_reviewer, ApprovalsReviewer::User); - assert_eq!( - app.chat_widget.config_ref().approvals_reviewer, - ApprovalsReviewer::User - ); - assert_eq!( - op_rx.try_recv(), - Ok(Op::OverrideTurnContext { - cwd: None, - approval_policy: None, - approvals_reviewer: Some(ApprovalsReviewer::User), - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, - }) - ); - let cell = match app_event_rx.try_recv() { - Ok(AppEvent::InsertHistoryCell(cell)) => cell, - other => panic!("expected InsertHistoryCell event, got {other:?}"), - }; - let rendered = cell - .display_lines(/*width*/ 120) - .into_iter() - .map(|line| line.to_string()) - .collect::>() - .join("\n"); - assert!(rendered.contains("Permissions updated to Default")); - - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(!config.contains("guardian_approval = true")); - assert!(!config.contains("guardian_subagent")); - assert_eq!( - toml::from_str::(&config)? - .as_table() - .and_then(|table| table.get("approvals_reviewer")), - Some(&TomlValue::String("user".to_string())) - ); - app_server.shutdown().await?; - Ok(()) -} - -#[tokio::test] -async fn update_feature_flags_disabling_guardian_in_profile_keeps_inherited_non_user_reviewer_enabled() --> Result<()> { - let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; - let codex_home = tempdir()?; - app.config.codex_home = codex_home.path().to_path_buf().abs(); - app.active_profile = Some("guardian".to_string()); - let config_toml_path = codex_home.path().join("config.toml").abs(); - let config_toml = "profile = \"guardian\"\napprovals_reviewer = \"guardian_subagent\"\n\n[features]\nguardian_approval = true\n"; - std::fs::write(config_toml_path.as_path(), config_toml)?; - let user_config = toml::from_str::(config_toml)?; - app.config.config_layer_stack = app - .config - .config_layer_stack - .with_user_config(&config_toml_path, user_config); - app.config - .features - .set_enabled(Feature::GuardianApproval, /*enabled*/ true)?; - app.chat_widget - .set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); - app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; - app.chat_widget - .set_approvals_reviewer(ApprovalsReviewer::AutoReview); - let mut app_server = start_config_write_test_app_server(&app).await?; - - app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, false)]) - .await; - - assert!(app.config.features.enabled(Feature::GuardianApproval)); - assert!( - app.chat_widget - .config_ref() - .features - .enabled(Feature::GuardianApproval) - ); - assert_eq!(app.config.approvals_reviewer, ApprovalsReviewer::AutoReview); - assert_eq!( - app.chat_widget.config_ref().approvals_reviewer, - ApprovalsReviewer::AutoReview - ); - assert!( - op_rx.try_recv().is_err(), - "disabling an inherited non-user reviewer should not patch the active session" - ); - let app_events = std::iter::from_fn(|| app_event_rx.try_recv().ok()).collect::>(); - assert!( - !app_events.iter().any(|event| match event { - AppEvent::InsertHistoryCell(cell) => cell - .display_lines(/*width*/ 120) - .iter() - .any(|line| line.to_string().contains("Permissions updated to")), - _ => false, - }), - "blocking disable with inherited guardian review should not emit a permissions history update: {app_events:?}" - ); - - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(config.contains("guardian_approval = true")); - assert_eq!( - toml::from_str::(&config)? - .as_table() - .and_then(|table| table.get("approvals_reviewer")), - Some(&TomlValue::String("guardian_subagent".to_string())) - ); - app_server.shutdown().await?; - Ok(()) -} - #[tokio::test] async fn open_agent_picker_allows_existing_agent_threads_when_feature_is_disabled() -> Result<()> { let (mut app, mut app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await; @@ -3979,7 +3746,6 @@ async fn make_test_app() -> App { workspace_command_runner: None, config, state_db: None, - active_profile: None, cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), @@ -4043,7 +3809,6 @@ async fn make_test_app_with_channels() -> ( workspace_command_runner: None, config, state_db: None, - active_profile: None, cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 4597c609138..59d56ca56eb 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1224,7 +1224,6 @@ fn config_request_overrides_from_config( overrides.insert(key.to_string(), serde_json::Value::String(value)); } }; - insert("profile", config.active_profile.clone()); insert( "model_reasoning_effort", config diff --git a/codex-rs/tui/src/config_update.rs b/codex-rs/tui/src/config_update.rs index c18ea44ce93..9d73bb1a2ad 100644 --- a/codex-rs/tui/src/config_update.rs +++ b/codex-rs/tui/src/config_update.rs @@ -35,49 +35,33 @@ pub(crate) fn clear_config_value(key_path: impl Into) -> ConfigEdit { replace_config_value(key_path, JsonValue::Null) } -pub(crate) fn profile_scoped_key_path(profile: Option<&str>, key_path: &str) -> String { - if let Some(profile) = profile { - let profile = serde_json::Value::String(profile.to_string()).to_string(); - format!("profiles.{profile}.{key_path}") - } else { - key_path.to_string() - } -} - pub(crate) fn app_scoped_key_path(app_id: &str, key_path: &str) -> String { let app_id = serde_json::Value::String(app_id.to_string()).to_string(); format!("apps.{app_id}.{key_path}") } pub(crate) fn build_model_selection_edits( - profile: Option<&str>, model: &str, effort: Option, ) -> Vec { let effort_edit = effort.map_or_else( - || clear_config_value(profile_scoped_key_path(profile, "model_reasoning_effort")), + || clear_config_value("model_reasoning_effort"), |effort| { replace_config_value( - profile_scoped_key_path(profile, "model_reasoning_effort"), + "model_reasoning_effort", serde_json::json!(effort.to_string()), ) }, ); vec![ - replace_config_value( - profile_scoped_key_path(profile, "model"), - serde_json::json!(model), - ), + replace_config_value("model", serde_json::json!(model)), effort_edit, ] } -pub(crate) fn build_service_tier_selection_edits( - profile: Option<&str>, - service_tier: Option<&str>, -) -> Vec { +pub(crate) fn build_service_tier_selection_edits(service_tier: Option<&str>) -> Vec { let service_tier_edit = service_tier.map_or_else( - || clear_config_value(profile_scoped_key_path(profile, "service_tier")), + || clear_config_value("service_tier"), |service_tier| { let config_value = if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE { SERVICE_TIER_DEFAULT_REQUEST_VALUE @@ -88,25 +72,18 @@ pub(crate) fn build_service_tier_selection_edits( None => service_tier, } }; - replace_config_value( - profile_scoped_key_path(profile, "service_tier"), - serde_json::json!(config_value), - ) + replace_config_value("service_tier", serde_json::json!(config_value)) }, ); vec![service_tier_edit] } #[cfg(target_os = "windows")] -pub(crate) fn build_windows_sandbox_mode_edits( - profile: Option<&str>, - elevated_enabled: bool, -) -> Vec { - let feature_key_path = - |feature: &str| profile_scoped_key_path(profile, &format!("features.{feature}")); +pub(crate) fn build_windows_sandbox_mode_edits(elevated_enabled: bool) -> Vec { + let feature_key_path = |feature: &str| format!("features.{feature}"); vec![ replace_config_value( - profile_scoped_key_path(profile, "windows.sandbox"), + "windows.sandbox", serde_json::json!(if elevated_enabled { "elevated" } else { @@ -119,17 +96,13 @@ pub(crate) fn build_windows_sandbox_mode_edits( ] } -pub(crate) fn build_feature_enabled_edit( - profile: Option<&str>, - feature_key: &str, - enabled: bool, -) -> ConfigEdit { - let key_path = profile_scoped_key_path(profile, &format!("features.{feature_key}")); +pub(crate) fn build_feature_enabled_edit(feature_key: &str, enabled: bool) -> ConfigEdit { + let key_path = format!("features.{feature_key}"); let is_default_false_feature = FEATURES .iter() .find(|spec| spec.key == feature_key) .is_some_and(|spec| !spec.default_enabled); - if enabled || profile.is_some() || !is_default_false_feature { + if enabled || !is_default_false_feature { replace_config_value(key_path, serde_json::json!(enabled)) } else { clear_config_value(key_path) @@ -210,14 +183,6 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - #[test] - fn profile_scoped_key_path_quotes_dotted_profile_names() { - assert_eq!( - profile_scoped_key_path(Some("team.prod"), "model"), - "profiles.\"team.prod\".model" - ); - } - #[test] fn app_scoped_key_path_quotes_dotted_app_ids() { assert_eq!( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 1833e056f7e..539c7bd4614 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1658,7 +1658,6 @@ async fn run_ratatui_app( } set_default_client_residency_requirement(config.enforce_residency.value()); - let active_profile = config.active_profile.clone(); let should_show_trust_screen = should_show_trust_screen(&config); let should_prompt_windows_sandbox_nux_at_startup = cfg!(target_os = "windows") && trust_decision_was_made @@ -1716,7 +1715,6 @@ async fn run_ratatui_app( cli_kv_overrides.clone(), overrides.clone(), loader_overrides.clone(), - active_profile, prompt, images, session_selection, diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 3dbc2c3da60..fb021a0fece 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -272,7 +272,6 @@ struct PickerPage { #[derive(Clone)] struct SessionPickerViewPersistence { codex_home: PathBuf, - active_profile: Option, } struct SessionPickerRunOptions { @@ -369,7 +368,6 @@ async fn run_resume_picker_with_launch_context( initial_density: SessionListDensity::from(config.tui_session_picker_view), view_persistence: Some(SessionPickerViewPersistence { codex_home: config.codex_home.to_path_buf(), - active_profile: config.active_profile.clone(), }), pager_keymap: runtime_keymap.pager, list_keymap: runtime_keymap.list, @@ -415,7 +413,6 @@ pub async fn run_fork_picker_with_app_server( initial_density: SessionListDensity::from(config.tui_session_picker_view), view_persistence: Some(SessionPickerViewPersistence { codex_home: config.codex_home.to_path_buf(), - active_profile: config.active_profile.clone(), }), pager_keymap: runtime_keymap.pager, list_keymap: runtime_keymap.list, @@ -1679,7 +1676,6 @@ impl PickerState { }; ConfigEditsBuilder::new(&persistence.codex_home) - .with_profile(persistence.active_profile.as_deref()) .set_session_picker_view(SessionPickerViewMode::from(self.density)) .apply() .await @@ -4444,7 +4440,6 @@ mod tests { ); state.view_persistence = Some(SessionPickerViewPersistence { codex_home: tmp.path().to_path_buf(), - active_profile: None, }); state @@ -4463,39 +4458,6 @@ session_picker_view = "dense" ); } - #[tokio::test] - async fn ctrl_o_persists_density_preference_for_active_profile() { - let tmp = tempdir().expect("tmpdir"); - let loader = page_only_loader(|_| {}); - let mut state = PickerState::new( - FrameRequester::test_dummy(), - loader, - ProviderFilter::MatchDefault(String::from("openai")), - /*show_all*/ true, - /*filter_cwd*/ None, - SessionPickerAction::Resume, - ); - state.view_persistence = Some(SessionPickerViewPersistence { - codex_home: tmp.path().to_path_buf(), - active_profile: Some(String::from("work")), - }); - - state - .handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL)) - .await - .unwrap(); - - assert_eq!(state.density, SessionListDensity::Dense); - let contents = - std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); - assert_eq!( - contents, - r#"[profiles.work.tui] -session_picker_view = "dense" -"# - ); - } - #[tokio::test] async fn ctrl_o_keeps_toggled_density_when_persistence_fails() { let tmp = tempdir().expect("tmpdir"); @@ -4512,7 +4474,6 @@ session_picker_view = "dense" ); state.view_persistence = Some(SessionPickerViewPersistence { codex_home: codex_home_file, - active_profile: None, }); state