Route approvals through the extension decision API (#43432)

## What changed

- Let approval extensions choose between cached approval, synchronous review, and a user prompt for tool and permission requests, while core enforces mandatory Guardian and fresh-review requirements.
- Carry an optional `review_reason` on Guardian assessment events and use it to trigger strict-review notifications, retaining the fallback for older events.
- Require synchronous review when cached evidence has an unusable encrypted parent compaction.

## Testing

Add regression coverage for manual prompts and cached approvals with non-UTF-8 working directories. Update approval contributors in existing tests to use the decision API.

GitOrigin-RevId: a4e66416070bdaa881daa41429c49021629f44c1
This commit is contained in:
jif authored and copyberry committed 2026-09-07 11:07:28 +00:00
1 parent 0df39752cb
commit e1eb98461c
24 files changed
+605 -250

No files matched your search

+2
View File
@@ -891,6 +891,7 @@ async fn review_guardian_mcp_elicitation(
guardian_request,
/*retry_reason*/ None,
crate::guardian::GuardianReviewOptions {
require_guardian: false,
plugin_attribution_override: None,
approval_request_source: codex_analytics::GuardianApprovalRequestSource::MainTurn,
external_cancel: Some(cancellation_token),
@@ -984,6 +985,7 @@ async fn review_guardian_mcp_elicitation(
guardian_request,
/*retry_reason*/ None,
crate::guardian::GuardianReviewOptions {
require_guardian: false,
plugin_attribution_override: None,
approval_request_source: codex_analytics::GuardianApprovalRequestSource::MainTurn,
external_cancel: Some(cancellation_token),
+47 -50
View File
@@ -2780,7 +2780,6 @@ impl Session {
) -> Option<RequestPermissionsResponse> {
let turn_context = &step_context.turn;
let approval_policy = step_context.settings.approval_policy();
let approvals_reviewer = step_context.settings.approvals_reviewer();
let Some(environment) = step_context
.environments
.turn_environments()
@@ -2827,7 +2826,6 @@ impl Session {
strict_auto_review: false,
});
};
if crate::guardian::routes_approval_policy_to_guardian(approval_policy, approvals_reviewer)
{
let originating_turn_state = {
let active = self.active_turn.lock().await;
@@ -2836,13 +2834,13 @@ impl Session {
let action = ApprovalAction::RequestPermissions {
id: call_id.clone(),
turn_id: turn_context.sub_id.clone(),
reason: args.reason,
reason: args.reason.clone(),
permissions: requested_permissions.clone(),
};
let approval_context = ApprovalContext {
review_context: crate::guardian::GuardianReviewContext::from(step_context),
cancellation_token: Some(cancellation_token.clone()),
call_id,
call_id: call_id.clone(),
tool_name: ToolName::plain("request_permissions"),
strict_auto_review: false,
approval_reason: None,
@@ -2857,54 +2855,53 @@ impl Session {
&approval_context,
) => decision,
};
let response = match decision {
ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
strict_auto_review: false,
if let Some(decision) = decision {
let (permissions, scope) = match decision {
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment:
NetworkPolicyAmendment {
action: NetworkPolicyRuleAction::Allow,
..
},
} => (requested_permissions.clone(), PermissionGrantScope::Turn),
ReviewDecision::ApprovedForSession => {
(requested_permissions.clone(), PermissionGrantScope::Session)
}
}
ReviewDecision::ApprovedForSession => RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Session,
ReviewDecision::ApprovedMcpPolicyAmendment
| ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment:
NetworkPolicyAmendment {
action: NetworkPolicyRuleAction::Deny,
..
},
}
| ReviewDecision::Abort
| ReviewDecision::Denied { .. }
| ReviewDecision::TimedOut => (
RequestPermissionProfile::default(),
PermissionGrantScope::Turn,
),
};
let response = RequestPermissionsResponse {
permissions,
scope,
strict_auto_review: false,
},
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
strict_auto_review: false,
},
NetworkPolicyRuleAction::Deny => RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
scope: PermissionGrantScope::Turn,
strict_auto_review: false,
},
},
ReviewDecision::ApprovedMcpPolicyAmendment
| ReviewDecision::Abort
| ReviewDecision::Denied { .. }
| ReviewDecision::TimedOut => RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
scope: PermissionGrantScope::Turn,
strict_auto_review: false,
},
};
let response = Self::normalize_request_permissions_response(
requested_permissions,
response,
&context,
);
self.record_granted_request_permissions_for_turn(
&response,
&environment.selection.environment_id,
originating_turn_state.as_ref(),
)
.await;
return Some(response);
};
let response = Self::normalize_request_permissions_response(
requested_permissions,
response,
&context,
);
self.record_granted_request_permissions_for_turn(
&response,
&environment.selection.environment_id,
originating_turn_state.as_ref(),
)
.await;
return Some(response);
}
}
let _elicitation = self.services.elicitations.register();