feat(request-permissions) approve with strict review (#19050)

## Summary
Allow the user to approve a request_permissions_tool request with the
condition that all commands in the rest of the turn are reviewed by
guardian, regardless of sandbox status.

## Testing
- [x] Added unit tests
- [x] Ran locally
This commit is contained in:
Dylan Hurd
2026-04-22 18:56:32 -07:00
committed by GitHub
parent c6ab601824
commit 5e71da1424
20 changed files with 609 additions and 134 deletions

View File

@@ -278,8 +278,8 @@ impl ApprovalOverlay {
permissions,
..
},
ApprovalDecision::Review(decision),
) => self.handle_permissions_decision(call_id, permissions, decision.clone()),
ApprovalDecision::Permissions(decision),
) => self.handle_permissions_decision(call_id, permissions, *decision),
(ApprovalRequest::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
self.handle_patch_decision(id, decision.clone());
}
@@ -322,27 +322,31 @@ impl ApprovalOverlay {
&self,
call_id: &str,
permissions: &RequestPermissionProfile,
decision: ReviewDecision,
decision: PermissionsDecision,
) {
let Some(request) = self.current_request.as_ref() else {
return;
};
let granted_permissions = match decision {
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => permissions.clone(),
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
Default::default()
}
ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => Default::default(),
PermissionsDecision::GrantForTurn
| PermissionsDecision::GrantForTurnWithStrictAutoReview
| PermissionsDecision::GrantForSession => permissions.clone(),
PermissionsDecision::Deny => Default::default(),
};
let scope = if matches!(decision, ReviewDecision::ApprovedForSession) {
let scope = if matches!(decision, PermissionsDecision::GrantForSession) {
PermissionGrantScope::Session
} else {
PermissionGrantScope::Turn
};
let strict_auto_review = matches!(
decision,
PermissionsDecision::GrantForTurnWithStrictAutoReview
);
if request.thread_label().is_none() {
let message = if granted_permissions.is_empty() {
"You did not grant additional permissions"
} else if strict_auto_review {
"You granted additional permissions with strict auto review"
} else if matches!(scope, PermissionGrantScope::Session) {
"You granted additional permissions for this session"
} else {
@@ -359,6 +363,7 @@ impl ApprovalOverlay {
codex_protocol::request_permissions::RequestPermissionsResponse {
permissions: granted_permissions,
scope,
strict_auto_review,
},
);
}
@@ -482,7 +487,11 @@ impl BottomPaneView for ApprovalOverlay {
permissions,
..
} => {
self.handle_permissions_decision(call_id, permissions, ReviewDecision::Abort);
self.handle_permissions_decision(
call_id,
permissions,
PermissionsDecision::Deny,
);
}
ApprovalRequest::ApplyPatch { id, .. } => {
self.handle_patch_decision(id, ReviewDecision::Abort);
@@ -679,9 +688,18 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
#[derive(Clone)]
enum ApprovalDecision {
Review(ReviewDecision),
Permissions(PermissionsDecision),
McpElicitation(ElicitationAction),
}
#[derive(Clone, Copy)]
enum PermissionsDecision {
GrantForTurn,
GrantForTurnWithStrictAutoReview,
GrantForSession,
Deny,
}
#[derive(Clone)]
struct ApprovalOption {
label: String,
@@ -901,20 +919,28 @@ fn patch_options() -> Vec<ApprovalOption> {
fn permissions_options() -> Vec<ApprovalOption> {
vec![
ApprovalOption {
label: "Yes, grant these permissions".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Approved),
label: "Yes, grant these permissions for this turn".to_string(),
decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForTurn),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
},
ApprovalOption {
label: "Yes, grant for this turn with strict auto review".to_string(),
decision: ApprovalDecision::Permissions(
PermissionsDecision::GrantForTurnWithStrictAutoReview,
),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('r'))],
},
ApprovalOption {
label: "Yes, grant these permissions for this session".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
decision: ApprovalDecision::Permissions(PermissionsDecision::GrantForSession),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))],
},
ApprovalOption {
label: "No, continue without permissions".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Denied),
decision: ApprovalDecision::Permissions(PermissionsDecision::Deny),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
},
@@ -1347,7 +1373,8 @@ mod tests {
assert_eq!(
labels,
vec![
"Yes, grant these permissions".to_string(),
"Yes, grant these permissions for this turn".to_string(),
"Yes, grant for this turn with strict auto review".to_string(),
"Yes, grant these permissions for this session".to_string(),
"No, continue without permissions".to_string(),
]
@@ -1410,6 +1437,34 @@ mod tests {
);
}
#[test]
fn permissions_strict_auto_review_shortcut_submits_turn_scope_with_strict_review() {
let (tx, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx);
let mut view =
ApprovalOverlay::new(make_permissions_request(), tx, Features::with_defaults());
view.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
let mut saw_op = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: Op::RequestPermissionsResponse { response, .. },
..
} = ev
{
assert_eq!(response.scope, PermissionGrantScope::Turn);
assert!(response.strict_auto_review);
saw_op = true;
break;
}
}
assert!(
saw_op,
"expected permission approval decision to emit a strict auto review response"
);
}
#[test]
fn additional_permissions_prompt_shows_permission_rule_line() {
let (tx, _rx) = unbounded_channel::<AppEvent>();

View File

@@ -9,8 +9,9 @@ expression: "normalize_snapshot_paths(render_overlay_lines(&view, 120))"
Permission rule: network; read `/tmp/readme.txt`; write `/tmp/out.txt`
1. Yes, grant these permissions (y)
2. Yes, grant these permissions for this session (a)
3. No, continue without permissions (n)
1. Yes, grant these permissions for this turn (y)
2. Yes, grant for this turn with strict auto review (r)
3. Yes, grant these permissions for this session (a)
4. No, continue without permissions (n)
Press enter to confirm or esc to cancel