diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1de6e065e7..72f3cb85ac 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1,4 +1,5 @@ use crate::bespoke_event_handling::apply_bespoke_event_handling; +use crate::config_api::ConfigApi; use crate::error_code::INTERNAL_ERROR_CODE; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::fuzzy_file_search::run_fuzzy_file_search; @@ -215,6 +216,7 @@ pub(crate) struct CodexMessageProcessor { outgoing: Arc, codex_linux_sandbox_exe: Option, config: Arc, + config_api: ConfigApi, cli_overrides: Vec<(String, TomlValue)>, conversation_listeners: HashMap>, active_login: Arc>>, @@ -265,12 +267,14 @@ impl CodexMessageProcessor { cli_overrides: Vec<(String, TomlValue)>, feedback: CodexFeedback, ) -> Self { + let config_api = ConfigApi::new(config.codex_home.clone(), cli_overrides.clone()); Self { auth_manager, conversation_manager, outgoing, codex_linux_sandbox_exe, config, + config_api, cli_overrides, conversation_listeners: HashMap::new(), active_login: Arc::new(Mutex::new(None)), @@ -282,13 +286,7 @@ impl CodexMessageProcessor { } async fn load_latest_config(&self) -> Result { - Config::load_with_cli_overrides(self.cli_overrides.clone()) - .await - .map_err(|err| JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to reload config: {err}"), - data: None, - }) + self.config_api.load_latest_thread_agnostic_config().await } fn review_request_from_target( diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 98e0f108e9..8161dbbe3c 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -7,6 +7,8 @@ use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::JSONRPCErrorError; +use codex_core::config::Config; +use codex_core::config::ConfigBuilder; use codex_core::config::ConfigService; use codex_core::config::ConfigServiceError; use serde_json::json; @@ -15,13 +17,17 @@ use toml::Value as TomlValue; #[derive(Clone)] pub(crate) struct ConfigApi { + codex_home: PathBuf, + cli_overrides: Vec<(String, TomlValue)>, service: ConfigService, } impl ConfigApi { pub(crate) fn new(codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>) -> Self { Self { - service: ConfigService::new(codex_home, cli_overrides), + service: ConfigService::new(codex_home.clone(), cli_overrides.clone()), + codex_home, + cli_overrides, } } @@ -45,6 +51,22 @@ impl ConfigApi { ) -> Result { self.service.batch_write(params).await.map_err(map_error) } + + pub(crate) async fn load_latest_thread_agnostic_config( + &self, + ) -> Result { + ConfigBuilder::default() + .codex_home(self.codex_home.clone()) + .cli_overrides(self.cli_overrides.clone()) + .thread_agnostic() + .build() + .await + .map_err(|err| JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to reload config: {err}"), + data: None, + }) + } } fn map_error(err: ConfigServiceError) -> JSONRPCErrorError { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8eac13fd2e..30e3eef40e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -363,6 +363,7 @@ pub struct ConfigBuilder { cli_overrides: Option>, harness_overrides: Option, loader_overrides: Option, + thread_agnostic: bool, } impl ConfigBuilder { @@ -371,6 +372,13 @@ impl ConfigBuilder { self } + /// Load a "thread-agnostic" config stack, which intentionally ignores any + /// in-repo `.codex/` config layers (because there is no cwd/project context). + pub fn thread_agnostic(mut self) -> Self { + self.thread_agnostic = true; + self + } + pub fn cli_overrides(mut self, cli_overrides: Vec<(String, TomlValue)>) -> Self { self.cli_overrides = Some(cli_overrides); self @@ -392,18 +400,22 @@ impl ConfigBuilder { cli_overrides, harness_overrides, loader_overrides, + thread_agnostic, } = self; let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?; let cli_overrides = cli_overrides.unwrap_or_default(); let harness_overrides = harness_overrides.unwrap_or_default(); let loader_overrides = loader_overrides.unwrap_or_default(); - let cwd = match harness_overrides.cwd.as_deref() { - Some(path) => AbsolutePathBuf::try_from(path)?, - None => AbsolutePathBuf::current_dir()?, + let cwd = if thread_agnostic { + None + } else { + Some(match harness_overrides.cwd.as_deref() { + Some(path) => AbsolutePathBuf::try_from(path)?, + None => AbsolutePathBuf::current_dir()?, + }) }; let config_layer_stack = - load_config_layers_state(&codex_home, Some(cwd), &cli_overrides, loader_overrides) - .await?; + load_config_layers_state(&codex_home, cwd, &cli_overrides, loader_overrides).await?; let merged_toml = config_layer_stack.effective_config(); // Note that each layer in ConfigLayerStack should have resolved @@ -2082,6 +2094,43 @@ trust_level = "trusted" Ok(()) } + #[tokio::test] + async fn config_builder_thread_agnostic_ignores_project_layers() -> anyhow::Result<()> { + let tmp = TempDir::new()?; + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir_all(&codex_home)?; + std::fs::write(codex_home.join(CONFIG_TOML_FILE), "model = \"from-user\"\n")?; + + let project = tmp.path().join("project"); + std::fs::create_dir_all(project.join(".codex"))?; + std::fs::write( + project.join(".codex").join(CONFIG_TOML_FILE), + "model = \"from-project\"\n", + )?; + + let harness_overrides = ConfigOverrides { + cwd: Some(project), + ..Default::default() + }; + + let with_project_layers = ConfigBuilder::default() + .codex_home(codex_home.clone()) + .harness_overrides(harness_overrides.clone()) + .build() + .await?; + assert_eq!(with_project_layers.model.as_deref(), Some("from-project")); + + let thread_agnostic = ConfigBuilder::default() + .codex_home(codex_home) + .harness_overrides(harness_overrides) + .thread_agnostic() + .build() + .await?; + assert_eq!(thread_agnostic.model.as_deref(), Some("from-user")); + + Ok(()) + } + #[tokio::test] async fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()> { let codex_home = TempDir::new()?;