mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
fd72e99384
commit
2c6605ab35
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")?
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user