diff --git a/codex-rs/config/src/requirements_layers/layer.rs b/codex-rs/config/src/requirements_layers/layer.rs index 3edcbbd18e..87f8057ff1 100644 --- a/codex-rs/config/src/requirements_layers/layer.rs +++ b/codex-rs/config/src/requirements_layers/layer.rs @@ -102,6 +102,13 @@ impl ComposableRequirementsLayer { (regular_toml, requirements) }; + // Merge aliases under one key after validating each individual layer. + if let Some(table) = regular_toml.as_table_mut() + && let Some(features) = table.remove("feature_requirements") + { + table.insert("features".to_string(), features); + } + // Hostname lookup is configuration-driven and may block on DNS, so only // resolve it when this layer contains hostname-based sandbox selectors. let hostname = requirements diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index 51bbae90c8..974cddbecc 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -469,6 +469,35 @@ approval_mode = "approve" ); } +#[test] +fn feature_aliases_merge_with_layer_precedence() { + for (low_key, high_key) in [ + ("features", "feature_requirements"), + ("feature_requirements", "features"), + ] { + let composed = compose(vec![ + layer( + "req_low", + "Low", + &format!("[{low_key}]\nchronicle = true\nshell_snapshot = false"), + ), + layer( + "req_high", + "High", + &format!("[{high_key}]\nchronicle = false\napps = false"), + ), + ]) + .expect("compose mixed feature aliases"); + + assert_eq!( + composed, + Some(expected_requirements( + "[features]\nchronicle = false\nshell_snapshot = false\napps = false" + )) + ); + } +} + #[test] fn merged_table_source_is_composite_in_priority_order() { let high_source = RequirementSource::EnterpriseManaged {