Honor model-provided Guardian review policies (#42744)

## What changed

- Add optional per-model Guardian modes for computer use, shell, code mode,
  file changes, MCP, network, and permission requests.
- Apply disabled, synchronous, or adaptive review by action category while
  preserving legacy configuration when model policy is absent.
- Bind cached risk scores to the active model policy, thread settings, and
  environment selections so outdated scores cannot approve later actions.
- Keep unknown review modes on the synchronous path and ignore unknown policy
  fields for forward compatibility.

## Testing

- Cover policy serialization, legacy fallback, scoped tool classification,
  disabled categories, and score invalidation after policy changes.

GitOrigin-RevId: f334a5f4fffb198da66cde3a400233e0df650516
This commit is contained in:
jif authored and copyberry committed 2026-09-04 11:59:18 +00:00
1 parent 9d253c885c
commit 4e48cd02da
26 files changed
+654 -186

No files matched your search

+3
View File
@@ -832,6 +832,9 @@ impl Session {
thread_extension_init.insert(codex_extension_api::ThreadOriginator(
session_configuration.originator.clone(),
));
// Publish the already resolved model before extensions make startup decisions.
// Turn construction refreshes this attachment when the selected model changes.
thread_extension_init.insert(model_info);
let mcp_thread_init = thread_extension_init.clone();
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
thread_id.to_string(),
+8 -2
View File
@@ -107,12 +107,18 @@ fn check_legacy_model_safety(
// TurnMetadataState pins both node REPL flags. Guardian prompt/evidence
// construction also reads node_repl_auto_review_required from the turn.
if retained_models.iter().any(|model| {
model.node_repl_auto_review_required != destination.node_repl_auto_review_required
model.computer_use_review_required() != destination.computer_use_review_required()
}) {
return Err(
"the destination changes the admitted node REPL review requirement".to_string(),
);
}
if retained_models
.iter()
.any(|model| model.guardian != destination.guardian)
{
return Err("the destination changes the admitted Guardian coverage".to_string());
}
if retained_models
.iter()
.any(|model| model.node_repl_disabled != destination.node_repl_disabled)
@@ -130,7 +136,7 @@ fn check_legacy_model_safety(
return Err("the destination changes the explicit Guardian reviewer model".to_string());
}
if admitted_config.features.enabled(Feature::GuardianV2)
if (admitted_config.features.enabled(Feature::GuardianV2) || destination.guardian.is_some())
&& admitted_config.features.enabled(Feature::GuardianApproval)
{
// GuardianV2Extension::on_tool_start reads the parent ModelInfo from
+1 -8
View File
@@ -1069,14 +1069,7 @@ async fn track_turn_resolved_config_analytics(
.services
.thread_extension_data
.get::<codex_extension_api::GuardianV2Enabled>()
.is_some_and(|state| {
state.computer_use_only
|| !turn_context
.config
.config_layer_stack
.requirements()
.auto_review_required_for_model(&turn_context.model_info().slug)
}),
.is_some(),
sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(),
collaboration_mode: turn_context.mode(),
personality: turn_context.personality(),