From 41833c879b4e65f6168f48b85d4fffb693279f8f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 19 Dec 2025 18:02:24 -0800 Subject: [PATCH] chore: enusre the logic that creates ConfigLayerStack has access to cwd --- codex-rs/cli/src/main.rs | 12 ++++++++- codex-rs/core/src/config/mod.rs | 19 ++++++++++--- codex-rs/core/src/config/service.rs | 2 ++ codex-rs/core/src/config_loader/README.md | 5 +++- codex-rs/core/src/config_loader/mod.rs | 2 ++ codex-rs/core/src/config_loader/tests.rs | 25 ++++++++++------- codex-rs/exec/src/lib.rs | 17 ++++++++++-- codex-rs/tui/src/lib.rs | 33 ++++++++++++++++------- codex-rs/tui2/src/lib.rs | 33 ++++++++++++++++------- 9 files changed, 111 insertions(+), 37 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 80db64767d..c37bd395e2 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,16 @@ 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() + .map(|path| path.canonicalize().unwrap_or(path)); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::try_from(path)?, + None => AbsolutePathBuf::try_from(std::env::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 986e9eb91a..223b0f58f8 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -346,8 +346,12 @@ 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::try_from(std::env::current_dir()?)?, + }; let config_layer_stack = - load_config_layers_state(&codex_home, &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 @@ -401,10 +405,12 @@ 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?; + load_config_layers_state(codex_home, cwd, &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 +444,10 @@ 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(); + let cwd = AbsolutePathBuf::try_from(std::env::current_dir()?)?; 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 +1961,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(), &cwd, &Vec::new(), overrides).await?; let cfg = deserialize_config_toml_with_base( config_layer_stack.effective_config(), codex_home.path(), @@ -2072,8 +2081,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(), + &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 707936cb74..f177ddeea4 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -329,8 +329,10 @@ impl ConfigService { } async fn load_layers_state(&self) -> std::io::Result { + let cwd = AbsolutePathBuf::try_from(std::env::current_dir()?)?; 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 9df656951c..c86fa11aec 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, 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::try_from(std::env::current_dir()?)?; let layers = load_config_layers_state( &codex_home, + &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 db633de5d7..8323601452 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -57,6 +57,7 @@ const DEFAULT_REQUIREMENTS_TOML_FILE_UNIX: &str = "/etc/codex/requirements.toml" /// See https://developers.openai.com/codex/security for details. pub async fn load_config_layers_state( codex_home: &Path, + cwd: &AbsolutePathBuf, cli_overrides: &[(String, TomlValue)], overrides: LoaderOverrides, ) -> io::Result { @@ -122,6 +123,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 6e376bbb2b..0b0ad7febd 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,11 @@ 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(), &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 +71,11 @@ 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(), &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 +143,11 @@ 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(), &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 147814b6ce..a5c9d7cb0e 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().map(|path| path.canonicalize().unwrap_or(path)); + let config_cwd = match resolved_cwd.as_deref() { + Some(path) => AbsolutePathBuf::try_from(path)?, + None => AbsolutePathBuf::try_from(std::env::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 db2b4fa48e..3360dd641b 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,29 @@ pub async fn run_main( } }; + let cwd = cli + .cwd + .clone() + .map(|path| path.canonicalize().unwrap_or(path)); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::try_from(path)?, + None => AbsolutePathBuf::try_from(std::env::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 +213,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 dac62abb56..d9886c1a9c 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,29 @@ pub async fn run_main( } }; + let cwd = cli + .cwd + .clone() + .map(|path| path.canonicalize().unwrap_or(path)); + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::try_from(path)?, + None => AbsolutePathBuf::try_from(std::env::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 +214,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 {