config: remove legacy profile write paths (#24055)

## Why

[#23883](https://github.com/openai/codex/pull/23883) moved the
user-facing `--profile` flag onto profile v2 and
[#23886](https://github.com/openai/codex/pull/23886) removed CLI
forwarding for the legacy profile-v1 path. Core and TUI config
persistence still carried `active_profile` and
`ConfigEditsBuilder::with_profile`, which let later writes continue
targeting legacy `[profiles.<name>]` tables after profile selection
moved to profile-v2 config files.

## What

- Remove legacy profile routing from
[`ConfigEditsBuilder`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/core/src/config/edit.rs#L1064-L1294),
so core config edits no longer carry `with_profile` or infer
`[profiles.*]` write targets from a `profile` key.
- Drop `active_profile` plumbing from runtime `Config`, TUI
startup/state, app-server config override forwarding, and Windows
sandbox setup persistence.
- Make app-server-backed TUI config edits use unscoped model,
service-tier, feature, Auto-review, plan-mode, and Windows sandbox paths
through
[`tui/src/config_update.rs`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/tui/src/config_update.rs#L43-L112).
- Update config edit coverage so legacy `profile` state stays untouched
by direct model writes, and remove tests whose only contract was the
deleted profile-scoped persistence path.

## Testing

- Not run locally.
This commit is contained in:
jif-oai
2026-05-22 12:50:42 +02:00
committed by GitHub
Unverified
parent fd72e99384
commit 2c6605ab35
18 changed files with 107 additions and 974 deletions
@@ -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
}
-206
View File
@@ -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,
+36 -135
View File
@@ -513,13 +513,6 @@ mod document_helpers {
struct ConfigDocument {
doc: DocumentMut,
profile: Option<String>,
}
#[derive(Copy, Clone)]
enum Scope {
Global,
Profile,
}
#[derive(Copy, Clone)]
@@ -529,25 +522,25 @@ enum TraversalMode {
}
impl ConfigDocument {
fn new(doc: DocumentMut, profile: Option<String>) -> Self {
Self { doc, profile }
fn new(doc: DocumentMut) -> Self {
Self { doc }
}
fn apply(&mut self, edit: &ConfigEdit) -> anyhow::Result<bool> {
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<TomlItem>) -> bool {
fn write_optional_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> 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::<Vec<_>>();
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::<Vec<_>>();
self.remove(&resolved)
}
@@ -709,7 +699,6 @@ impl ConfigDocument {
.filter(|disabled_tool| seen.insert(disabled_tool.clone()))
.collect::<Vec<_>>();
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<String, McpServerConfig>) -> 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<String> {
let resolved: Vec<String> = 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::<DocumentMut>()?
};
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.<name>]` 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<ConfigEdit>,
) -> anyhow::Result<()> {
pub async fn apply(codex_home: &Path, edits: Vec<ConfigEdit>) -> 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<String>,
edits: Vec<ConfigEdit>,
}
@@ -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<ReasoningEffort>) -> 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")?
+19 -106
View File
@@ -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"
model = "o5-preview"
model_reasoning_effort = "minimal"
[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");
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"
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");
-4
View File
@@ -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<String>,
/// 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,
+1 -1
View File
@@ -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);
-3
View File
@@ -252,7 +252,6 @@ pub struct WindowsSandboxSetupRequest {
pub command_cwd: PathBuf,
pub env_map: HashMap<String, String>,
pub codex_home: PathBuf,
pub active_profile: Option<String>,
}
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()
+2 -10
View File
@@ -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<HashMap<String, Value>> {
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<codex_app_server_protocol::ApprovalsReviewer> {
@@ -271,7 +271,6 @@ fn new_config(model: Option<String>, 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,
-3
View File
@@ -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<StateDbHandle>,
pub(crate) active_profile: Option<String>,
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<String>,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
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,
+18 -102
View File
@@ -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<ApprovalsReviewer> {
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<AskForApproval> {
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<AppServerSandboxMode> {
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<MemoriesToml> {
@@ -1096,20 +1019,13 @@ fn features_toml_from_json(value: &serde_json::Value) -> Option<FeaturesToml> {
#[cfg(target_os = "windows")]
fn windows_sandbox_mode_from_effective_config(
effective_config: &ConfigReadResponse,
active_profile: Option<&str>,
) -> Option<codex_config::types::WindowsSandboxModeToml> {
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")]
+19 -77
View File
@@ -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 {
-1
View File
@@ -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(),
-235
View File
@@ -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::<TomlValue>(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::<TomlValue>(&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::<TomlValue>(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::<Vec<_>>()
.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::<TomlValue>(&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::<TomlValue>(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::<Vec<_>>();
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::<TomlValue>(&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(),
-1
View File
@@ -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
+12 -47
View File
@@ -35,49 +35,33 @@ pub(crate) fn clear_config_value(key_path: impl Into<String>) -> 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<impl ToString>,
) -> Vec<ConfigEdit> {
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<ConfigEdit> {
pub(crate) fn build_service_tier_selection_edits(service_tier: Option<&str>) -> Vec<ConfigEdit> {
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<ConfigEdit> {
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<ConfigEdit> {
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!(
-2
View File
@@ -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,
-39
View File
@@ -272,7 +272,6 @@ struct PickerPage {
#[derive(Clone)]
struct SessionPickerViewPersistence {
codex_home: PathBuf,
active_profile: Option<String>,
}
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