diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 12581d33e..a145dcf7e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1154,6 +1154,7 @@ dependencies = [ "codex-stdio-to-uds", "codex-tui", "codex-tui2", + "codex-utils-absolute-path", "codex-windows-sandbox", "ctor 0.5.0", "libc", diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 84e6e9aca..3cbdd97f5 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -37,13 +37,13 @@ codex-rmcp-client = { workspace = true } codex-stdio-to-uds = { workspace = true } codex-tui = { workspace = true } codex-tui2 = { workspace = true } +codex-utils-absolute-path = { workspace = true } ctor = { workspace = true } libc = { workspace = true } owo-colors = { workspace = true } -regex-lite = { workspace = true} +regex-lite = { workspace = true } serde_json = { workspace = true } supports-color = { workspace = true } -toml = { workspace = true } tokio = { workspace = true, features = [ "io-std", "macros", @@ -51,6 +51,7 @@ tokio = { workspace = true, features = [ "rt-multi-thread", "signal", ] } +toml = { workspace = true } tracing = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 80db64767..ae6dabe67 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -44,6 +44,7 @@ use codex_core::features::Feature; use codex_core::features::FeatureOverrides; use codex_core::features::Features; use codex_core::features::is_known_feature_key; +use codex_utils_absolute_path::AbsolutePathBuf; /// Codex CLI /// @@ -687,7 +688,13 @@ async fn is_tui2_enabled(cli: &TuiCli) -> std::io::Result { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; let codex_home = find_codex_home()?; - let config_toml = load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides).await?; + let cwd = cli.cwd.clone(); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::from_absolute_path(path)?, + None => AbsolutePathBuf::current_dir()?, + }; + let config_toml = + load_config_as_toml_with_cli_overrides(&codex_home, &config_cwd, cli_kv_overrides).await?; let config_profile = config_toml.get_config_profile(cli.config_profile.clone())?; let overrides = FeatureOverrides::default(); let features = Features::from_config(&config_toml, &config_profile, overrides); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 986e9eb91..da94f76cb 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -346,8 +346,13 @@ impl ConfigBuilder { 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 config_layer_stack = - load_config_layers_state(&codex_home, &cli_overrides, loader_overrides).await?; + load_config_layers_state(&codex_home, Some(cwd), &cli_overrides, loader_overrides) + .await?; let merged_toml = config_layer_stack.effective_config(); // Note that each layer in ConfigLayerStack should have resolved @@ -401,10 +406,16 @@ impl Config { /// applied yet, which risks failing to enforce required constraints. pub async fn load_config_as_toml_with_cli_overrides( codex_home: &Path, + cwd: &AbsolutePathBuf, cli_overrides: Vec<(String, TomlValue)>, ) -> std::io::Result { - let config_layer_stack = - load_config_layers_state(codex_home, &cli_overrides, LoaderOverrides::default()).await?; + let config_layer_stack = load_config_layers_state( + codex_home, + Some(cwd.clone()), + &cli_overrides, + LoaderOverrides::default(), + ) + .await?; let merged_toml = config_layer_stack.effective_config(); let cfg = deserialize_config_toml_with_base(merged_toml, codex_home).map_err(|e| { @@ -438,8 +449,12 @@ pub async fn load_global_mcp_servers( // config layers for deprecated fields rather than reporting on the merged // result. let cli_overrides = Vec::<(String, TomlValue)>::new(); + // There is no cwd/project context for this query, so this will not include + // MCP servers defined in in-repo .codex/ folders. + let cwd: Option = None; let config_layer_stack = - load_config_layers_state(codex_home, &cli_overrides, LoaderOverrides::default()).await?; + load_config_layers_state(codex_home, cwd, &cli_overrides, LoaderOverrides::default()) + .await?; let merged_toml = config_layer_stack.effective_config(); let Some(servers_value) = merged_toml.get("mcp_servers") else { return Ok(BTreeMap::new()); @@ -1953,8 +1968,9 @@ trust_level = "trusted" managed_preferences_base64: None, }; + let cwd = AbsolutePathBuf::try_from(codex_home.path())?; let config_layer_stack = - load_config_layers_state(codex_home.path(), &Vec::new(), overrides).await?; + load_config_layers_state(codex_home.path(), Some(cwd), &Vec::new(), overrides).await?; let cfg = deserialize_config_toml_with_base( config_layer_stack.effective_config(), codex_home.path(), @@ -2072,8 +2088,10 @@ trust_level = "trusted" managed_preferences_base64: None, }; + let cwd = AbsolutePathBuf::try_from(codex_home.path())?; let config_layer_stack = load_config_layers_state( codex_home.path(), + Some(cwd), &[("model".to_string(), TomlValue::String("cli".to_string()))], overrides, ) diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index 707936cb7..27785ff0f 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -132,7 +132,7 @@ impl ConfigService { params: ConfigReadParams, ) -> Result { let layers = self - .load_layers_state() + .load_thread_agnostic_config() .await .map_err(|err| ConfigServiceError::io("failed to read configuration layers", err))?; @@ -185,7 +185,7 @@ impl ConfigService { &self, ) -> Result { let layers = self - .load_layers_state() + .load_thread_agnostic_config() .await .map_err(|err| ConfigServiceError::io("failed to load configuration", err))?; @@ -219,7 +219,7 @@ impl ConfigService { } let layers = self - .load_layers_state() + .load_thread_agnostic_config() .await .map_err(|err| ConfigServiceError::io("failed to load configuration", err))?; let user_layer = match layers.get_user_layer() { @@ -328,9 +328,14 @@ impl ConfigService { }) } - async fn load_layers_state(&self) -> std::io::Result { + /// Loads a "thread-agnostic" config, which means the config layers do not + /// include any in-repo .codex/ folders because there is no cwd/project root + /// associated with this query. + async fn load_thread_agnostic_config(&self) -> std::io::Result { + let cwd: Option = None; load_config_layers_state( &self.codex_home, + cwd, &self.cli_overrides, self.loader_overrides.clone(), ) diff --git a/codex-rs/core/src/config_loader/README.md b/codex-rs/core/src/config_loader/README.md index 9df656951..d0df9a734 100644 --- a/codex-rs/core/src/config_loader/README.md +++ b/codex-rs/core/src/config_loader/README.md @@ -10,7 +10,7 @@ This module is the canonical place to **load and describe Codex configuration la Exported from `codex_core::config_loader`: -- `load_config_layers_state(codex_home, cli_overrides, overrides) -> ConfigLayerStack` +- `load_config_layers_state(codex_home, cwd_opt, cli_overrides, overrides) -> ConfigLayerStack` - `ConfigLayerStack` - `effective_config() -> toml::Value` - `origins() -> HashMap` @@ -37,11 +37,14 @@ Most callers want the effective config plus metadata: ```rust use codex_core::config_loader::{load_config_layers_state, LoaderOverrides}; +use codex_utils_absolute_path::AbsolutePathBuf; use toml::Value as TomlValue; let cli_overrides: Vec<(String, TomlValue)> = Vec::new(); +let cwd = AbsolutePathBuf::current_dir()?; let layers = load_config_layers_state( &codex_home, + Some(cwd), &cli_overrides, LoaderOverrides::default(), ).await?; diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index db633de5d..c05825db8 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -55,8 +55,14 @@ const DEFAULT_REQUIREMENTS_TOML_FILE_UNIX: &str = "/etc/codex/requirements.toml" /// (*) Only available on macOS via managed device profiles. /// /// See https://developers.openai.com/codex/security for details. +/// +/// When loading the config stack for a thread, there should be a `cwd` +/// associated with it such that `cwd` should be `Some(...)`. Only for +/// thread-agnostic config loading (e.g., for the app server's `/config` +/// endpoint) should `cwd` be `None`. pub async fn load_config_layers_state( codex_home: &Path, + cwd: Option, cli_overrides: &[(String, TomlValue)], overrides: LoaderOverrides, ) -> io::Result { @@ -122,6 +128,7 @@ pub async fn load_config_layers_state( } // TODO(mbolin): Add layers for cwd, tree, and repo config files. + let _ = cwd; // Add a layer for runtime overrides from the CLI or UI, if any exist. if !cli_overrides.is_empty() { diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 6e376bbb2..5a643b7e9 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -5,6 +5,7 @@ use crate::config_loader::ConfigRequirements; use crate::config_loader::config_requirements::ConfigRequirementsToml; use crate::config_loader::load_requirements_toml; use codex_protocol::protocol::AskForApproval; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; use toml::Value as TomlValue; @@ -40,9 +41,15 @@ extra = true managed_preferences_base64: None, }; - let state = load_config_layers_state(tmp.path(), &[] as &[(String, TomlValue)], overrides) - .await - .expect("load config"); + let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd"); + let state = load_config_layers_state( + tmp.path(), + Some(cwd), + &[] as &[(String, TomlValue)], + overrides, + ) + .await + .expect("load config"); let loaded = state.effective_config(); let table = loaded.as_table().expect("top-level table expected"); @@ -68,9 +75,15 @@ async fn returns_empty_when_all_layers_missing() { managed_preferences_base64: None, }; - let layers = load_config_layers_state(tmp.path(), &[] as &[(String, TomlValue)], overrides) - .await - .expect("load layers"); + let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd"); + let layers = load_config_layers_state( + tmp.path(), + Some(cwd), + &[] as &[(String, TomlValue)], + overrides, + ) + .await + .expect("load layers"); assert!( layers.get_user_layer().is_none(), "no user layer when CODEX_HOME/config.toml does not exist" @@ -138,9 +151,15 @@ flag = true managed_preferences_base64: Some(encoded), }; - let state = load_config_layers_state(tmp.path(), &[] as &[(String, TomlValue)], overrides) - .await - .expect("load config"); + let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd"); + let state = load_config_layers_state( + tmp.path(), + Some(cwd), + &[] as &[(String, TomlValue)], + overrides, + ) + .await + .expect("load config"); let loaded = state.effective_config(); let nested = loaded .get("nested") diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 147814b6c..93a481b63 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -37,6 +37,7 @@ use codex_core::protocol::SessionSource; use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::SandboxMode; use codex_protocol::user_input::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; use event_processor_with_human_output::EventProcessorWithHumanOutput; use event_processor_with_jsonl_output::EventProcessorWithJsonOutput; use serde_json::Value; @@ -132,6 +133,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any } }; + let resolved_cwd = cwd.clone(); + let config_cwd = match resolved_cwd.as_deref() { + Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?, + None => AbsolutePathBuf::current_dir()?, + }; + // we load config.toml here to determine project state. #[allow(clippy::print_stderr)] let config_toml = { @@ -143,7 +150,13 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any } }; - match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides.clone()).await { + match load_config_as_toml_with_cli_overrides( + &codex_home, + &config_cwd, + cli_kv_overrides.clone(), + ) + .await + { Ok(config_toml) => config_toml, Err(err) => { eprintln!("Error loading config.toml: {err}"); @@ -190,7 +203,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // Default to never ask for approvals in headless mode. Feature flags can override. approval_policy: Some(AskForApproval::Never), sandbox_mode, - cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)), + cwd: resolved_cwd, model_provider: model_provider.clone(), codex_linux_sandbox_exe, base_instructions: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index db2b4fa48..b18754cab 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -23,6 +23,7 @@ use codex_core::find_conversation_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; +use codex_utils_absolute_path::AbsolutePathBuf; use std::fs::OpenOptions; use std::path::PathBuf; use tracing::error; @@ -152,15 +153,26 @@ pub async fn run_main( } }; + let cwd = cli.cwd.clone(); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?, + None => AbsolutePathBuf::current_dir()?, + }; + #[allow(clippy::print_stderr)] - let config_toml = - match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides.clone()).await { - Ok(config_toml) => config_toml, - Err(err) => { - eprintln!("Error loading config.toml: {err}"); - std::process::exit(1); - } - }; + let config_toml = match load_config_as_toml_with_cli_overrides( + &codex_home, + &config_cwd, + cli_kv_overrides.clone(), + ) + .await + { + Ok(config_toml) => config_toml, + Err(err) => { + eprintln!("Error loading config.toml: {err}"); + std::process::exit(1); + } + }; let model_provider_override = if cli.oss { let resolved = resolve_oss_provider( @@ -198,8 +210,6 @@ pub async fn run_main( None // No model specified, will use the default. }; - // canonicalize the cwd - let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); let additional_dirs = cli.add_dir.clone(); let overrides = ConfigOverrides { diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index dac62abb5..e31194b5b 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -23,6 +23,7 @@ use codex_core::find_conversation_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; +use codex_utils_absolute_path::AbsolutePathBuf; use std::fs::OpenOptions; use std::path::PathBuf; use tracing::error; @@ -153,15 +154,26 @@ pub async fn run_main( } }; + let cwd = cli.cwd.clone(); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?, + None => AbsolutePathBuf::current_dir()?, + }; + #[allow(clippy::print_stderr)] - let config_toml = - match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides.clone()).await { - Ok(config_toml) => config_toml, - Err(err) => { - eprintln!("Error loading config.toml: {err}"); - std::process::exit(1); - } - }; + let config_toml = match load_config_as_toml_with_cli_overrides( + &codex_home, + &config_cwd, + cli_kv_overrides.clone(), + ) + .await + { + Ok(config_toml) => config_toml, + Err(err) => { + eprintln!("Error loading config.toml: {err}"); + std::process::exit(1); + } + }; let model_provider_override = if cli.oss { let resolved = resolve_oss_provider( @@ -199,8 +211,6 @@ pub async fn run_main( None // No model specified, will use the default. }; - // canonicalize the cwd - let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); let additional_dirs = cli.add_dir.clone(); let overrides = ConfigOverrides { diff --git a/codex-rs/utils/absolute-path/src/lib.rs b/codex-rs/utils/absolute-path/src/lib.rs index 257a83d95..77d16e2d5 100644 --- a/codex-rs/utils/absolute-path/src/lib.rs +++ b/codex-rs/utils/absolute-path/src/lib.rs @@ -34,6 +34,11 @@ impl AbsolutePathBuf { Ok(Self(absolute_path.into_owned())) } + pub fn current_dir() -> std::io::Result { + let current_dir = std::env::current_dir()?; + Self::from_absolute_path(current_dir) + } + pub fn join>(&self, path: P) -> std::io::Result { Self::resolve_path_against_base(path, &self.0) }