Rewrite branch to config-only

This commit is contained in:
pakrym-oai
2026-03-21 12:12:59 -07:00
parent ec32866c37
commit 45aac237d9
51 changed files with 734 additions and 308 deletions
+9
View File
@@ -1488,6 +1488,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-arg0",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-feedback",
"codex-protocol",
@@ -1537,6 +1538,7 @@ dependencies = [
"clap",
"codex-app-server-protocol",
"codex-core",
"codex-exec-server",
"codex-otel",
"codex-protocol",
"codex-utils-cli",
@@ -1643,6 +1645,7 @@ dependencies = [
"clap",
"codex-connectors",
"codex-core",
"codex-exec-server",
"codex-git",
"codex-utils-cargo-bin",
"codex-utils-cli",
@@ -1671,6 +1674,7 @@ dependencies = [
"codex-config",
"codex-core",
"codex-exec",
"codex-exec-server",
"codex-execpolicy",
"codex-features",
"codex-login",
@@ -1767,6 +1771,7 @@ dependencies = [
"codex-client",
"codex-cloud-tasks-client",
"codex-core",
"codex-exec-server",
"codex-login",
"codex-tui",
"codex-utils-cli",
@@ -1975,6 +1980,7 @@ dependencies = [
"codex-arg0",
"codex-cloud-requirements",
"codex-core",
"codex-exec-server",
"codex-feedback",
"codex-otel",
"codex-protocol",
@@ -2240,6 +2246,7 @@ dependencies = [
"anyhow",
"codex-arg0",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-protocol",
"codex-shell-command",
@@ -2587,6 +2594,7 @@ dependencies = [
"codex-client",
"codex-cloud-requirements",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-feedback",
"codex-file-search",
@@ -2680,6 +2688,7 @@ dependencies = [
"codex-client",
"codex-cloud-requirements",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-feedback",
"codex-file-search",
+1
View File
@@ -29,6 +29,7 @@ tracing = { workspace = true }
url = { workspace = true }
[dev-dependencies]
codex-exec-server = { workspace = true }
pretty_assertions = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
+10 -3
View File
@@ -870,10 +870,17 @@ mod tests {
use tokio_tungstenite::tungstenite::Message;
async fn build_test_config() -> Config {
match ConfigBuilder::default().build().await {
match ConfigBuilder::default()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => config,
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
.expect("default config should load"),
Err(_) => Config::load_default_with_cli_overrides(
Vec::new(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.expect("default config should load"),
}
}
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
codex-app-server-protocol = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-cli = { workspace = true }
+6 -3
View File
@@ -2097,9 +2097,12 @@ impl TestClientTracing {
}
.parse_overrides()
.map_err(|e| anyhow::anyhow!("error parsing -c overrides: {e}"))?;
let config = Config::load_with_cli_overrides(cli_kv_overrides)
.await
.context("error loading config")?;
let config = Config::load_with_cli_overrides(
cli_kv_overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("error loading config")?;
let otel_provider = codex_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
@@ -511,7 +511,7 @@ impl CodexMessageProcessor {
.cli_overrides(self.cli_overrides.clone())
.fallback_cwd(fallback_cwd)
.cloud_requirements(cloud_requirements)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.map_err(|err| JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
@@ -5689,7 +5689,7 @@ impl CodexMessageProcessor {
let edits = vec![ConfigEdit::SetSkillConfig { path, enabled }];
let result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits(edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match result {
@@ -7785,7 +7785,7 @@ async fn sync_default_client_residency_requirement(
match codex_core::config::ConfigBuilder::default()
.cli_overrides(cli_overrides.to_vec())
.cloud_requirements(loader)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => set_default_client_residency_requirement(config.enforce_residency.value()),
@@ -7829,7 +7829,7 @@ async fn derive_config_from_params(
.cli_overrides(merged_cli_overrides)
.harness_overrides(typesafe_overrides)
.cloud_requirements(cloud_requirements.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
}
@@ -7858,7 +7858,7 @@ async fn derive_config_for_cwd(
.harness_overrides(typesafe_overrides)
.fallback_cwd(cwd)
.cloud_requirements(cloud_requirements.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
}
+1
View File
@@ -93,6 +93,7 @@ impl ConfigApi {
self.cli_overrides.clone(),
self.loader_overrides.clone(),
cloud_requirements,
codex_exec_server::Environment::default().get_filesystem(),
)
}
+10 -3
View File
@@ -742,10 +742,17 @@ mod tests {
use pretty_assertions::assert_eq;
async fn build_test_config() -> Config {
match ConfigBuilder::default().build().await {
match ConfigBuilder::default()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => config,
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
.expect("default config should load"),
Err(_) => Config::load_default_with_cli_overrides(
Vec::new(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.expect("default config should load"),
}
}
+11 -8
View File
@@ -397,7 +397,7 @@ pub async fn run_main_with_transport(
let cloud_requirements = match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides.clone())
.loader_overrides(loader_overrides.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => {
@@ -441,19 +441,22 @@ pub async fn run_main_with_transport(
.cli_overrides(cli_kv_overrides.clone())
.loader_overrides(loader_overrides)
.cloud_requirements(cloud_requirements.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => config,
Err(err) => {
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
config_warnings.push(message);
Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?
let file_system = codex_exec_server::Environment::default().get_filesystem();
Config::load_default_with_cli_overrides(cli_kv_overrides.clone(), file_system)
.await
.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?
}
};
+1
View File
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-connectors = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+1
View File
@@ -27,6 +27,7 @@ pub async fn run_apply_command(
.config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?,
codex_exec_server::Environment::default().get_filesystem(),
)
.await?;
+1
View File
@@ -29,6 +29,7 @@ codex-utils-cli = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-exec-server = { workspace = true }
codex-execpolicy = { workspace = true }
codex-features = { workspace = true }
codex-login = { workspace = true }
+3 -1
View File
@@ -436,7 +436,9 @@ async fn build_debug_sandbox_config(
.codex_home(codex_home.clone())
.fallback_cwd(Some(codex_home));
}
builder.build().await
builder
.build(codex_exec_server::Environment::default().get_filesystem())
.await
}
fn config_uses_permission_profiles(config: &Config) -> bool {
+6 -1
View File
@@ -372,7 +372,12 @@ async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config
}
};
match Config::load_with_cli_overrides(cli_overrides).await {
match Config::load_with_cli_overrides(
cli_overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
Ok(config) => config,
Err(e) => {
eprintln!("Error loading configuration: {e}");
+9 -4
View File
@@ -884,6 +884,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
let config = Config::load_with_cli_overrides_and_harness_overrides(
cli_kv_overrides,
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await?;
let mut rows = Vec::with_capacity(FEATURES.len());
@@ -923,7 +924,7 @@ async fn enable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyhow
ConfigEditsBuilder::new(&codex_home)
.with_profile(interactive.config_profile.as_deref())
.set_feature_enabled(feature, /*enabled*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await?;
println!("Enabled feature `{feature}` in config.toml.");
maybe_print_under_development_feature_warning(&codex_home, interactive, feature);
@@ -936,7 +937,7 @@ async fn disable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyho
ConfigEditsBuilder::new(&codex_home)
.with_profile(interactive.config_profile.as_deref())
.set_feature_enabled(feature, /*enabled*/ false)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await?;
println!("Disabled feature `{feature}` in config.toml.");
Ok(())
@@ -976,8 +977,12 @@ async fn run_debug_clear_memories_command(
config_profile: interactive.config_profile.clone(),
..Default::default()
};
let config =
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;
let config = Config::load_with_cli_overrides_and_harness_overrides(
cli_kv_overrides,
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await?;
let state_path = state_db_path(config.sqlite_home.as_path());
let mut cleared_state_db = false;
+32 -17
View File
@@ -240,9 +240,12 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let config = Config::load_with_cli_overrides(
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("failed to load configuration")?;
let AddArgs {
name,
@@ -312,7 +315,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
ConfigEditsBuilder::new(&codex_home)
.replace_mcp_servers(&servers)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
@@ -368,7 +371,7 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
if removed {
ConfigEditsBuilder::new(&codex_home)
.replace_mcp_servers(&servers)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
}
@@ -386,9 +389,12 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let config = Config::load_with_cli_overrides(
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("failed to load configuration")?;
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
@@ -437,9 +443,12 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let config = Config::load_with_cli_overrides(
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("failed to load configuration")?;
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
@@ -467,9 +476,12 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let config = Config::load_with_cli_overrides(
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("failed to load configuration")?;
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
@@ -716,9 +728,12 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let config = Config::load_with_cli_overrides(
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.context("failed to load configuration")?;
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
+1
View File
@@ -22,6 +22,7 @@ codex-cloud-tasks-client = { path = "../cloud-tasks-client", features = [
] }
codex-client = { workspace = true }
codex-core = { path = "../core" }
codex-exec-server = { workspace = true }
codex-login = { path = "../login" }
codex-tui = { path = "../tui" }
codex-utils-cli = { workspace = true }
+6 -1
View File
@@ -61,7 +61,12 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
pub async fn load_auth_manager() -> Option<AuthManager> {
// TODO: pass in cli overrides once cloud tasks properly support them.
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
let config = Config::load_with_cli_overrides(
Vec::new(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.ok()?;
Some(AuthManager::new(
config.codex_home,
/*enable_codex_api_key_env*/ false,
+7 -3
View File
@@ -71,7 +71,8 @@ async fn apply_role_to_config_inner(
role_layer_toml,
preserve_current_profile,
preserve_current_provider,
)?;
)
.await?;
Ok(())
}
@@ -143,7 +144,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b
mod reload {
use super::*;
pub(super) fn build_next_config(
pub(super) async fn build_next_config(
config: &Config,
role_layer_toml: TomlValue,
preserve_current_profile: bool,
@@ -159,12 +160,15 @@ mod reload {
merged_config.profile = None;
}
let file_system = codex_exec_server::Environment::default().get_filesystem();
let mut next_config = Config::load_config_with_layer_stack(
merged_config,
reload_overrides(config, preserve_current_provider),
config.codex_home.clone(),
config_layer_stack,
)?;
file_system,
)
.await?;
if preserve_current_profile {
next_config.active_profile = config.active_profile.clone();
}
+89 -65
View File
@@ -4,6 +4,7 @@ use super::AgentsToml;
use super::ConfigToml;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use serde::Deserialize;
@@ -14,17 +15,18 @@ use std::path::Path;
use std::path::PathBuf;
use toml::Value as TomlValue;
pub(crate) fn load_agent_roles(
pub(crate) async fn load_agent_roles(
cfg: &ConfigToml,
config_layer_stack: &ConfigLayerStack,
startup_warnings: &mut Vec<String>,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
let layers = config_layer_stack.get_layers(
ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ false,
);
if layers.is_empty() {
return load_agent_roles_without_layers(cfg);
return load_agent_roles_without_layers(cfg, file_system).await;
}
let mut roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
@@ -40,13 +42,14 @@ pub(crate) fn load_agent_roles(
};
if let Some(agents_toml) = agents_toml {
for (declared_role_name, role_toml) in &agents_toml.roles {
let (role_name, role) = match read_declared_role(declared_role_name, role_toml) {
Ok(role) => role,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
let (role_name, role) =
match read_declared_role(declared_role_name, role_toml, file_system).await {
Ok(role) => role,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
if let Some(config_file) = role.config_file.clone() {
declared_role_files.insert(config_file);
}
@@ -71,7 +74,10 @@ pub(crate) fn load_agent_roles(
config_folder.as_path().join("agents").as_path(),
&declared_role_files,
startup_warnings,
)? {
file_system,
)
.await?
{
if layer_roles.contains_key(&role_name) {
push_agent_role_warning(
startup_warnings,
@@ -113,13 +119,15 @@ fn push_agent_role_warning(startup_warnings: &mut Vec<String>, err: std::io::Err
startup_warnings.push(message);
}
fn load_agent_roles_without_layers(
async fn load_agent_roles_without_layers(
cfg: &ConfigToml,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
let mut roles = BTreeMap::new();
if let Some(agents_toml) = cfg.agents.as_ref() {
for (declared_role_name, role_toml) in &agents_toml.roles {
let (role_name, role) = read_declared_role(declared_role_name, role_toml)?;
let (role_name, role) =
read_declared_role(declared_role_name, role_toml, file_system).await?;
validate_required_agent_role_description(&role_name, role.description.as_deref())?;
if roles.insert(role_name.clone(), role).is_some() {
@@ -134,14 +142,19 @@ fn load_agent_roles_without_layers(
Ok(roles)
}
fn read_declared_role(
async fn read_declared_role(
declared_role_name: &str,
role_toml: &AgentRoleToml,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<(String, AgentRoleConfig)> {
let mut role = agent_role_config_from_toml(declared_role_name, role_toml)?;
validate_agent_role_config_file(declared_role_name, role.config_file.as_deref(), file_system)
.await?;
let mut role_name = declared_role_name.to_string();
if let Some(config_file) = role.config_file.as_deref() {
let parsed_file = read_resolved_agent_role_file(config_file, Some(declared_role_name))?;
let parsed_file =
read_resolved_agent_role_file(config_file, Some(declared_role_name), file_system)
.await?;
role_name = parsed_file.role_name;
role.description = parsed_file.description.or(role.description);
role.nickname_candidates = parsed_file.nickname_candidates.or(role.nickname_candidates);
@@ -176,7 +189,6 @@ fn agent_role_config_from_toml(
role: &AgentRoleToml,
) -> std::io::Result<AgentRoleConfig> {
let config_file = role.config_file.as_ref().map(AbsolutePathBuf::to_path_buf);
validate_agent_role_config_file(role_name, config_file.as_deref())?;
let description = normalize_agent_role_description(
&format!("agents.{role_name}.description"),
role.description.as_deref(),
@@ -293,11 +305,12 @@ pub(crate) fn parse_agent_role_file_contents(
})
}
fn read_resolved_agent_role_file(
async fn read_resolved_agent_role_file(
path: &Path,
role_name_hint: Option<&str>,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<ResolvedAgentRoleFile> {
let contents = std::fs::read_to_string(path)?;
let contents = super::read_text_file(path, file_system).await?;
parse_agent_role_file_contents(
&contents,
path,
@@ -359,24 +372,29 @@ fn validate_agent_role_file_developer_instructions(
}
}
fn validate_agent_role_config_file(
async fn validate_agent_role_config_file(
role_name: &str,
config_file: Option<&Path>,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<()> {
let Some(config_file) = config_file else {
return Ok(());
};
let metadata = std::fs::metadata(config_file).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
config_file.display()
),
)
})?;
if metadata.is_file() {
let absolute_path = super::absolute_path_for_filesystem_read(config_file)?;
let metadata = file_system
.get_metadata(&absolute_path)
.await
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
config_file.display()
),
)
})?;
if metadata.is_file {
Ok(())
} else {
Err(std::io::Error::new(
@@ -441,25 +459,31 @@ fn normalize_agent_role_nickname_candidates(
Ok(Some(normalized_candidates))
}
fn discover_agent_roles_in_dir(
async fn discover_agent_roles_in_dir(
agents_dir: &Path,
declared_role_files: &BTreeSet<PathBuf>,
startup_warnings: &mut Vec<String>,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
let mut roles = BTreeMap::new();
for agent_file in collect_agent_role_files(agents_dir)? {
for agent_file in collect_agent_role_files(agents_dir, file_system).await? {
if declared_role_files.contains(&agent_file) {
continue;
}
let parsed_file =
match read_resolved_agent_role_file(&agent_file, /*role_name_hint*/ None) {
Ok(parsed_file) => parsed_file,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
let parsed_file = match read_resolved_agent_role_file(
&agent_file,
/*role_name_hint*/ None,
file_system,
)
.await
{
Ok(parsed_file) => parsed_file,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
continue;
}
};
let role_name = parsed_file.role_name;
if roles.contains_key(&role_name) {
push_agent_role_warning(
@@ -487,36 +511,36 @@ fn discover_agent_roles_in_dir(
Ok(roles)
}
fn collect_agent_role_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
async fn collect_agent_role_files(
dir: &Path,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<Vec<PathBuf>> {
let mut pending_dirs = vec![super::absolute_path_for_filesystem_read(dir)?];
let mut files = Vec::new();
collect_agent_role_files_recursive(dir, &mut files)?;
files.sort();
Ok(files)
}
while let Some(next_dir) = pending_dirs.pop() {
let read_dir = match file_system.read_directory(&next_dir).await {
Ok(read_dir) => read_dir,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err),
};
fn collect_agent_role_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
let read_dir = match std::fs::read_dir(dir) {
Ok(read_dir) => read_dir,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err),
};
for entry in read_dir {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
collect_agent_role_files_recursive(&path, files)?;
continue;
}
if file_type.is_file()
&& path
.extension()
.is_some_and(|extension| extension == "toml")
{
files.push(path);
for entry in read_dir {
let path = next_dir.join(&entry.file_name)?;
if entry.is_directory {
pending_dirs.push(path);
continue;
}
if entry.is_file
&& path
.as_path()
.extension()
.is_some_and(|extension| extension == "toml")
{
files.push(path.into_path_buf());
}
}
}
Ok(())
files.sort();
Ok(files)
}
+51 -9
View File
@@ -33,9 +33,40 @@ use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use tempfile::TempDir;
pub(super) fn load_from_base_config_with_overrides_for_tests(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: PathBuf,
) -> std::io::Result<Config> {
// Note this ignores requirements.toml enforcement for tests.
let config_layer_stack = crate::config_loader::ConfigLayerStack::default();
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|err| std::io::Error::other(format!("failed to build tokio runtime: {err}")))?;
let file_system = codex_exec_server::Environment::default().get_filesystem();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
runtime.spawn(async move {
let result = Config::load_config_with_layer_stack(
cfg,
overrides,
codex_home,
config_layer_stack,
file_system,
)
.await;
let _ = tx.send(result);
});
rx.recv().map_err(|err| {
std::io::Error::other(format!("failed to receive config from runtime: {err}"))
})?
}
fn stdio_mcp(command: &str) -> McpServerConfig {
McpServerConfig {
transport: McpServerTransportConfig::Stdio {
@@ -2997,8 +3028,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> {
Ok(())
}
#[test]
fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> {
#[tokio::test]
async fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config_layer_stack = ConfigLayerStack::new(
Vec::new(),
@@ -3012,6 +3043,7 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
)
.map_err(std::io::Error::other)?;
let file_system = codex_exec_server::Environment::default().get_filesystem();
let config = Config::load_config_with_layer_stack(
ConfigToml::default(),
ConfigOverrides {
@@ -3020,7 +3052,9 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
},
codex_home.path().to_path_buf(),
config_layer_stack,
)?;
file_system,
)
.await?;
assert_eq!(
config.guardian_developer_instructions.as_deref(),
@@ -3030,8 +3064,9 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
Ok(())
}
#[test]
fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> std::io::Result<()> {
#[tokio::test]
async fn load_config_ignores_empty_requirements_guardian_developer_instructions()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config_layer_stack = ConfigLayerStack::new(
Vec::new(),
@@ -3043,6 +3078,7 @@ fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> s
)
.map_err(std::io::Error::other)?;
let file_system = codex_exec_server::Environment::default().get_filesystem();
let config = Config::load_config_with_layer_stack(
ConfigToml::default(),
ConfigOverrides {
@@ -3051,7 +3087,9 @@ fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> s
},
codex_home.path().to_path_buf(),
config_layer_stack,
)?;
file_system,
)
.await?;
assert_eq!(config.guardian_developer_instructions, None);
@@ -4773,8 +4811,9 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
Ok(())
}
#[test]
fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()> {
#[tokio::test]
async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()>
{
let fixture = create_test_fixture()?;
let requirements_toml = crate::config_loader::ConfigRequirementsToml {
@@ -4817,6 +4856,7 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
crate::config_loader::ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("config layer stack");
let file_system = codex_exec_server::Environment::default().get_filesystem();
let config = Config::load_config_with_layer_stack(
fixture.cfg.clone(),
ConfigOverrides {
@@ -4825,7 +4865,9 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
},
fixture.codex_home(),
config_layer_stack,
)?;
file_system,
)
.await?;
assert!(
!config
+34 -24
View File
@@ -1,9 +1,9 @@
use crate::config::types::McpServerConfig;
use crate::config::types::Notice;
use crate::path_utils::resolve_symlink_write_paths;
use crate::path_utils::write_atomically;
use anyhow::Context;
use codex_config::CONFIG_TOML_FILE;
use codex_exec_server::ExecutorFileSystem;
use codex_features::FEATURES;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ServiceTier;
@@ -11,9 +11,10 @@ use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use tokio::task;
use std::sync::Arc;
use toml_edit::ArrayOfTables;
use toml_edit::DocumentMut;
use toml_edit::Item as TomlItem;
@@ -699,11 +700,11 @@ fn normalize_skill_config_path(path: &Path) -> String {
.to_string()
}
/// Persist edits using a blocking strategy.
pub fn apply_blocking(
async fn apply_with_file_system(
codex_home: &Path,
profile: Option<&str>,
edits: &[ConfigEdit],
file_system: &dyn ExecutorFileSystem,
) -> anyhow::Result<()> {
if edits.is_empty() {
return Ok(());
@@ -712,9 +713,9 @@ pub fn apply_blocking(
let config_path = codex_home.join(CONFIG_TOML_FILE);
let write_paths = resolve_symlink_write_paths(&config_path)?;
let serialized = match write_paths.read_path {
Some(path) => match std::fs::read_to_string(&path) {
Some(path) => match super::read_text_file(path.as_path(), file_system).await {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) if err.kind() == ErrorKind::NotFound => String::new(),
Err(err) => return Err(err.into()),
},
None => String::new(),
@@ -743,27 +744,33 @@ pub fn apply_blocking(
return Ok(());
}
write_atomically(&write_paths.write_path, &document.doc.to_string()).with_context(|| {
format!(
"failed to persist config.toml at {}",
write_paths.write_path.display()
)
})?;
let write_path = super::absolute_path_for_filesystem_read(write_paths.write_path.as_path())?;
file_system
.write_file(&write_path, document.doc.to_string().into_bytes())
.await
.with_context(|| format!("failed to persist config.toml at {}", write_path.display()))?;
Ok(())
}
/// Persist edits asynchronously by offloading the blocking writer.
/// Persist edits asynchronously.
pub async fn apply(
codex_home: &Path,
profile: Option<&str>,
edits: Vec<ConfigEdit>,
file_system: Arc<dyn ExecutorFileSystem>,
) -> anyhow::Result<()> {
let codex_home = codex_home.to_path_buf();
let profile = profile.map(ToOwned::to_owned);
task::spawn_blocking(move || apply_blocking(&codex_home, profile.as_deref(), &edits))
.await
.context("config persistence task panicked")?
apply_with_file_system(codex_home, profile, &edits, &*file_system).await
}
/// Persist edits using a blocking strategy.
#[cfg(test)]
pub fn apply_blocking(
codex_home: &Path,
profile: Option<&str>,
edits: &[ConfigEdit],
) -> anyhow::Result<()> {
tests::apply_blocking_for_tests(codex_home, profile, edits)
}
/// Fluent builder to batch config edits and apply them atomically.
@@ -976,17 +983,20 @@ impl ConfigEditsBuilder {
}
/// Apply edits on a blocking thread.
#[cfg(test)]
pub fn apply_blocking(self) -> anyhow::Result<()> {
apply_blocking(&self.codex_home, self.profile.as_deref(), &self.edits)
}
/// Apply edits asynchronously via a blocking offload.
pub async fn apply(self) -> anyhow::Result<()> {
task::spawn_blocking(move || {
apply_blocking(&self.codex_home, self.profile.as_deref(), &self.edits)
})
/// Apply edits asynchronously.
pub async fn apply(self, file_system: Arc<dyn ExecutorFileSystem>) -> anyhow::Result<()> {
apply(
&self.codex_home,
self.profile.as_deref(),
self.edits,
file_system,
)
.await
.context("config persistence task panicked")?
}
}
+49
View File
@@ -7,6 +7,55 @@ use std::os::unix::fs::symlink;
use tempfile::tempdir;
use toml::Value as TomlValue;
pub(super) fn apply_blocking_for_tests(
codex_home: &Path,
profile: Option<&str>,
edits: &[ConfigEdit],
) -> anyhow::Result<()> {
if edits.is_empty() {
return Ok(());
}
let config_path = codex_home.join(CONFIG_TOML_FILE);
let write_paths = crate::path_utils::resolve_symlink_write_paths(&config_path)?;
let serialized = match write_paths.read_path {
Some(path) => match std::fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) => return Err(err.into()),
},
None => String::new(),
};
let doc = if serialized.is_empty() {
DocumentMut::new()
} else {
serialized.parse::<DocumentMut>()?
};
let profile = 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 mutated = false;
for edit in edits {
mutated |= document.apply(edit)?;
}
if !mutated {
return Ok(());
}
crate::path_utils::write_atomically(&write_paths.write_path, &document.doc.to_string())
.map_err(anyhow::Error::from)?;
Ok(())
}
#[test]
fn blocking_set_model_top_level() {
let tmp = tempdir().expect("tmpdir");
+126 -44
View File
@@ -61,6 +61,8 @@ use crate::windows_sandbox::resolve_windows_sandbox_mode;
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
@@ -96,6 +98,7 @@ use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use crate::config::permissions::compile_permission_profile;
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
@@ -642,7 +645,7 @@ impl ConfigBuilder {
self
}
pub async fn build(self) -> std::io::Result<Config> {
pub async fn build(self, file_system: Arc<dyn ExecutorFileSystem>) -> std::io::Result<Config> {
let Self {
codex_home,
cli_overrides,
@@ -652,7 +655,9 @@ impl ConfigBuilder {
fallback_cwd,
} = self;
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
if let Err(err) = maybe_migrate_smart_approvals_alias(&codex_home).await {
if let Err(err) =
maybe_migrate_smart_approvals_alias(&codex_home, file_system.clone()).await
{
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
}
let cli_overrides = cli_overrides.unwrap_or_default();
@@ -698,7 +703,9 @@ impl ConfigBuilder {
harness_overrides,
codex_home,
config_layer_stack,
file_system,
)
.await
}
}
@@ -758,13 +765,16 @@ fn push_smart_approvals_alias_migration_edits(
/// migrated feature value is `true`.
/// In all cases it removes the deprecated `smart_approvals` entry so future
/// loads only see the canonical feature flag name.
async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Result<bool> {
async fn maybe_migrate_smart_approvals_alias(
codex_home: &Path,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<bool> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
if !tokio::fs::try_exists(&config_path).await? {
return Ok(false);
}
let config_contents = tokio::fs::read_to_string(&config_path).await?;
let config_contents = match read_text_file(config_path.as_path(), &*file_system).await {
Ok(contents) => contents,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
Err(err) => return Err(err),
};
let Ok(config_toml) = toml::from_str::<ConfigToml>(&config_contents) else {
return Ok(false);
};
@@ -799,7 +809,7 @@ async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Resu
ConfigEditsBuilder::new(codex_home)
.with_edits(edits)
.apply()
.apply(file_system)
.await
.map_err(|err| {
std::io::Error::other(format!("failed to migrate guardian_approval alias: {err}"))
@@ -811,16 +821,18 @@ impl Config {
/// This is the preferred way to create an instance of [Config].
pub async fn load_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<Self> {
ConfigBuilder::default()
.cli_overrides(cli_overrides)
.build()
.build(file_system)
.await
}
/// Load a default configuration when user config files are invalid.
pub fn load_default_with_cli_overrides(
pub async fn load_default_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<Self> {
let codex_home = find_codex_home()?;
let mut merged = toml::Value::try_from(ConfigToml::default()).map_err(|e| {
@@ -837,7 +849,9 @@ impl Config {
ConfigOverrides::default(),
codex_home,
ConfigLayerStack::default(),
file_system,
)
.await
}
/// This is a secondary way of creating [Config], which is appropriate when
@@ -851,11 +865,12 @@ impl Config {
pub async fn load_with_cli_overrides_and_harness_overrides(
cli_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<Self> {
ConfigBuilder::default()
.cli_overrides(cli_overrides)
.harness_overrides(harness_overrides)
.build()
.build(file_system)
.await
}
}
@@ -867,8 +882,9 @@ pub async fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cwd: &AbsolutePathBuf,
cli_overrides: Vec<(String, TomlValue)>,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<ConfigToml> {
if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home).await {
if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home, file_system.clone()).await {
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
}
let config_layer_stack = load_config_layers_state(
@@ -901,8 +917,45 @@ pub(crate) fn deserialize_config_toml_with_base(
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
let file_contents = std::fs::read_to_string(path)?;
fn absolute_path_for_filesystem_read(path: &Path) -> std::io::Result<AbsolutePathBuf> {
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
AbsolutePathBuf::try_from(absolute_path.as_path())
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))
}
async fn read_text_file_with_executor_filesystem(
file_system: &dyn ExecutorFileSystem,
path: &Path,
) -> std::io::Result<String> {
let absolute_path = absolute_path_for_filesystem_read(path)?;
let bytes = file_system.read_file(&absolute_path).await?;
String::from_utf8(bytes).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidData,
format!(
"failed to decode UTF-8 from {}: {err}",
absolute_path.display()
),
)
})
}
async fn read_text_file(
path: &Path,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<String> {
read_text_file_with_executor_filesystem(file_system, path).await
}
async fn load_catalog_json(
path: &AbsolutePathBuf,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<ModelsResponse> {
let file_contents = read_text_file(path.as_path(), file_system).await?;
let catalog = serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidData,
@@ -924,12 +977,14 @@ fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse>
Ok(catalog)
}
fn load_model_catalog(
async fn load_model_catalog(
model_catalog_json: Option<AbsolutePathBuf>,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<Option<ModelsResponse>> {
model_catalog_json
.map(|path| load_catalog_json(&path))
.transpose()
let Some(path) = model_catalog_json else {
return Ok(None);
};
load_catalog_json(&path, file_system).await.map(Some)
}
fn filter_mcp_servers_by_requirements(
@@ -1154,20 +1209,26 @@ pub(crate) fn set_project_trust_level_inner(
/// Patch `CODEX_HOME/config.toml` project state to set trust level.
/// Use with caution.
pub fn set_project_trust_level(
pub async fn set_project_trust_level(
codex_home: &Path,
project_path: &Path,
trust_level: TrustLevel,
file_system: Arc<dyn ExecutorFileSystem>,
) -> anyhow::Result<()> {
use crate::config::edit::ConfigEditsBuilder;
ConfigEditsBuilder::new(codex_home)
.set_project_trust_level(project_path, trust_level)
.apply_blocking()
.apply(file_system)
.await
}
/// Save the default OSS provider preference to config.toml
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
pub async fn set_default_oss_provider(
codex_home: &Path,
provider: &str,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<()> {
// Validate that the provider is one of the known OSS providers
match provider {
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => {
@@ -1197,7 +1258,8 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R
ConfigEditsBuilder::new(codex_home)
.with_edits(edits)
.apply_blocking()
.apply(file_system)
.await
.map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}")))
}
@@ -2089,16 +2151,15 @@ impl Config {
overrides: ConfigOverrides,
codex_home: PathBuf,
) -> std::io::Result<Self> {
// Note this ignores requirements.toml enforcement for tests.
let config_layer_stack = ConfigLayerStack::default();
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack)
tests::load_from_base_config_with_overrides_for_tests(cfg, overrides, codex_home)
}
pub(crate) fn load_config_with_layer_stack(
pub(crate) async fn load_config_with_layer_stack(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: PathBuf,
config_layer_stack: ConfigLayerStack,
file_system: Arc<dyn ExecutorFileSystem>,
) -> std::io::Result<Self> {
validate_reserved_model_provider_ids(&cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
@@ -2115,7 +2176,7 @@ impl Config {
network: network_requirements,
} = config_layer_stack.requirements().clone();
let user_instructions = Self::load_instructions(Some(&codex_home));
let user_instructions = Self::load_instructions(Some(&codex_home), &*file_system).await;
let mut startup_warnings = Vec::new();
// Destructure ConfigOverrides fully to ensure all overrides are applied.
@@ -2243,8 +2304,10 @@ impl Config {
None => WindowsSandboxLevel::from_features(&features),
};
let memories_root = memory_root(&codex_home);
std::fs::create_dir_all(&memories_root)?;
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
file_system
.create_directory(&memories_root, CreateDirectoryOptions { recursive: true })
.await?;
if !additional_writable_roots
.iter()
.any(|existing| existing == &memories_root)
@@ -2356,8 +2419,13 @@ impl Config {
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
let agent_roles =
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
let agent_roles = agent_roles::load_agent_roles(
&cfg,
&config_layer_stack,
&mut startup_warnings,
&*file_system,
)
.await?;
let openai_base_url = cfg
.openai_base_url
@@ -2522,8 +2590,12 @@ impl Config {
.model_instructions_file
.as_ref()
.or(cfg.model_instructions_file.as_ref());
let file_base_instructions =
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
let file_base_instructions = Self::try_read_non_empty_file(
model_instructions_path,
"model instructions file",
&*file_system,
)
.await?;
let base_instructions = base_instructions.or(file_base_instructions);
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
let guardian_developer_instructions = guardian_developer_instructions_from_requirements(
@@ -2545,7 +2617,9 @@ impl Config {
let file_compact_prompt = Self::try_read_non_empty_file(
experimental_compact_prompt_path,
"experimental compact prompt file",
)?;
&*file_system,
)
.await?;
let compact_prompt = compact_prompt.or(file_compact_prompt);
let js_repl_node_path = js_repl_node_path_override
.or(config_profile.js_repl_node_path.map(Into::into))
@@ -2573,7 +2647,9 @@ impl Config {
.model_catalog_json
.clone()
.or(cfg.model_catalog_json.clone()),
)?;
&*file_system,
)
.await?;
let log_dir = cfg
.log_dir
@@ -2851,12 +2927,15 @@ impl Config {
Ok(config)
}
fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
async fn load_instructions(
codex_dir: Option<&Path>,
file_system: &dyn ExecutorFileSystem,
) -> Option<String> {
let base = codex_dir?;
for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] {
let mut path = base.to_path_buf();
path.push(candidate);
if let Ok(contents) = std::fs::read_to_string(&path) {
if let Ok(contents) = read_text_file(path.as_path(), file_system).await {
let trimmed = contents.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
@@ -2869,20 +2948,23 @@ impl Config {
/// If `path` is `Some`, attempts to read the file at the given path and
/// returns its contents as a trimmed `String`. If the file is empty, or
/// is `Some` but cannot be read, returns an `Err`.
fn try_read_non_empty_file(
async fn try_read_non_empty_file(
path: Option<&AbsolutePathBuf>,
context: &str,
file_system: &dyn ExecutorFileSystem,
) -> std::io::Result<Option<String>> {
let Some(path) = path else {
return Ok(None);
};
let contents = std::fs::read_to_string(path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read {context} {}: {e}", path.display()),
)
})?;
let contents = read_text_file(path.as_path(), file_system)
.await
.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read {context} {}: {e}", path.display()),
)
})?;
let s = contents.trim().to_string();
if s.is_empty() {
+25 -12
View File
@@ -15,7 +15,6 @@ use crate::config_loader::merge_toml_values;
use crate::path_utils;
use crate::path_utils::SymlinkWritePaths;
use crate::path_utils::resolve_symlink_write_paths;
use crate::path_utils::write_atomically;
use codex_app_server_protocol::Config as ApiConfig;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigLayerMetadata;
@@ -29,13 +28,14 @@ use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::OverriddenMetadata;
use codex_app_server_protocol::WriteStatus;
use codex_config::CONFIG_TOML_FILE;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value as JsonValue;
use std::borrow::Cow;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use thiserror::Error;
use tokio::task;
use toml::Value as TomlValue;
use toml_edit::Item as TomlItem;
@@ -114,6 +114,7 @@ pub struct ConfigService {
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
file_system: Arc<dyn ExecutorFileSystem>,
}
impl ConfigService {
@@ -122,21 +123,27 @@ impl ConfigService {
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
file_system: Arc<dyn ExecutorFileSystem>,
) -> Self {
Self {
codex_home,
cli_overrides,
loader_overrides,
cloud_requirements,
file_system,
}
}
pub fn new_with_defaults(codex_home: PathBuf) -> Self {
pub fn new_with_defaults(
codex_home: PathBuf,
file_system: Arc<dyn ExecutorFileSystem>,
) -> Self {
Self {
codex_home,
cli_overrides: Vec::new(),
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
file_system,
}
}
@@ -155,7 +162,7 @@ impl ConfigService {
.loader_overrides(self.loader_overrides.clone())
.fallback_cwd(Some(cwd.to_path_buf()))
.cloud_requirements(self.cloud_requirements.clone())
.build()
.build(self.file_system.clone())
.await
.map_err(|err| {
ConfigServiceError::io("failed to read configuration layers", err)
@@ -276,7 +283,7 @@ impl ConfigService {
.map_err(|err| ConfigServiceError::io("failed to load configuration", err))?;
let user_layer = match layers.get_user_layer() {
Some(layer) => Cow::Borrowed(layer),
None => Cow::Owned(create_empty_user_layer(&allowed_path).await?),
None => Cow::Owned(create_empty_user_layer(&allowed_path, &*self.file_system).await?),
};
if let Some(expected) = expected_version.as_deref()
@@ -381,7 +388,7 @@ impl ConfigService {
if !config_edits.is_empty() {
ConfigEditsBuilder::new(&self.codex_home)
.with_edits(config_edits)
.apply()
.apply(self.file_system.clone())
.await
.map_err(|err| ConfigServiceError::anyhow("failed to persist config.toml", err))?;
}
@@ -427,6 +434,7 @@ impl ConfigService {
async fn create_empty_user_layer(
config_toml: &AbsolutePathBuf,
file_system: &dyn ExecutorFileSystem,
) -> Result<ConfigLayerEntry, ConfigServiceError> {
let SymlinkWritePaths {
read_path,
@@ -434,12 +442,12 @@ async fn create_empty_user_layer(
} = resolve_symlink_write_paths(config_toml.as_path())
.map_err(|err| ConfigServiceError::io("failed to resolve user config path", err))?;
let toml_value = match read_path {
Some(path) => match tokio::fs::read_to_string(&path).await {
Some(path) => match super::read_text_file(path.as_path(), file_system).await {
Ok(contents) => toml::from_str(&contents).map_err(|e| {
ConfigServiceError::toml("failed to parse existing user config.toml", e)
})?,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
write_empty_user_config(write_path.clone()).await?;
write_empty_user_config(write_path.clone(), file_system).await?;
TomlValue::Table(toml::map::Map::new())
}
Err(err) => {
@@ -450,7 +458,7 @@ async fn create_empty_user_layer(
}
},
None => {
write_empty_user_config(write_path).await?;
write_empty_user_config(write_path, file_system).await?;
TomlValue::Table(toml::map::Map::new())
}
};
@@ -462,10 +470,15 @@ async fn create_empty_user_layer(
))
}
async fn write_empty_user_config(write_path: PathBuf) -> Result<(), ConfigServiceError> {
task::spawn_blocking(move || write_atomically(&write_path, ""))
async fn write_empty_user_config(
write_path: PathBuf,
file_system: &dyn ExecutorFileSystem,
) -> Result<(), ConfigServiceError> {
let absolute_path = super::absolute_path_for_filesystem_read(write_path.as_path())
.map_err(|err| ConfigServiceError::io("failed to resolve user config path", err))?;
file_system
.write_file(&absolute_path, Vec::new())
.await
.map_err(|err| ConfigServiceError::anyhow("config persistence task panicked", err.into()))?
.map_err(|err| ConfigServiceError::io("failed to create empty user config.toml", err))
}
+24 -6
View File
@@ -74,7 +74,10 @@ unified_exec = true
"#;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), original)?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -108,7 +111,10 @@ async fn write_value_supports_nested_app_paths() -> Result<()> {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -304,7 +310,10 @@ async fn version_conflict_rejected() {
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "model = \"user\"").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -327,7 +336,10 @@ async fn write_value_defaults_to_user_config_path() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
service
.write_value(ConfigValueWriteParams {
file_path: None,
@@ -391,7 +403,10 @@ async fn reserved_builtin_provider_override_rejected() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -650,7 +665,10 @@ alpha = "a"
std::fs::write(&path, base)?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::new_with_defaults(
tmp.path().to_path_buf(),
codex_exec_server::Environment::default().get_filesystem(),
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
+11 -4
View File
@@ -1002,8 +1002,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_
assert_eq!(guardian_config.model, Some("active-model".to_string()));
}
#[test]
fn guardian_review_session_config_uses_requirements_guardian_override() {
#[tokio::test]
async fn guardian_review_session_config_uses_requirements_guardian_override() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack = ConfigLayerStack::new(
@@ -1017,6 +1017,7 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
},
)
.expect("config layer stack");
let file_system = codex_exec_server::Environment::default().get_filesystem();
let parent_config = Config::load_config_with_layer_stack(
ConfigToml::default(),
ConfigOverrides {
@@ -1025,7 +1026,9 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
},
codex_home.path().to_path_buf(),
config_layer_stack,
file_system,
)
.await
.expect("load config");
let guardian_config =
@@ -1038,13 +1041,15 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
);
}
#[test]
fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() {
#[tokio::test]
async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override()
{
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack =
ConfigLayerStack::new(Vec::new(), Default::default(), Default::default())
.expect("config layer stack");
let file_system = codex_exec_server::Environment::default().get_filesystem();
let parent_config = Config::load_config_with_layer_stack(
ConfigToml::default(),
ConfigOverrides {
@@ -1053,7 +1058,9 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
},
codex_home.path().to_path_buf(),
config_layer_stack,
file_system,
)
.await
.expect("load config");
let guardian_config =
+1 -1
View File
@@ -213,7 +213,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
if let Err(err) = ConfigEditsBuilder::new(&codex_home)
.replace_mcp_servers(&servers)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
warn!("failed to persist MCP dependencies for mentioned skills: {err}");
+1 -1
View File
@@ -1295,7 +1295,7 @@ async fn persist_codex_app_tool_approval(
],
value: value("approve"),
}])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
}
+1 -1
View File
@@ -53,7 +53,7 @@ pub async fn maybe_migrate_personality(
ConfigEditsBuilder::new(codex_home)
.set_personality(Some(Personality::Pragmatic))
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.map_err(|err| {
io::Error::other(format!("failed to persist personality migration: {err}"))
+18 -15
View File
@@ -689,19 +689,22 @@ impl PluginsManager {
.await
.map_err(PluginInstallError::join)??;
ConfigService::new_with_defaults(self.codex_home.clone())
.write_value(ConfigValueWriteParams {
key_path: format!("plugins.{}", result.plugin_id.as_key()),
value: json!({
"enabled": true,
}),
merge_strategy: MergeStrategy::Replace,
file_path: None,
expected_version: None,
})
.await
.map(|_| ())
.map_err(PluginInstallError::from)?;
ConfigService::new_with_defaults(
self.codex_home.clone(),
codex_exec_server::Environment::default().get_filesystem(),
)
.write_value(ConfigValueWriteParams {
key_path: format!("plugins.{}", result.plugin_id.as_key()),
value: json!({
"enabled": true,
}),
merge_strategy: MergeStrategy::Replace,
file_path: None,
expected_version: None,
})
.await
.map(|_| ())
.map_err(PluginInstallError::from)?;
let analytics_events_client = match self.analytics_events_client.read() {
Ok(client) => client.clone(),
@@ -759,7 +762,7 @@ impl PluginsManager {
.with_edits([ConfigEdit::ClearPath {
segments: vec!["plugins".to_string(), plugin_id.as_key()],
}])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await?;
let analytics_events_client = match self.analytics_events_client.read() {
@@ -958,7 +961,7 @@ impl PluginsManager {
} else {
ConfigEditsBuilder::new(&self.codex_home)
.with_edits(config_edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
};
self.clear_cache();
+1 -1
View File
@@ -350,7 +350,7 @@ async fn run_windows_sandbox_setup_and_persist(
.with_profile(active_profile.as_deref())
.set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode))
.clear_legacy_windows_sandbox_keys()
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.map_err(|err| anyhow::anyhow!("failed to persist windows sandbox mode: {err}"))
}
+1 -1
View File
@@ -155,7 +155,7 @@ pub async fn load_default_config_for_test(codex_home: &TempDir) -> Config {
ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(default_test_overrides())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("defaults for test should always succeed")
}
+1
View File
@@ -23,6 +23,7 @@ codex-app-server-client = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-feedback = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
+6 -4
View File
@@ -266,6 +266,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
&codex_home,
&config_cwd,
cli_kv_overrides.clone(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
@@ -365,7 +366,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.cloud_requirements(cloud_requirements)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await?;
#[allow(clippy::print_stderr)]
@@ -1632,7 +1633,8 @@ mod tests {
use tracing_opentelemetry::OpenTelemetrySpanExt;
fn test_tracing_subscriber() -> impl tracing::Subscriber + Send + Sync {
let provider = SdkTracerProvider::builder().build();
let provider = SdkTracerProvider::builder()
.build(codex_exec_server::Environment::default().get_filesystem());
let tracer = provider.tracer("codex-exec-tests");
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
}
@@ -1853,7 +1855,7 @@ mod tests {
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("build default config");
@@ -1877,7 +1879,7 @@ mod tests {
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("build auto-review config");
+1
View File
@@ -19,6 +19,7 @@ workspace = true
anyhow = { workspace = true }
codex-arg0 = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
+6 -2
View File
@@ -190,8 +190,12 @@ impl CodexToolCallParam {
.map(|(k, v)| (k, json_to_toml(v)))
.collect();
let cfg =
Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?;
let cfg = Config::load_with_cli_overrides_and_harness_overrides(
cli_overrides,
overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await?;
Ok((prompt, cfg))
}
+8 -5
View File
@@ -63,11 +63,14 @@ pub async fn run_main(
format!("error parsing -c overrides: {e}"),
)
})?;
let config = Config::load_with_cli_overrides(cli_kv_overrides)
.await
.map_err(|e| {
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
let config = Config::load_with_cli_overrides(
cli_kv_overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.map_err(|e| {
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
let otel = codex_core::otel_init::build_provider(
&config,
+1
View File
@@ -37,6 +37,7 @@ codex-chatgpt = { workspace = true }
codex-client = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-feedback = { workspace = true }
codex-file-search = { workspace = true }
+30 -21
View File
@@ -698,7 +698,7 @@ async fn prepare_startup_tooltip_override(
if let Err(err) = ConfigEditsBuilder::new(&config.codex_home)
.set_model_availability_nux_count(&updated_shown_count)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -951,7 +951,7 @@ impl App {
.codex_home(self.config.codex_home.clone())
.cli_overrides(self.cli_kv_overrides.clone())
.harness_overrides(overrides)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
}
@@ -1206,7 +1206,10 @@ impl App {
// Persist first so the live session does not diverge from disk if the
// config edit fails. Runtime/UI state is patched below only after the
// durable config update succeeds.
if let Err(err) = builder.apply().await {
if let Err(err) = builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(error = %err, "failed to persist feature flags");
self.chat_widget
.add_error_message(format!("Failed to update experimental features: {err}"));
@@ -3270,7 +3273,10 @@ impl App {
"unelevated"
})
.clear_legacy_windows_sandbox_keys();
match builder.apply().await {
match builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
if elevated_enabled {
self.config.set_windows_sandbox_enabled(/*value*/ false);
@@ -3364,7 +3370,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_model(Some(model.as_str()), effort)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -3405,7 +3411,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_personality(Some(personality))
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -3441,7 +3447,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_service_tier(service_tier)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -3480,7 +3486,10 @@ impl App {
}
};
match builder.apply().await {
match builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
match kind {
RealtimeAudioDeviceKind::Microphone => {
@@ -3611,7 +3620,7 @@ impl App {
segments,
value: policy.to_string().into(),
}])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3646,7 +3655,7 @@ impl App {
AppEvent::PersistFullAccessWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_full_access_warning(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3661,7 +3670,7 @@ impl App {
AppEvent::PersistWorldWritableWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_world_writable_warning(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3676,7 +3685,7 @@ impl App {
AppEvent::PersistRateLimitSwitchPromptHidden => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_rate_limit_model_nudge(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3709,7 +3718,7 @@ impl App {
};
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3733,7 +3742,7 @@ impl App {
} => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.record_model_migration_seen(from_model.as_str(), to_model.as_str())
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -3767,7 +3776,7 @@ impl App {
}];
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits(edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -3819,7 +3828,7 @@ impl App {
};
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits(edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -3939,7 +3948,7 @@ impl App {
let edit = codex_core::config::edit::status_line_items_edit(&ids);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match apply_result {
Ok(()) => {
@@ -3965,7 +3974,7 @@ impl App {
let edit = codex_core::config::edit::terminal_title_items_edit(&ids);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match apply_result {
Ok(()) => {
@@ -3991,7 +4000,7 @@ impl App {
let edit = codex_core::config::edit::syntax_theme_edit(&name);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match apply_result {
Ok(()) => {
@@ -7145,7 +7154,7 @@ guardian_approval = true
let codex_home = tempdir().expect("temp codex home");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("config");
@@ -7243,7 +7252,7 @@ guardian_approval = true
value: "user".into(),
},
])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("persist app toggle");
+8 -1
View File
@@ -41,5 +41,12 @@ where
}
pub async fn should_use_app_server_tui(cli: &Cli) -> std::io::Result<bool> {
should_use_app_server_tui_with(cli, Config::load_with_cli_overrides_and_harness_overrides).await
should_use_app_server_tui_with(cli, |cli_kv_overrides, config_overrides| {
Config::load_with_cli_overrides_and_harness_overrides(
cli_kv_overrides,
config_overrides,
codex_exec_server::Environment::default().get_filesystem(),
)
})
.await
}
+5 -4
View File
@@ -328,6 +328,7 @@ pub async fn run_main(
&codex_home,
&config_cwd,
cli_kv_overrides.clone(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
@@ -1209,7 +1210,7 @@ async fn load_config_or_exit_with_fallback_cwd(
.harness_overrides(overrides)
.cloud_requirements(cloud_requirements)
.fallback_cwd(fallback_cwd)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => config,
@@ -1268,7 +1269,7 @@ mod tests {
async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
}
@@ -1462,7 +1463,7 @@ trust_level = "untrusted"
let trusted_config = ConfigBuilder::default()
.codex_home(codex_home.clone())
.harness_overrides(trusted_overrides.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await?;
assert_eq!(
trusted_config.permissions.approval_policy.value(),
@@ -1476,7 +1477,7 @@ trust_level = "untrusted"
let untrusted_config = ConfigBuilder::default()
.codex_home(codex_home)
.harness_overrides(untrusted_overrides)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await?;
assert_eq!(
untrusted_config.permissions.approval_policy.value(),
+28 -3
View File
@@ -144,9 +144,34 @@ impl TrustDirectoryWidget {
fn handle_trust(&mut self) {
let target =
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
tracing::error!("Failed to set project trusted: {e:?}");
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
let codex_home = self.codex_home.clone();
let target_for_task = target.clone();
let target_display = target.display().to_string();
let handle = match tokio::runtime::Handle::try_current() {
Ok(handle) => handle,
Err(err) => {
self.error = Some(format!("Failed to set trust for {target_display}: {err}"));
return;
}
};
handle.spawn(async move {
if let Err(err) = set_project_trust_level(
&codex_home,
&target_for_task,
TrustLevel::Trusted,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
tracing::error!(
target = %target_for_task.display(),
"Failed to set project trusted: {err:?}"
);
}
});
if self.error.is_some() {
self.error = None;
}
self.selection = Some(TrustDirectorySelection::Trust);
+6 -1
View File
@@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result<Str
// If the user manually selected an OSS provider, we save it as the
// default one to use later.
if let Ok(ref provider) = result
&& let Err(e) = set_default_oss_provider(codex_home, provider)
&& let Err(e) = set_default_oss_provider(
codex_home,
provider,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
tracing::warn!("Failed to save OSS provider preference: {e}");
}
+6 -3
View File
@@ -935,9 +935,12 @@ async fn resolve_auth() -> Result<TranscriptionAuthContext, String> {
let token = auth
.get_token()
.map_err(|e| format!("failed to get auth token: {e}"))?;
let config = Config::load_with_cli_overrides(Vec::new())
.await
.map_err(|e| format!("failed to load config: {e}"))?;
let config = Config::load_with_cli_overrides(
Vec::new(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.map_err(|e| format!("failed to load config: {e}"))?;
Ok(TranscriptionAuthContext {
mode: auth.api_auth_mode(),
bearer_token: token,
+1
View File
@@ -41,6 +41,7 @@ codex-chatgpt = { workspace = true }
codex-client = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-feedback = { workspace = true }
codex-file-search = { workspace = true }
+29 -20
View File
@@ -799,7 +799,7 @@ async fn prepare_startup_tooltip_override(
if let Err(err) = ConfigEditsBuilder::new(&config.codex_home)
.set_model_availability_nux_count(&updated_shown_count)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -1043,7 +1043,7 @@ impl App {
.codex_home(self.config.codex_home.clone())
.cli_overrides(self.cli_kv_overrides.clone())
.harness_overrides(overrides)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
}
@@ -1298,7 +1298,10 @@ impl App {
// Persist first so the live session does not diverge from disk if the
// config edit fails. Runtime/UI state is patched below only after the
// durable config update succeeds.
if let Err(err) = builder.apply().await {
if let Err(err) = builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(error = %err, "failed to persist feature flags");
self.chat_widget
.add_error_message(format!("Failed to update experimental features: {err}"));
@@ -4002,7 +4005,10 @@ impl App {
"unelevated"
})
.clear_legacy_windows_sandbox_keys();
match builder.apply().await {
match builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
if elevated_enabled {
self.config.set_windows_sandbox_enabled(/*value*/ false);
@@ -4100,7 +4106,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_model(Some(model.as_str()), effort)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -4141,7 +4147,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_personality(Some(personality))
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -4177,7 +4183,7 @@ impl App {
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_service_tier(service_tier)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -4216,7 +4222,10 @@ impl App {
}
};
match builder.apply().await {
match builder
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
match kind {
RealtimeAudioDeviceKind::Microphone => {
@@ -4347,7 +4356,7 @@ impl App {
segments,
value: policy.to_string().into(),
}])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4382,7 +4391,7 @@ impl App {
AppEvent::PersistFullAccessWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_full_access_warning(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4397,7 +4406,7 @@ impl App {
AppEvent::PersistWorldWritableWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_world_writable_warning(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4412,7 +4421,7 @@ impl App {
AppEvent::PersistRateLimitSwitchPromptHidden => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.set_hide_rate_limit_model_nudge(/*acknowledged*/ true)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4445,7 +4454,7 @@ impl App {
};
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4469,7 +4478,7 @@ impl App {
} => {
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.record_model_migration_seen(from_model.as_str(), to_model.as_str())
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
tracing::error!(
@@ -4503,7 +4512,7 @@ impl App {
}];
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits(edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -4555,7 +4564,7 @@ impl App {
};
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits(edits)
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(()) => {
@@ -4675,7 +4684,7 @@ impl App {
let edit = codex_core::config::edit::status_line_items_edit(&ids);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match apply_result {
Ok(()) => {
@@ -4700,7 +4709,7 @@ impl App {
let edit = codex_core::config::edit::syntax_theme_edit(&name);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await;
match apply_result {
Ok(()) => {
@@ -8421,7 +8430,7 @@ guardian_approval = true
let codex_home = tempdir().expect("temp codex home");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("config");
@@ -8519,7 +8528,7 @@ guardian_approval = true
value: "user".into(),
},
])
.apply()
.apply(codex_exec_server::Environment::default().get_filesystem())
.await
.expect("persist app toggle");
+5 -4
View File
@@ -654,6 +654,7 @@ pub async fn run_main(
&codex_home,
&config_cwd,
cli_kv_overrides.clone(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
@@ -1546,7 +1547,7 @@ async fn load_config_or_exit_with_fallback_cwd(
.harness_overrides(overrides)
.cloud_requirements(cloud_requirements)
.fallback_cwd(fallback_cwd)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
{
Ok(config) => config,
@@ -1608,7 +1609,7 @@ mod tests {
async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await
}
@@ -1965,7 +1966,7 @@ trust_level = "untrusted"
let trusted_config = ConfigBuilder::default()
.codex_home(codex_home.clone())
.harness_overrides(trusted_overrides.clone())
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await?;
assert_eq!(
trusted_config.permissions.approval_policy.value(),
@@ -1979,7 +1980,7 @@ trust_level = "untrusted"
let untrusted_config = ConfigBuilder::default()
.codex_home(codex_home)
.harness_overrides(untrusted_overrides)
.build()
.build(codex_exec_server::Environment::default().get_filesystem())
.await?;
assert_eq!(
untrusted_config.permissions.approval_policy.value(),
@@ -144,9 +144,34 @@ impl TrustDirectoryWidget {
fn handle_trust(&mut self) {
let target =
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
tracing::error!("Failed to set project trusted: {e:?}");
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
let codex_home = self.codex_home.clone();
let target_for_task = target.clone();
let target_display = target.display().to_string();
let handle = match tokio::runtime::Handle::try_current() {
Ok(handle) => handle,
Err(err) => {
self.error = Some(format!("Failed to set trust for {target_display}: {err}"));
return;
}
};
handle.spawn(async move {
if let Err(err) = set_project_trust_level(
&codex_home,
&target_for_task,
TrustLevel::Trusted,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
tracing::error!(
target = %target_for_task.display(),
"Failed to set project trusted: {err:?}"
);
}
});
if self.error.is_some() {
self.error = None;
}
self.selection = Some(TrustDirectorySelection::Trust);
+6 -1
View File
@@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result<Str
// If the user manually selected an OSS provider, we save it as the
// default one to use later.
if let Ok(ref provider) = result
&& let Err(e) = set_default_oss_provider(codex_home, provider)
&& let Err(e) = set_default_oss_provider(
codex_home,
provider,
codex_exec_server::Environment::default().get_filesystem(),
)
.await
{
tracing::warn!("Failed to save OSS provider preference: {e}");
}
+6 -3
View File
@@ -773,9 +773,12 @@ async fn resolve_auth() -> Result<TranscriptionAuthContext, String> {
let token = auth
.get_token()
.map_err(|e| format!("failed to get auth token: {e}"))?;
let config = Config::load_with_cli_overrides(Vec::new())
.await
.map_err(|e| format!("failed to load config: {e}"))?;
let config = Config::load_with_cli_overrides(
Vec::new(),
codex_exec_server::Environment::default().get_filesystem(),
)
.await
.map_err(|e| format!("failed to load config: {e}"))?;
Ok(TranscriptionAuthContext {
mode: auth.api_auth_mode(),
bearer_token: token,