mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
5f60b01352
commit
20debf746b
@@ -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("&"),
|
||||
'<' => escaped.push_str("<"),
|
||||
'>' => escaped.push_str(">"),
|
||||
'"' => escaped.push_str("""),
|
||||
'\'' => escaped.push_str("'"),
|
||||
_ => 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 <Admin> & "Ops""));
|
||||
assert!(source_path.contains("id<&>"));
|
||||
assert!(!source_path.contains("Name <Admin>"));
|
||||
}
|
||||
|
||||
fn command_group(matcher: Option<&str>) -> MatcherGroup {
|
||||
MatcherGroup {
|
||||
matcher: matcher.map(str::to_string),
|
||||
|
||||
Reference in New Issue
Block a user