Compose requirements layers (#24619)

## Summary

PR 2 of 5 in the cloud-managed config client stack.

Adds a shared requirements-layer composition engine. The composer
defines how ordered requirements layers combine, with focused tests for
the merge semantics and provenance behavior. The final PR in the stack
wires runtime requirements sources into this path.

## Details

- Mental model: requirements layers are ordered lowest priority first,
matching `ConfigLayerStack`; lower-priority layers provide defaults
while higher-priority layers win scalar/list conflicts.
- Regular fields use config-style TOML merging, including recursive
table merging, so requirements layering follows the same broad model as
`config.toml` layering.
- Domain-specific fields keep explicit semantics: `rules.prefix_rules`
and hooks preserve high-priority-first output, hooks fail closed on
active managed-dir conflicts, and `permissions.filesystem.deny_read`
dedupes as a stable high-priority-first union.
- `remote_sandbox_config` is evaluated within each layer before the
regular TOML merge, so host-specific sandbox constraints do not leak
across layers.
- Provenance points at the exact source when one layer owns a value and
uses composite provenance when a table field is assembled from multiple
layers.

## Validation

Local validation:

- `just fmt`
- `cargo check -p codex-config`
- `just test -p codex-config requirements_composition`
- `git diff --check`

CI will run the broader test matrix.
This commit is contained in:
joeflorencio-openai
2026-05-31 15:14:06 -07:00
committed by GitHub
Unverified
parent 5f60b01352
commit 20debf746b
10 changed files with 1932 additions and 6 deletions
+69
View File
@@ -277,6 +277,16 @@ fn fallback_managed_hooks_source_path(
Some(RequirementSource::CloudRequirements) => {
synthetic_layer_path("<cloud-requirements>/requirements.toml")
}
Some(RequirementSource::Composite { .. }) => {
synthetic_layer_path("<requirements-composition>/requirements.toml")
}
Some(RequirementSource::EnterpriseManaged { id, name }) => {
let name = escape_xml_text(name);
let id = escape_xml_text(id);
synthetic_layer_path(&format!(
"<enterprise-managed:{name}:{id}>/requirements.toml"
))
}
Some(RequirementSource::LegacyManagedConfigTomlFromMdm) => {
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
}
@@ -380,6 +390,21 @@ fn synthetic_layer_path(path: &str) -> AbsolutePathBuf {
}
}
fn escape_xml_text(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&amp;"),
'<' => escaped.push_str("&lt;"),
'>' => escaped.push_str("&gt;"),
'"' => escaped.push_str("&quot;"),
'\'' => escaped.push_str("&apos;"),
_ => escaped.push(ch),
}
}
escaped
}
fn append_hook_events(
handlers: &mut Vec<ConfiguredHandler>,
hook_entries: &mut Vec<HookListEntry>,
@@ -607,6 +632,14 @@ fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> Hoo
HookSource::LegacyManagedConfigMdm
}
Some(RequirementSource::CloudRequirements) => HookSource::CloudRequirements,
Some(RequirementSource::Composite { sources }) => {
// Requirements hook composition preserves contributing sources in
// priority order, but discovery only carries one source for the
// whole merged hooks field. Use the primary contributor as the best
// available coarse attribution.
hook_source_for_requirement_source(sources.first())
}
Some(RequirementSource::EnterpriseManaged { .. }) => HookSource::CloudRequirements,
Some(RequirementSource::Unknown) | None => HookSource::Unknown,
}
}
@@ -616,6 +649,7 @@ mod tests {
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
use codex_config::HookEventsToml;
use codex_config::RequirementSource;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -672,6 +706,41 @@ mod tests {
}
}
#[test]
fn composite_requirement_hook_source_uses_primary_source() {
let source = RequirementSource::Composite {
sources: vec![
RequirementSource::SystemRequirementsToml {
file: test_path_buf("/etc/codex/requirements.toml").abs(),
},
RequirementSource::EnterpriseManaged {
id: "layer-1".to_string(),
name: "Engineering".to_string(),
},
],
};
assert_eq!(
super::hook_source_for_requirement_source(Some(&source)),
HookSource::System
);
}
#[test]
fn enterprise_managed_synthetic_path_escapes_display_fields() {
let source = RequirementSource::EnterpriseManaged {
id: "id<&>".to_string(),
name: "Name <Admin> & \"Ops\"".to_string(),
};
let source_path = super::fallback_managed_hooks_source_path(Some(&source));
let source_path = source_path.display().to_string();
assert!(source_path.contains("Name &lt;Admin&gt; &amp; &quot;Ops&quot;"));
assert!(source_path.contains("id&lt;&amp;&gt;"));
assert!(!source_path.contains("Name <Admin>"));
}
fn command_group(matcher: Option<&str>) -> MatcherGroup {
MatcherGroup {
matcher: matcher.map(str::to_string),