Files
codex/codex-rs/config/src/merge_tests.rs
T
viyatb-oaiandGitHub fe7c069fe6 feat(permissions): resolve permission profile inheritance (#22270)
## Stack

This is the foundation PR for the permission-profile inheritance stack.

- This PR adds config-level `extends` resolution and merge semantics.
- Follow-up: #23705 applies resolved profiles at runtime and updates the
active-profile protocol surfaces.

## Why

Permission profiles are starting to carry enough policy that
copy-pasting near-identical definitions becomes hard to review and easy
to drift. Before the runtime can consume inherited profiles, the config
layer needs one explicit resolver that can merge parent chains and
reject unsafe or invalid inheritance shapes.

## What changed

- Add `extends` to permission-profile TOML and resolve parent chains in
inheritance order.
- Merge inherited profile TOML with the existing config merge behavior
while preserving the permission-specific normalization needed for
network domain keys.
- Keep parent descriptions out of resolved child profiles and record
inherited profile names separately for downstream consumers.
- Reject undefined parents, unsupported built-in parents, and
inheritance cycles with targeted errors.
- Cover resolver behavior with TOML fixture tests and refresh the
generated config schema.

## Validation

- `cargo test -p codex-config`
- `cargo test -p codex-core permissions_profiles_`
2026-05-20 20:12:07 +00:00

127 lines
2.6 KiB
Rust

use super::*;
use crate::config_toml::ConfigToml;
use crate::types::MemoriesToml;
use pretty_assertions::assert_eq;
fn parse_toml(value: &str) -> TomlValue {
toml::from_str(value).expect("TOML should parse")
}
#[test]
fn merge_toml_values_normalizes_legacy_key_from_base_layer() {
let mut base = parse_toml(
r#"
[memories]
no_memories_if_mcp_or_web_search = false
"#,
);
let overlay = parse_toml(
r#"
[memories]
disable_on_external_context = true
"#,
);
merge_toml_values(&mut base, &overlay);
let expected = parse_toml(
r#"
[memories]
disable_on_external_context = true
"#,
);
assert_eq!(base, expected);
let config: ConfigToml = base.try_into().expect("merged config should deserialize");
assert_eq!(
config.memories,
Some(MemoriesToml {
disable_on_external_context: Some(true),
..Default::default()
})
);
}
#[test]
fn merge_toml_values_normalizes_legacy_key_from_overlay_layer() {
let mut base = parse_toml(
r#"
[memories]
disable_on_external_context = false
"#,
);
let overlay = parse_toml(
r#"
[memories]
no_memories_if_mcp_or_web_search = true
"#,
);
merge_toml_values(&mut base, &overlay);
let expected = parse_toml(
r#"
[memories]
disable_on_external_context = true
"#,
);
assert_eq!(base, expected);
let config: ConfigToml = base.try_into().expect("merged config should deserialize");
assert_eq!(
config.memories,
Some(MemoriesToml {
disable_on_external_context: Some(true),
..Default::default()
})
);
}
#[test]
fn merge_toml_values_prefers_canonical_key_when_one_layer_has_both_names() {
let mut base = TomlValue::Table(toml::map::Map::new());
let overlay = parse_toml(
r#"
[memories]
disable_on_external_context = true
no_memories_if_mcp_or_web_search = false
"#,
);
merge_toml_values(&mut base, &overlay);
let expected = parse_toml(
r#"
[memories]
disable_on_external_context = true
"#,
);
assert_eq!(base, expected);
}
#[test]
fn merge_toml_values_normalizes_permission_network_domains_before_overlaying() {
let mut base = parse_toml(
r#"
[permissions.dev.network.domains]
"example.com" = "deny"
"#,
);
let overlay = parse_toml(
r#"
[permissions.dev.network.domains]
"EXAMPLE.COM" = "allow"
"#,
);
merge_toml_values(&mut base, &overlay);
let expected = parse_toml(
r#"
[permissions.dev.network.domains]
"example.com" = "allow"
"#,
);
assert_eq!(base, expected);
}