Source Guardian REPL policy from model metadata (#41919)

## What changed

- Add `auto_review.node_repl_policy` to model messages for `node_repl` and `cua_repl` reviews.
- Use the reviewer model's configured policy, falling back to the bundled policy when the field is absent and skipping injection when it is explicitly empty.
- Include the effective policy in Guardian session reuse checks and reject unsafe parent-model fallback transitions that would change it.

## Testing

- Cover catalog, bundled, and explicitly empty policies across Node and CUA review paths.
- Verify that policy changes invalidate cached reviewer sessions and incompatible fallback transitions.

GitOrigin-RevId: 51c0bdfed9b464091b6eb47d91ca5a0990d7a315
This commit is contained in:
olliem-oai authored and copyberry committed 2026-08-31 22:08:44 +00:00
1 parent 746798b2f7
commit 865bbf9a69
11 files changed
+144 -24

No files matched your search

@@ -8,6 +8,7 @@ use super::step_settings::StepSettingsUpdate;
use super::turn_context::TurnContext;
use crate::config::Config;
use crate::config::ConstraintResult;
use crate::context::GuardianNodeReplPolicy;
use crate::exec_policy::AllowPrefixRules;
use crate::guardian::BUNDLED_GUARDIAN_POLICY_TEMPLATE;
use codex_features::Feature;
@@ -173,6 +174,17 @@ fn check_legacy_model_safety(
// catalog refresh. V1 uses the admitted config; V2 can use the live config.
// An unchanged explicit reviewer override prevents both fallback paths.
if destination.auto_review_model_override.is_none() {
let destination_node_repl_policy =
GuardianNodeReplPolicy::from_model_messages(destination.model_messages.as_ref());
for model in retained_models {
let policy = GuardianNodeReplPolicy::from_model_messages(model.model_messages.as_ref());
if policy != destination_node_repl_policy {
return Err(
"the destination changes the Guardian parent-fallback node REPL policy"
.to_string(),
);
}
}
for config in [admitted_config, live_config] {
let destination_policy =
config.resolve_guardian_policy(destination.model_messages.as_ref());
@@ -861,6 +861,7 @@ fn parent_review_messages(model: &mut ModelInfo) -> &mut AutoReviewMessages {
.get_or_insert(AutoReviewMessages {
policy: None,
policy_template: None,
node_repl_policy: None,
rejection_instructions: None,
timeout_instructions: None,
})
@@ -996,6 +997,11 @@ async fn parent_fallback_preserves_explicit_empty_and_bundled_defaults() {
);
parent_review_messages(&mut destination).policy = None;
assert_eq!(check(&destination), Ok(()));
parent_review_messages(&mut destination).node_repl_policy = Some(String::new());
assert_eq!(
check(&destination),
Err("the destination changes the Guardian parent-fallback node REPL policy".to_string())
);
}
#[tokio::test]
@@ -1008,6 +1014,7 @@ async fn unchanged_explicit_reviewer_does_not_use_parent_policy() {
parent_review_messages(&mut admitted).policy = Some("catalog policy A".to_string());
parent_review_messages(&mut destination).policy = Some("catalog policy B".to_string());
parent_review_messages(&mut destination).policy_template = Some(String::new());
parent_review_messages(&mut destination).node_repl_policy = Some(String::new());
assert_eq!(
check_legacy_model_safety(
&admitted,
@@ -411,6 +411,7 @@ async fn model_resolution_preserves_startup_overrides_and_instruction_provenance
let auto_review = AutoReviewMessages {
policy: Some("catalog review policy".to_string()),
policy_template: Some("catalog review template".to_string()),
node_repl_policy: None,
rejection_instructions: None,
timeout_instructions: None,
};