diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b3f8d88027..a394c2440d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -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", diff --git a/codex-rs/app-server-client/Cargo.toml b/codex-rs/app-server-client/Cargo.toml index 5a3a1aa73f..fdf186dc02 100644 --- a/codex-rs/app-server-client/Cargo.toml +++ b/codex-rs/app-server-client/Cargo.toml @@ -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"] } diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index acf9c77a10..e9dd302571 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -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"), } } diff --git a/codex-rs/app-server-test-client/Cargo.toml b/codex-rs/app-server-test-client/Cargo.toml index 32d2588fdf..e26ab5d8c9 100644 --- a/codex-rs/app-server-test-client/Cargo.toml +++ b/codex-rs/app-server-test-client/Cargo.toml @@ -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 } diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index a479f96529..43f94b7a2a 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -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"), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1b02e4bb6c..9199aea693 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -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 } diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index bc82e9152e..424a457da7 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -93,6 +93,7 @@ impl ConfigApi { self.cli_overrides.clone(), self.loader_overrides.clone(), cloud_requirements, + codex_exec_server::Environment::default().get_filesystem(), ) } diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 4288d15393..9655c4690a 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -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"), } } diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 8b4afc23d0..ebd8f8d312 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -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}"), + ) + })? } }; diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 17a6f97ad3..658a7e791f 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -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"] } diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index 6497777a9f..a21bd30440 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -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?; diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index c2fd1300c6..28392f7187 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -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 } diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index c65b6dcad5..a0bb9549dd 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -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 { diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index d0cc1a3a1d..0ca7803c67 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -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}"); diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 8446e457b2..7d44983248 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -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; diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 30a911cb63..270992f194 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -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); diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml index a80c455de4..9bdd98b00c 100644 --- a/codex-rs/cloud-tasks/Cargo.toml +++ b/codex-rs/cloud-tasks/Cargo.toml @@ -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 } diff --git a/codex-rs/cloud-tasks/src/util.rs b/codex-rs/cloud-tasks/src/util.rs index 0dcf18432a..4db878c8c6 100644 --- a/codex-rs/cloud-tasks/src/util.rs +++ b/codex-rs/cloud-tasks/src/util.rs @@ -61,7 +61,12 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option { pub async fn load_auth_manager() -> Option { // 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, diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 06c6eae1e6..c01050a404 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -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(); } diff --git a/codex-rs/core/src/config/agent_roles.rs b/codex-rs/core/src/config/agent_roles.rs index c527435e92..18be389780 100644 --- a/codex-rs/core/src/config/agent_roles.rs +++ b/codex-rs/core/src/config/agent_roles.rs @@ -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, + file_system: &dyn ExecutorFileSystem, ) -> std::io::Result> { 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 = 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, 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> { 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 { 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 { - 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, startup_warnings: &mut Vec, + file_system: &dyn ExecutorFileSystem, ) -> std::io::Result> { 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> { +async fn collect_agent_role_files( + dir: &Path, + file_system: &dyn ExecutorFileSystem, +) -> std::io::Result> { + 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) -> 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) } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1c78759ec4..5f411cc1a2 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -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 { + // 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 diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 2865ace4b2..790b51b81e 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -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, + file_system: Arc, ) -> 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) -> anyhow::Result<()> { + apply( + &self.codex_home, + self.profile.as_deref(), + self.edits, + file_system, + ) .await - .context("config persistence task panicked")? } } diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index 5a31d84dd0..62e23771a1 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -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::()? + }; + + 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"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 17abe55c46..6c5b2d8c9a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -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 { + pub async fn build(self, file_system: Arc) -> std::io::Result { 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 { +async fn maybe_migrate_smart_approvals_alias( + codex_home: &Path, + file_system: Arc, +) -> std::io::Result { 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::(&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, ) -> std::io::Result { 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, ) -> std::io::Result { 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, ) -> std::io::Result { 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, ) -> std::io::Result { - 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 { - let file_contents = std::fs::read_to_string(path)?; +fn absolute_path_for_filesystem_read(path: &Path) -> std::io::Result { + 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 { + 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 { + read_text_file_with_executor_filesystem(file_system, path).await +} + +async fn load_catalog_json( + path: &AbsolutePathBuf, + file_system: &dyn ExecutorFileSystem, +) -> std::io::Result { + let file_contents = read_text_file(path.as_path(), file_system).await?; let catalog = serde_json::from_str::(&file_contents).map_err(|err| { std::io::Error::new( ErrorKind::InvalidData, @@ -924,12 +977,14 @@ fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result Ok(catalog) } -fn load_model_catalog( +async fn load_model_catalog( model_catalog_json: Option, + file_system: &dyn ExecutorFileSystem, ) -> std::io::Result> { - 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, ) -> 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, +) -> 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 { - // 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, ) -> std::io::Result { 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 { + async fn load_instructions( + codex_dir: Option<&Path>, + file_system: &dyn ExecutorFileSystem, + ) -> Option { 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> { 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() { diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index 279af666c1..40f24a458a 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -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, } impl ConfigService { @@ -122,21 +123,27 @@ impl ConfigService { cli_overrides: Vec<(String, TomlValue)>, loader_overrides: LoaderOverrides, cloud_requirements: CloudRequirementsLoader, + file_system: Arc, ) -> 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, + ) -> 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 { 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)) } diff --git a/codex-rs/core/src/config/service_tests.rs b/codex-rs/core/src/config/service_tests.rs index bc3006a9a9..56d8026054 100644 --- a/codex-rs/core/src/config/service_tests.rs +++ b/codex-rs/core/src/config/service_tests.rs @@ -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()), diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index e1595ea167..41dac8c180 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -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 = diff --git a/codex-rs/core/src/mcp/skill_dependencies.rs b/codex-rs/core/src/mcp/skill_dependencies.rs index dc2dc360e2..f134320412 100644 --- a/codex-rs/core/src/mcp/skill_dependencies.rs +++ b/codex-rs/core/src/mcp/skill_dependencies.rs @@ -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}"); diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index f82cc73bb5..568aa575e3 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -1295,7 +1295,7 @@ async fn persist_codex_app_tool_approval( ], value: value("approve"), }]) - .apply() + .apply(codex_exec_server::Environment::default().get_filesystem()) .await } diff --git a/codex-rs/core/src/personality_migration.rs b/codex-rs/core/src/personality_migration.rs index f535465209..010e1d9a39 100644 --- a/codex-rs/core/src/personality_migration.rs +++ b/codex-rs/core/src/personality_migration.rs @@ -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}")) diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index f02758d273..3bc874636f 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -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(); diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 1fdf3ee338..bddaba2924 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -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}")) } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 0b31793402..140b5b5954 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -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") } diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index 3785235606..1c43dd5c0d 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -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 } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f648a63952..41be71b883 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -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"); diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 4c05f27c10..a7d38a0fed 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -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 } diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index f3f66b5559..c4e9cd0894 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -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)) } diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index 6e4be0a91c..01c873b3d5 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -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, diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 8013b1325e..2465158fcb 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -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 } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4aa51df21c..c5146f4286 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -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"); diff --git a/codex-rs/tui/src/app_server_tui_dispatch.rs b/codex-rs/tui/src/app_server_tui_dispatch.rs index 63c5dc8dd0..15d9c4dcb4 100644 --- a/codex-rs/tui/src/app_server_tui_dispatch.rs +++ b/codex-rs/tui/src/app_server_tui_dispatch.rs @@ -41,5 +41,12 @@ where } pub async fn should_use_app_server_tui(cli: &Cli) -> std::io::Result { - 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 } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5416200691..df66144ebe 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -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 { 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(), diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index 06c73cc2db..f0843eafc0 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -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); diff --git a/codex-rs/tui/src/oss_selection.rs b/codex-rs/tui/src/oss_selection.rs index c73b4e332a..e02abdd2a2 100644 --- a/codex-rs/tui/src/oss_selection.rs +++ b/codex-rs/tui/src/oss_selection.rs @@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result Result { 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, diff --git a/codex-rs/tui_app_server/Cargo.toml b/codex-rs/tui_app_server/Cargo.toml index 8866042051..f4833883e2 100644 --- a/codex-rs/tui_app_server/Cargo.toml +++ b/codex-rs/tui_app_server/Cargo.toml @@ -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 } diff --git a/codex-rs/tui_app_server/src/app.rs b/codex-rs/tui_app_server/src/app.rs index 171df69270..eb9e77663d 100644 --- a/codex-rs/tui_app_server/src/app.rs +++ b/codex-rs/tui_app_server/src/app.rs @@ -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"); diff --git a/codex-rs/tui_app_server/src/lib.rs b/codex-rs/tui_app_server/src/lib.rs index 17e309d5fb..b126b03f24 100644 --- a/codex-rs/tui_app_server/src/lib.rs +++ b/codex-rs/tui_app_server/src/lib.rs @@ -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 { 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(), diff --git a/codex-rs/tui_app_server/src/onboarding/trust_directory.rs b/codex-rs/tui_app_server/src/onboarding/trust_directory.rs index 06c73cc2db..f0843eafc0 100644 --- a/codex-rs/tui_app_server/src/onboarding/trust_directory.rs +++ b/codex-rs/tui_app_server/src/onboarding/trust_directory.rs @@ -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); diff --git a/codex-rs/tui_app_server/src/oss_selection.rs b/codex-rs/tui_app_server/src/oss_selection.rs index c73b4e332a..e02abdd2a2 100644 --- a/codex-rs/tui_app_server/src/oss_selection.rs +++ b/codex-rs/tui_app_server/src/oss_selection.rs @@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result Result { 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,