feat(core) Persist request_permission data across turns (#14009)

## Summary
request_permissions flows should support persisting results for the
session.

Open Question: Still deciding if we need within-turn approvals - this
adds complexity but I could see it being useful

## Testing
- [x] Updated unit tests

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-03-09 14:36:38 -07:00
committed by GitHub
co-authored by Codex
parent 831ee51c86
commit d241dc598c
21 changed files with 638 additions and 8 deletions
@@ -28,6 +28,7 @@ use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::request_permissions::PermissionGrantScope;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -282,9 +283,16 @@ impl ApprovalOverlay {
ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => Default::default(),
};
let scope = if matches!(decision, ReviewDecision::ApprovedForSession) {
PermissionGrantScope::Session
} else {
PermissionGrantScope::Turn
};
if request.thread_label().is_none() {
let message = if granted_permissions.is_empty() {
"You did not grant additional permissions"
} else if matches!(scope, PermissionGrantScope::Session) {
"You granted additional permissions for this session"
} else {
"You granted additional permissions"
};
@@ -299,6 +307,7 @@ impl ApprovalOverlay {
id: call_id.to_string(),
response: codex_protocol::request_permissions::RequestPermissionsResponse {
permissions: granted_permissions,
scope,
},
},
});
@@ -831,6 +840,12 @@ fn permissions_options() -> Vec<ApprovalOption> {
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
},
ApprovalOption {
label: "Yes, grant these permissions for this session".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
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),
@@ -1244,11 +1259,39 @@ mod tests {
labels,
vec![
"Yes, grant these permissions".to_string(),
"Yes, grant these permissions for this session".to_string(),
"No, continue without permissions".to_string(),
]
);
}
#[test]
fn permissions_session_shortcut_submits_session_scope() {
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('a'), 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::Session);
saw_op = true;
break;
}
}
assert!(
saw_op,
"expected permission approval decision to emit a session-scoped response"
);
}
#[test]
fn additional_permissions_prompt_shows_permission_rule_line() {
let (tx, _rx) = unbounded_channel::<AppEvent>();
@@ -10,6 +10,7 @@ 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. No, continue without permissions (n)
2. Yes, grant these permissions for this session (a)
3. No, continue without permissions (n)
Press enter to confirm or esc to cancel