chore: config editor (#5878)

The goal is to have a single place where we actually write files

In a follow-up PR, will move everything config related in a dedicated
module and move the helpers in a dedicated file
This commit is contained in:
jif-oai
2025-10-29 20:52:46 +00:00
committed by GitHub
parent 2b20cd66af
commit db31f6966d
8 changed files with 976 additions and 1056 deletions

View File

@@ -74,9 +74,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
use codex_core::config::load_config_as_toml;
use codex_core::config_edit::CONFIG_KEY_EFFORT;
use codex_core::config_edit::CONFIG_KEY_MODEL;
use codex_core::config_edit::persist_overrides_and_clear_if_none;
use codex_core::config_edit::ConfigEditsBuilder;
use codex_core::default_client::get_codex_user_agent;
use codex_core::exec::ExecParams;
use codex_core::exec_env::create_env;
@@ -689,19 +687,12 @@ impl CodexMessageProcessor {
model,
reasoning_effort,
} = params;
let effort_str = reasoning_effort.map(|effort| effort.to_string());
let overrides: [(&[&str], Option<&str>); 2] = [
(&[CONFIG_KEY_MODEL], model.as_deref()),
(&[CONFIG_KEY_EFFORT], effort_str.as_deref()),
];
match persist_overrides_and_clear_if_none(
&self.config.codex_home,
self.config.active_profile.as_deref(),
&overrides,
)
.await
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(self.config.active_profile.as_deref())
.set_model(model.as_deref(), reasoning_effort)
.apply()
.await
{
Ok(()) => {
let response = SetDefaultModelResponse {};
@@ -710,7 +701,7 @@ impl CodexMessageProcessor {
Err(err) => {
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to persist overrides: {err}"),
message: format!("failed to persist model selection: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;

View File

@@ -11,7 +11,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_global_mcp_servers;
use codex_core::config::write_global_mcp_servers;
use codex_core::config_edit::ConfigEditsBuilder;
use codex_core::config_types::McpServerConfig;
use codex_core::config_types::McpServerTransportConfig;
use codex_core::features::Feature;
@@ -263,7 +263,10 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
servers.insert(name.clone(), new_entry);
write_global_mcp_servers(&codex_home, &servers)
ConfigEditsBuilder::new(&codex_home)
.replace_mcp_servers(&servers)
.apply()
.await
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
println!("Added global MCP server '{name}'.");
@@ -321,7 +324,10 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
let removed = servers.remove(&name).is_some();
if removed {
write_global_mcp_servers(&codex_home, &servers)
ConfigEditsBuilder::new(&codex_home)
.replace_mcp_servers(&servers)
.apply()
.await
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
}

View File

@@ -2,7 +2,7 @@ use std::path::Path;
use anyhow::Result;
use codex_core::config::load_global_mcp_servers;
use codex_core::config::write_global_mcp_servers;
use codex_core::config_edit::ConfigEditsBuilder;
use codex_core::config_types::McpServerTransportConfig;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
@@ -59,7 +59,9 @@ async fn list_and_get_render_expected_output() -> Result<()> {
}
other => panic!("unexpected transport: {other:?}"),
}
write_global_mcp_servers(codex_home.path(), &servers)?;
ConfigEditsBuilder::new(codex_home.path())
.replace_mcp_servers(&servers)
.apply_blocking()?;
let mut list_cmd = codex_command(codex_home.path())?;
let list_output = list_cmd.args(["mcp", "list"]).output()?;
@@ -149,7 +151,9 @@ async fn get_disabled_server_shows_single_line() -> Result<()> {
.get_mut("docs")
.expect("docs server should exist after add");
docs.enabled = false;
write_global_mcp_servers(codex_home.path(), &servers)?;
ConfigEditsBuilder::new(codex_home.path())
.replace_mcp_servers(&servers)
.apply_blocking()?;
let mut get_cmd = codex_command(codex_home.path())?;
let get_output = get_cmd.args(["mcp", "get", "docs"]).output()?;

View File

@@ -7,7 +7,6 @@ use crate::config_profile::ConfigProfile;
use crate::config_types::DEFAULT_OTEL_ENVIRONMENT;
use crate::config_types::History;
use crate::config_types::McpServerConfig;
use crate::config_types::McpServerTransportConfig;
use crate::config_types::Notice;
use crate::config_types::Notifications;
use crate::config_types::OtelConfig;
@@ -34,7 +33,6 @@ use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use anyhow::Context;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_protocol::config_types::ForcedLoginMethod;
@@ -53,12 +51,8 @@ use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use toml::Value as TomlValue;
use toml_edit::Array as TomlArray;
use toml_edit::DocumentMut;
use toml_edit::Item as TomlItem;
use toml_edit::Table as TomlTable;
#[cfg(target_os = "windows")]
pub const OPENAI_DEFAULT_MODEL: &str = "gpt-5";
@@ -383,141 +377,10 @@ fn ensure_no_inline_bearer_tokens(value: &TomlValue) -> std::io::Result<()> {
Ok(())
}
pub fn write_global_mcp_servers(
codex_home: &Path,
servers: &BTreeMap<String, McpServerConfig>,
) -> std::io::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
Ok(contents) => contents
.parse::<DocumentMut>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e),
};
doc.as_table_mut().remove("mcp_servers");
if !servers.is_empty() {
let mut table = TomlTable::new();
table.set_implicit(true);
doc["mcp_servers"] = TomlItem::Table(table);
for (name, config) in servers {
let mut entry = TomlTable::new();
entry.set_implicit(false);
match &config.transport {
McpServerTransportConfig::Stdio {
command,
args,
env,
env_vars,
cwd,
} => {
entry["command"] = toml_edit::value(command.clone());
if !args.is_empty() {
let mut args_array = TomlArray::new();
for arg in args {
args_array.push(arg.clone());
}
entry["args"] = TomlItem::Value(args_array.into());
}
if let Some(env) = env
&& !env.is_empty()
{
let mut env_table = TomlTable::new();
env_table.set_implicit(false);
let mut pairs: Vec<_> = env.iter().collect();
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
for (key, value) in pairs {
env_table.insert(key, toml_edit::value(value.clone()));
}
entry["env"] = TomlItem::Table(env_table);
}
if !env_vars.is_empty() {
entry["env_vars"] =
TomlItem::Value(env_vars.iter().collect::<TomlArray>().into());
}
if let Some(cwd) = cwd {
entry["cwd"] = toml_edit::value(cwd.to_string_lossy().to_string());
}
}
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
env_http_headers,
} => {
entry["url"] = toml_edit::value(url.clone());
if let Some(env_var) = bearer_token_env_var {
entry["bearer_token_env_var"] = toml_edit::value(env_var.clone());
}
if let Some(headers) = http_headers
&& !headers.is_empty()
{
let mut table = TomlTable::new();
table.set_implicit(false);
let mut pairs: Vec<_> = headers.iter().collect();
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
for (key, value) in pairs {
table.insert(key, toml_edit::value(value.clone()));
}
entry["http_headers"] = TomlItem::Table(table);
}
if let Some(headers) = env_http_headers
&& !headers.is_empty()
{
let mut table = TomlTable::new();
table.set_implicit(false);
let mut pairs: Vec<_> = headers.iter().collect();
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
for (key, value) in pairs {
table.insert(key, toml_edit::value(value.clone()));
}
entry["env_http_headers"] = TomlItem::Table(table);
}
}
}
if !config.enabled {
entry["enabled"] = toml_edit::value(false);
}
if let Some(timeout) = config.startup_timeout_sec {
entry["startup_timeout_sec"] = toml_edit::value(timeout.as_secs_f64());
}
if let Some(timeout) = config.tool_timeout_sec {
entry["tool_timeout_sec"] = toml_edit::value(timeout.as_secs_f64());
}
if let Some(enabled_tools) = &config.enabled_tools {
entry["enabled_tools"] =
TomlItem::Value(enabled_tools.iter().collect::<TomlArray>().into());
}
if let Some(disabled_tools) = &config.disabled_tools {
entry["disabled_tools"] =
TomlItem::Value(disabled_tools.iter().collect::<TomlArray>().into());
}
doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry);
}
}
std::fs::create_dir_all(codex_home)?;
let tmp_file = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
tmp_file.persist(config_path).map_err(|err| err.error)?;
Ok(())
}
fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyhow::Result<()> {
pub(crate) fn set_project_trusted_inner(
doc: &mut DocumentMut,
project_path: &Path,
) -> anyhow::Result<()> {
// Ensure we render a human-friendly structure:
//
// [projects]
@@ -585,209 +448,11 @@ fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyh
/// Patch `CODEX_HOME/config.toml` project state.
/// Use with caution.
pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
use crate::config_edit::ConfigEditsBuilder;
set_project_trusted_inner(&mut doc, project_path)?;
// ensure codex_home exists
std::fs::create_dir_all(codex_home)?;
// create a tmp_file
let tmp_file = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
// atomically move the tmp file into config.toml
tmp_file.persist(config_path)?;
Ok(())
}
/// Persist the acknowledgement flag for the Windows onboarding screen.
pub fn set_windows_wsl_setup_acknowledged(
codex_home: &Path,
acknowledged: bool,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(config_path.clone()) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
doc["windows_wsl_setup_acknowledged"] = toml_edit::value(acknowledged);
std::fs::create_dir_all(codex_home)?;
let tmp_file = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
tmp_file.persist(config_path)?;
Ok(())
}
/// Persist the acknowledgement flag for the full access warning prompt.
pub fn set_hide_full_access_warning(codex_home: &Path, acknowledged: bool) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(config_path.clone()) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
let notices_table = load_or_create_top_level_table(&mut doc, Notice::TABLE_KEY)?;
notices_table["hide_full_access_warning"] = toml_edit::value(acknowledged);
std::fs::create_dir_all(codex_home)?;
let tmp_file = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
tmp_file.persist(config_path)?;
Ok(())
}
fn load_or_create_top_level_table<'a>(
doc: &'a mut DocumentMut,
key: &str,
) -> anyhow::Result<&'a mut toml_edit::Table> {
let mut created_table = false;
let root = doc.as_table_mut();
let needs_table =
!root.contains_key(key) || root.get(key).and_then(|item| item.as_table()).is_none();
if needs_table {
root.insert(key, toml_edit::table());
created_table = true;
}
let Some(table) = doc[key].as_table_mut() else {
return Err(anyhow::anyhow!(format!(
"table [{key}] missing after initialization"
)));
};
if created_table {
table.set_implicit(true);
}
Ok(table)
}
fn ensure_profile_table<'a>(
doc: &'a mut DocumentMut,
profile_name: &str,
) -> anyhow::Result<&'a mut toml_edit::Table> {
let mut created_profiles_table = false;
{
let root = doc.as_table_mut();
let needs_table = !root.contains_key("profiles")
|| root
.get("profiles")
.and_then(|item| item.as_table())
.is_none();
if needs_table {
root.insert("profiles", toml_edit::table());
created_profiles_table = true;
}
}
let Some(profiles_table) = doc["profiles"].as_table_mut() else {
return Err(anyhow::anyhow!(
"profiles table missing after initialization"
));
};
if created_profiles_table {
profiles_table.set_implicit(true);
}
let needs_profile_table = !profiles_table.contains_key(profile_name)
|| profiles_table
.get(profile_name)
.and_then(|item| item.as_table())
.is_none();
if needs_profile_table {
profiles_table.insert(profile_name, toml_edit::table());
}
let Some(profile_table) = profiles_table
.get_mut(profile_name)
.and_then(|item| item.as_table_mut())
else {
return Err(anyhow::anyhow!(format!(
"profile table missing for {profile_name}"
)));
};
profile_table.set_implicit(false);
Ok(profile_table)
}
// TODO(jif) refactor config persistence.
pub async fn persist_model_selection(
codex_home: &Path,
active_profile: Option<&str>,
model: &str,
effort: Option<ReasoningEffort>,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let serialized = match tokio::fs::read_to_string(&config_path).await {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) => return Err(err.into()),
};
let mut doc = if serialized.is_empty() {
DocumentMut::new()
} else {
serialized.parse::<DocumentMut>()?
};
if let Some(profile_name) = active_profile {
let profile_table = ensure_profile_table(&mut doc, profile_name)?;
profile_table["model"] = toml_edit::value(model);
match effort {
Some(effort) => {
profile_table["model_reasoning_effort"] = toml_edit::value(effort.to_string());
}
None => {
profile_table.remove("model_reasoning_effort");
}
}
} else {
let table = doc.as_table_mut();
table["model"] = toml_edit::value(model);
match effort {
Some(effort) => {
table["model_reasoning_effort"] = toml_edit::value(effort.to_string());
}
None => {
table.remove("model_reasoning_effort");
}
}
}
// TODO(jif) refactor the home creation
tokio::fs::create_dir_all(codex_home)
.await
.with_context(|| {
format!(
"failed to create Codex home directory at {}",
codex_home.display()
)
})?;
tokio::fs::write(&config_path, doc.to_string())
.await
.with_context(|| format!("failed to persist config.toml at {}", config_path.display()))?;
Ok(())
ConfigEditsBuilder::new(codex_home)
.set_project_trusted(project_path)
.apply_blocking()
}
/// Apply a single dotted-path override onto a TOML value.
@@ -1579,7 +1244,11 @@ pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
#[cfg(test)]
mod tests {
use crate::config_edit::ConfigEdit;
use crate::config_edit::ConfigEditsBuilder;
use crate::config_edit::apply_blocking;
use crate::config_types::HistoryPersistence;
use crate::config_types::McpServerTransportConfig;
use crate::config_types::Notifications;
use crate::features::Feature;
@@ -2107,7 +1776,7 @@ trust_level = "trusted"
}
#[tokio::test]
async fn write_global_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let mut servers = BTreeMap::new();
@@ -2129,7 +1798,11 @@ trust_level = "trusted"
},
);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let loaded = load_global_mcp_servers(codex_home.path()).await?;
assert_eq!(loaded.len(), 1);
@@ -2155,7 +1828,11 @@ trust_level = "trusted"
assert!(docs.enabled);
let empty = BTreeMap::new();
write_global_mcp_servers(codex_home.path(), &empty)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(empty.clone())],
)?;
let loaded = load_global_mcp_servers(codex_home.path()).await?;
assert!(loaded.is_empty());
@@ -2243,7 +1920,7 @@ bearer_token = "secret"
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2267,7 +1944,11 @@ bearer_token = "secret"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2310,7 +1991,7 @@ ZIG_VAR = "3"
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2331,7 +2012,11 @@ ZIG_VAR = "3"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2353,7 +2038,7 @@ ZIG_VAR = "3"
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cwd_path = PathBuf::from("/tmp/codex-mcp");
@@ -2375,7 +2060,11 @@ ZIG_VAR = "3"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2397,8 +2086,7 @@ ZIG_VAR = "3"
}
#[tokio::test]
async fn write_global_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow::Result<()>
{
async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2418,7 +2106,11 @@ ZIG_VAR = "3"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2453,8 +2145,7 @@ startup_timeout_sec = 2.0
}
#[tokio::test]
async fn write_global_mcp_servers_streamable_http_serializes_custom_headers()
-> anyhow::Result<()> {
async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2476,7 +2167,11 @@ startup_timeout_sec = 2.0
disabled_tools: None,
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2522,8 +2217,7 @@ X-Auth = "DOCS_AUTH"
}
#[tokio::test]
async fn write_global_mcp_servers_streamable_http_removes_optional_sections()
-> anyhow::Result<()> {
async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
@@ -2548,7 +2242,11 @@ X-Auth = "DOCS_AUTH"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let serialized_with_optional = std::fs::read_to_string(&config_path)?;
assert!(serialized_with_optional.contains("bearer_token_env_var = \"MCP_TOKEN\""));
assert!(serialized_with_optional.contains("[mcp_servers.docs.http_headers]"));
@@ -2570,7 +2268,11 @@ X-Auth = "DOCS_AUTH"
disabled_tools: None,
},
);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let serialized = std::fs::read_to_string(&config_path)?;
assert_eq!(
@@ -2603,7 +2305,7 @@ url = "https://example.com/mcp"
}
#[tokio::test]
async fn write_global_mcp_servers_streamable_http_isolates_headers_between_servers()
async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
-> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
@@ -2650,7 +2352,11 @@ url = "https://example.com/mcp"
),
]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let serialized = std::fs::read_to_string(&config_path)?;
assert!(
@@ -2704,7 +2410,7 @@ url = "https://example.com/mcp"
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2725,7 +2431,11 @@ url = "https://example.com/mcp"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2742,7 +2452,7 @@ url = "https://example.com/mcp"
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
@@ -2763,7 +2473,11 @@ url = "https://example.com/mcp"
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
@@ -2785,16 +2499,13 @@ url = "https://example.com/mcp"
}
#[tokio::test]
async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> {
async fn set_model_updates_defaults() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
persist_model_selection(
codex_home.path(),
None,
"gpt-5-codex",
Some(ReasoningEffort::High),
)
.await?;
ConfigEditsBuilder::new(codex_home.path())
.set_model(Some("gpt-5-codex"), Some(ReasoningEffort::High))
.apply()
.await?;
let serialized =
tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
@@ -2807,7 +2518,7 @@ url = "https://example.com/mcp"
}
#[tokio::test]
async fn persist_model_selection_overwrites_existing_model() -> anyhow::Result<()> {
async fn set_model_overwrites_existing_model() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
@@ -2823,13 +2534,10 @@ model = "gpt-4.1"
)
.await?;
persist_model_selection(
codex_home.path(),
None,
"o4-mini",
Some(ReasoningEffort::High),
)
.await?;
ConfigEditsBuilder::new(codex_home.path())
.set_model(Some("o4-mini"), Some(ReasoningEffort::High))
.apply()
.await?;
let serialized = tokio::fs::read_to_string(config_path).await?;
let parsed: ConfigToml = toml::from_str(&serialized)?;
@@ -2848,16 +2556,14 @@ model = "gpt-4.1"
}
#[tokio::test]
async fn persist_model_selection_updates_profile() -> anyhow::Result<()> {
async fn set_model_updates_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
persist_model_selection(
codex_home.path(),
Some("dev"),
"gpt-5-codex",
Some(ReasoningEffort::Medium),
)
.await?;
ConfigEditsBuilder::new(codex_home.path())
.with_profile(Some("dev"))
.set_model(Some("gpt-5-codex"), Some(ReasoningEffort::Medium))
.apply()
.await?;
let serialized =
tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
@@ -2877,7 +2583,7 @@ model = "gpt-4.1"
}
#[tokio::test]
async fn persist_model_selection_updates_existing_profile() -> anyhow::Result<()> {
async fn set_model_updates_existing_profile() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
@@ -2894,13 +2600,11 @@ model = "gpt-5-codex"
)
.await?;
persist_model_selection(
codex_home.path(),
Some("dev"),
"o4-high",
Some(ReasoningEffort::Medium),
)
.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)?;

