mirror of
https://github.com/openai/codex.git
synced 2026-05-05 22:01:37 +03:00
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:
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user