From 14dbd0610a7a43fc716d394044c9f20ab9721cc6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 22 Dec 2025 16:40:26 -0800 Subject: [PATCH] chore: include User layer in ConfigLayerStack even if config.toml is empty (#8456) This is necessary so that `$CODEX_HOME/skills` and `$CODEX_HOME/rules` still get loaded even if `$CODEX_HOME/config.toml` does not exist. See #8453. For now, it is possible to omit this layer when creating a dummy `ConfigLayerStack` in a test. We can revisit that later, if it turns out to be the right thing to do. --- .../app-server-protocol/src/protocol/v2.rs | 2 ++ codex-rs/core/src/config_loader/mod.rs | 20 +++++++++++------- codex-rs/core/src/config_loader/tests.rs | 21 ++++++++++++++++--- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 7ba7cc04b..7a360cee2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -237,6 +237,8 @@ pub enum ConfigLayerSource { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] User { + /// This is the path to the user's config.toml file, though it is not + /// guaranteed to exist. file: AbsolutePathBuf, }, diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 4bc9fb9c5..7d98db708 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -101,7 +101,7 @@ pub async fn load_config_layers_state( // exists, but is malformed, then this error should be propagated to the // user. let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home)?; - match tokio::fs::read_to_string(&user_file).await { + let user_layer = match tokio::fs::read_to_string(&user_file).await { Ok(contents) => { let user_config: TomlValue = toml::from_str(&contents).map_err(|e| { io::Error::new( @@ -123,13 +123,18 @@ pub async fn load_config_layers_state( })?; let user_config = resolve_relative_paths_in_config_toml(user_config, user_config_parent)?; - layers.push(ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - user_config, - )); + ConfigLayerEntry::new(ConfigLayerSource::User { file: user_file }, user_config) } Err(e) => { - if e.kind() != io::ErrorKind::NotFound { + if e.kind() == io::ErrorKind::NotFound { + // If there is no config.toml file, record an empty entry + // for this user layer, as this may still have subfolders + // that are significant in the overall ConfigLayerStack. + ConfigLayerEntry::new( + ConfigLayerSource::User { file: user_file }, + TomlValue::Table(toml::map::Map::new()), + ) + } else { return Err(io::Error::new( e.kind(), format!( @@ -139,7 +144,8 @@ pub async fn load_config_layers_state( )); } } - } + }; + layers.push(user_layer); if let Some(cwd) = cwd { let mut merged_so_far = TomlValue::Table(toml::map::Map::new()); diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 7160f8c10..4efeeaa98 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -88,9 +88,24 @@ async fn returns_empty_when_all_layers_missing() { ) .await .expect("load layers"); - assert!( - layers.get_user_layer().is_none(), - "no user layer when CODEX_HOME/config.toml does not exist" + let user_layer = layers + .get_user_layer() + .expect("expected a user layer even when CODEX_HOME/config.toml does not exist"); + assert_eq!( + &ConfigLayerEntry { + name: super::ConfigLayerSource::User { + file: AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, tmp.path()) + .expect("resolve user config.toml path") + }, + config: TomlValue::Table(toml::map::Map::new()), + version: version_for_toml(&TomlValue::Table(toml::map::Map::new())), + }, + user_layer, + ); + assert_eq!( + user_layer.config, + TomlValue::Table(toml::map::Map::new()), + "expected empty config for user layer when config.toml does not exist" ); let binding = layers.effective_config();