File diff suppressed because it is too large Load Diff

View File

@@ -361,7 +361,7 @@ pub struct Notice {
}
impl Notice {
/// used by set_hide_full_access_warning until we refactor config updates
/// referenced by config_edit helpers when writing notice flags
pub(crate) const TABLE_KEY: &'static str = "notice";
}

View File

@@ -17,8 +17,7 @@ use codex_ansi_escape::ansi_escape_line;
use codex_core::AuthManager;
use codex_core::ConversationManager;
use codex_core::config::Config;
use codex_core::config::persist_model_selection;
use codex_core::config::set_hide_full_access_warning;
use codex_core::config_edit::ConfigEditsBuilder;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::SessionSource;
use codex_core::protocol::TokenUsage;
@@ -374,7 +373,10 @@ impl App {
}
AppEvent::PersistModelSelection { model, effort } => {
let profile = self.active_profile.as_deref();
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_model(Some(model.as_str()), effort)
.apply()
.await
{
Ok(()) => {
@@ -421,7 +423,11 @@ impl App {
self.chat_widget.set_full_access_warning_acknowledged(ack);
}
AppEvent::PersistFullAccessWarningAcknowledged => {
if let Err(err) = set_hide_full_access_warning(&self.config.codex_home, true) {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_full_access_warning(true)
.apply()
.await
{
tracing::error!(
error = %err,
"failed to persist full access warning acknowledgement"

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
use codex_core::config::set_windows_wsl_setup_acknowledged;
use codex_core::config_edit::ConfigEditsBuilder;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -66,7 +66,10 @@ impl WindowsSetupWidget {
fn handle_continue(&mut self) {
self.highlighted = WindowsSetupSelection::Continue;
match set_windows_wsl_setup_acknowledged(&self.codex_home, true) {
match ConfigEditsBuilder::new(&self.codex_home)
.set_windows_wsl_setup_acknowledged(true)
.apply_blocking()
{
Ok(()) => {
self.selection = Some(WindowsSetupSelection::Continue);
self.exit_requested = false;