feat: include available decisions in command approval requests (#12758)

Command-approval clients currently infer which choices to show from
side-channel fields like `networkApprovalContext`,
`proposedExecpolicyAmendment`, and `additionalPermissions`. That makes
the request shape harder to evolve, and it forces each client to
replicate the server's heuristics instead of receiving the exact
decision list for the prompt.

This PR introduces a mapping between `CommandExecutionApprovalDecision`
and `codex_protocol::protocol::ReviewDecision`:

```rust
impl From<CoreReviewDecision> for CommandExecutionApprovalDecision {
    fn from(value: CoreReviewDecision) -> Self {
        match value {
            CoreReviewDecision::Approved => Self::Accept,
            CoreReviewDecision::ApprovedExecpolicyAmendment {
                proposed_execpolicy_amendment,
            } => Self::AcceptWithExecpolicyAmendment {
                execpolicy_amendment: proposed_execpolicy_amendment.into(),
            },
            CoreReviewDecision::ApprovedForSession => Self::AcceptForSession,
            CoreReviewDecision::NetworkPolicyAmendment {
                network_policy_amendment,
            } => Self::ApplyNetworkPolicyAmendment {
                network_policy_amendment: network_policy_amendment.into(),
            },
            CoreReviewDecision::Abort => Self::Cancel,
            CoreReviewDecision::Denied => Self::Decline,
        }
    }
}
```

And updates `CommandExecutionRequestApprovalParams` to have a new field:

```rust
available_decisions: Option<Vec<CommandExecutionApprovalDecision>>
```

when, if specified, should make it easier for clients to display an
appropriate list of options in the UI.

This makes it possible for `CoreShellActionProvider::prompt()` in
`unix_escalation.rs` to specify the `Vec<ReviewDecision>` directly,
adding support for `ApprovedForSession` when approving a skill script,
which was previously missing in the TUI.

Note this results in a significant change to `exec_options()` in
`approval_overlay.rs`, as the displayed options are now derived from
`available_decisions: &[ReviewDecision]`.

## What Changed

- Add `available_decisions` to
[`ExecApprovalRequestEvent`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/protocol/src/approvals.rs#L111-L175),
including helpers to derive the legacy default choices when older
senders omit the field.
- Map `codex_protocol::protocol::ReviewDecision` to app-server
`CommandExecutionApprovalDecision` and expose the ordered list as
experimental `availableDecisions` in
[`CommandExecutionRequestApprovalParams`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/app-server-protocol/src/protocol/v2.rs#L3798-L3807).
- Thread optional `available_decisions` through the core approval path
so Unix shell escalation can explicitly request `ApprovedForSession` for
session-scoped approvals instead of relying on client heuristics.
[`unix_escalation.rs`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs#L194-L214)
- Update the TUI approval overlay to build its buttons from the ordered
decision list, while preserving the legacy fallback when
`available_decisions` is missing.
- Update the app-server README, test client output, and generated schema
artifacts to document and surface the new field.

## Testing

- Add `approval_overlay.rs` coverage for explicit decision lists,
including the generic `ApprovedForSession` path and network approval
options.
- Update `chatwidget/tests.rs` and app-server protocol tests to populate
the new optional field and keep older event shapes working.

## Developers Docs

- If we document `item/commandExecution/requestApproval` on
[developers.openai.com/codex](https://developers.openai.com/codex), add
experimental `availableDecisions` as the preferred source of approval
choices and note that older servers may omit it.
This commit is contained in:
Michael Bolin
2026-02-26 01:10:46 +00:00
committed by GitHub
parent 4f45668106
commit 14116ade8d
31 changed files with 695 additions and 286 deletions
+1
View File
@@ -3569,6 +3569,7 @@ mod tests {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: Vec::new(),
},
),
@@ -402,6 +402,7 @@ mod tests {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: Vec::new(),
},
),
@@ -544,6 +545,7 @@ mod tests {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: Vec::new(),
},
),
@@ -622,6 +624,7 @@ mod tests {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: Vec::new(),
},
),
+233 -261
View File
@@ -20,10 +20,8 @@ use codex_core::features::Features;
use codex_protocol::mcp::RequestId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::ElicitationAction;
use codex_protocol::protocol::ExecPolicyAmendment;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NetworkPolicyAmendment;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
@@ -46,9 +44,8 @@ pub(crate) enum ApprovalRequest {
id: String,
command: Vec<String>,
reason: Option<String>,
available_decisions: Vec<ReviewDecision>,
network_approval_context: Option<NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
additional_permissions: Option<PermissionProfile>,
},
ApplyPatch {
@@ -67,7 +64,6 @@ pub(crate) enum ApprovalRequest {
/// Modal overlay asking the user to approve or deny one or more requests.
pub(crate) struct ApprovalOverlay {
current_request: Option<ApprovalRequest>,
current_variant: Option<ApprovalVariant>,
queue: Vec<ApprovalRequest>,
app_event_tx: AppEventSender,
list: ListSelectionView,
@@ -81,7 +77,6 @@ impl ApprovalOverlay {
pub fn new(request: ApprovalRequest, app_event_tx: AppEventSender, features: Features) -> Self {
let mut view = Self {
current_request: None,
current_variant: None,
queue: Vec::new(),
app_event_tx: app_event_tx.clone(),
list: ListSelectionView::new(Default::default(), app_event_tx),
@@ -99,31 +94,28 @@ impl ApprovalOverlay {
}
fn set_current(&mut self, request: ApprovalRequest) {
self.current_request = Some(request.clone());
let ApprovalRequestState { variant, header } = ApprovalRequestState::from(request);
self.current_variant = Some(variant.clone());
self.current_complete = false;
let (options, params) = Self::build_options(variant, header, &self.features);
let header = build_header(&request);
let (options, params) = Self::build_options(&request, header, &self.features);
self.current_request = Some(request);
self.options = options;
self.list = ListSelectionView::new(params, self.app_event_tx.clone());
}
fn build_options(
variant: ApprovalVariant,
request: &ApprovalRequest,
header: Box<dyn Renderable>,
_features: &Features,
) -> (Vec<ApprovalOption>, SelectionViewParams) {
let (options, title) = match &variant {
ApprovalVariant::Exec {
let (options, title) = match request {
ApprovalRequest::Exec {
available_decisions,
network_approval_context,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
additional_permissions,
..
} => (
exec_options(
proposed_execpolicy_amendment.clone(),
proposed_network_policy_amendments.clone(),
available_decisions,
network_approval_context.as_ref(),
additional_permissions.as_ref(),
),
@@ -137,11 +129,11 @@ impl ApprovalOverlay {
},
),
),
ApprovalVariant::ApplyPatch { .. } => (
ApprovalRequest::ApplyPatch { .. } => (
patch_options(),
"Would you like to make the following edits?".to_string(),
),
ApprovalVariant::McpElicitation { server_name, .. } => (
ApprovalRequest::McpElicitation { server_name, .. } => (
elicitation_options(),
format!("{server_name} needs your approval."),
),
@@ -188,18 +180,19 @@ impl ApprovalOverlay {
let Some(option) = self.options.get(actual_idx) else {
return;
};
if let Some(variant) = self.current_variant.as_ref() {
match (variant, &option.decision) {
(ApprovalVariant::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => {
if let Some(request) = self.current_request.as_ref() {
match (request, &option.decision) {
(ApprovalRequest::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => {
self.handle_exec_decision(id, command, decision.clone());
}
(ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
(ApprovalRequest::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
self.handle_patch_decision(id, decision.clone());
}
(
ApprovalVariant::McpElicitation {
ApprovalRequest::McpElicitation {
server_name,
request_id,
..
},
ApprovalDecision::McpElicitation(decision),
) => {
@@ -300,18 +293,19 @@ impl BottomPaneView for ApprovalOverlay {
return CancellationEvent::Handled;
}
if !self.current_complete
&& let Some(variant) = self.current_variant.as_ref()
&& let Some(request) = self.current_request.as_ref()
{
match &variant {
ApprovalVariant::Exec { id, command, .. } => {
match request {
ApprovalRequest::Exec { id, command, .. } => {
self.handle_exec_decision(id, command, ReviewDecision::Abort);
}
ApprovalVariant::ApplyPatch { id, .. } => {
ApprovalRequest::ApplyPatch { id, .. } => {
self.handle_patch_decision(id, ReviewDecision::Abort);
}
ApprovalVariant::McpElicitation {
ApprovalRequest::McpElicitation {
server_name,
request_id,
..
} => {
self.handle_elicitation_decision(
server_name,
@@ -353,122 +347,77 @@ impl Renderable for ApprovalOverlay {
}
}
struct ApprovalRequestState {
variant: ApprovalVariant,
header: Box<dyn Renderable>,
}
impl From<ApprovalRequest> for ApprovalRequestState {
fn from(value: ApprovalRequest) -> Self {
match value {
ApprovalRequest::Exec {
id,
command,
reason,
network_approval_context,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
additional_permissions,
} => {
let mut header: Vec<Line<'static>> = Vec::new();
if let Some(reason) = reason {
header.push(Line::from(vec!["Reason: ".into(), reason.italic()]));
header.push(Line::from(""));
}
if let Some(ref additional_permissions) = additional_permissions
&& let Some(rule_line) =
format_additional_permissions_rule(additional_permissions)
{
header.push(Line::from(vec![
"Permission rule: ".into(),
rule_line.cyan(),
]));
header.push(Line::from(""));
}
let full_cmd = strip_bash_lc_and_escape(&command);
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
if let Some(first) = full_cmd_lines.first_mut() {
first.spans.insert(0, Span::from("$ "));
}
if network_approval_context.is_none() {
header.extend(full_cmd_lines);
}
Self {
variant: ApprovalVariant::Exec {
id,
command,
network_approval_context,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
additional_permissions,
},
header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })),
}
fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
match request {
ApprovalRequest::Exec {
reason,
command,
network_approval_context,
additional_permissions,
..
} => {
let mut header: Vec<Line<'static>> = Vec::new();
if let Some(reason) = reason {
header.push(Line::from(vec!["Reason: ".into(), reason.clone().italic()]));
header.push(Line::from(""));
}
ApprovalRequest::ApplyPatch {
id,
reason,
cwd,
changes,
} => {
let mut header: Vec<Box<dyn Renderable>> = Vec::new();
if let Some(reason) = reason
&& !reason.is_empty()
{
header.push(Box::new(
Paragraph::new(Line::from_iter(["Reason: ".into(), reason.italic()]))
.wrap(Wrap { trim: false }),
));
header.push(Box::new(Line::from("")));
}
header.push(DiffSummary::new(changes, cwd).into());
Self {
variant: ApprovalVariant::ApplyPatch { id },
header: Box::new(ColumnRenderable::with(header)),
}
if let Some(additional_permissions) = additional_permissions
&& let Some(rule_line) = format_additional_permissions_rule(additional_permissions)
{
header.push(Line::from(vec![
"Permission rule: ".into(),
rule_line.cyan(),
]));
header.push(Line::from(""));
}
ApprovalRequest::McpElicitation {
server_name,
request_id,
message,
} => {
let header = Paragraph::new(vec![
Line::from(vec!["Server: ".into(), server_name.clone().bold()]),
Line::from(""),
Line::from(message),
])
.wrap(Wrap { trim: false });
Self {
variant: ApprovalVariant::McpElicitation {
server_name,
request_id,
},
header: Box::new(header),
}
let full_cmd = strip_bash_lc_and_escape(command);
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
if let Some(first) = full_cmd_lines.first_mut() {
first.spans.insert(0, Span::from("$ "));
}
if network_approval_context.is_none() {
header.extend(full_cmd_lines);
}
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
}
ApprovalRequest::ApplyPatch {
reason,
cwd,
changes,
..
} => {
let mut header: Vec<Box<dyn Renderable>> = Vec::new();
if let Some(reason) = reason
&& !reason.is_empty()
{
header.push(Box::new(
Paragraph::new(Line::from_iter([
"Reason: ".into(),
reason.clone().italic(),
]))
.wrap(Wrap { trim: false }),
));
header.push(Box::new(Line::from("")));
}
header.push(DiffSummary::new(changes.clone(), cwd.clone()).into());
Box::new(ColumnRenderable::with(header))
}
ApprovalRequest::McpElicitation {
server_name,
message,
..
} => {
let header = Paragraph::new(vec![
Line::from(vec!["Server: ".into(), server_name.clone().bold()]),
Line::from(""),
Line::from(message.clone()),
])
.wrap(Wrap { trim: false });
Box::new(header)
}
}
}
#[derive(Clone)]
enum ApprovalVariant {
Exec {
id: String,
command: Vec<String>,
network_approval_context: Option<NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
additional_permissions: Option<PermissionProfile>,
},
ApplyPatch {
id: String,
},
McpElicitation {
server_name: String,
request_id: RequestId,
},
}
#[derive(Clone)]
enum ApprovalDecision {
Review(ReviewDecision),
@@ -492,100 +441,93 @@ impl ApprovalOption {
}
fn exec_options(
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
available_decisions: &[ReviewDecision],
network_approval_context: Option<&NetworkApprovalContext>,
additional_permissions: Option<&PermissionProfile>,
) -> Vec<ApprovalOption> {
if network_approval_context.is_some() {
let mut options = vec![
ApprovalOption {
label: "Yes, just this once".to_string(),
available_decisions
.iter()
.filter_map(|decision| match decision {
ReviewDecision::Approved => Some(ApprovalOption {
label: if network_approval_context.is_some() {
"Yes, just this once".to_string()
} else {
"Yes, proceed".to_string()
},
decision: ApprovalDecision::Review(ReviewDecision::Approved),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
},
ApprovalOption {
label: "Yes, and allow this host for this conversation".to_string(),
}),
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment,
} => {
let rendered_prefix =
strip_bash_lc_and_escape(proposed_execpolicy_amendment.command());
if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') {
return None;
}
Some(ApprovalOption {
label: format!(
"Yes, and don't ask again for commands that start with `{rendered_prefix}`"
),
decision: ApprovalDecision::Review(
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: proposed_execpolicy_amendment.clone(),
},
),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))],
})
}
ReviewDecision::ApprovedForSession => Some(ApprovalOption {
label: if network_approval_context.is_some() {
"Yes, and allow this host for this conversation".to_string()
} else if additional_permissions.is_some() {
"Yes, and allow these permissions for this session".to_string()
} else {
"Yes, and don't ask again for this command in this session".to_string()
},
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))],
},
];
for amendment in proposed_network_policy_amendments.unwrap_or_default() {
let (label, shortcut) = match amendment.action {
NetworkPolicyRuleAction::Allow => (
"Yes, and allow this host in the future".to_string(),
KeyCode::Char('p'),
),
NetworkPolicyRuleAction::Deny => continue,
};
options.push(ApprovalOption {
label,
decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: amendment,
}),
}),
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => {
let (label, shortcut) = match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => (
"Yes, and allow this host in the future".to_string(),
KeyCode::Char('p'),
),
NetworkPolicyRuleAction::Deny => (
"No, and block this host in the future".to_string(),
KeyCode::Char('d'),
),
};
Some(ApprovalOption {
label,
decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.clone(),
}),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(shortcut)],
})
}
ReviewDecision::Denied => Some(ApprovalOption {
label: "No, continue without running it".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Denied),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(shortcut)],
});
}
options.push(ApprovalOption {
label: "No, and tell Codex what to do differently".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Abort),
display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
});
return options;
}
if additional_permissions.is_some() {
return vec![
ApprovalOption {
label: "Yes, proceed".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Approved),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
},
ApprovalOption {
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('d'))],
}),
ReviewDecision::Abort => Some(ApprovalOption {
label: "No, and tell Codex what to do differently".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Abort),
display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
},
];
}
vec![ApprovalOption {
label: "Yes, proceed".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Approved),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
}]
.into_iter()
.chain(proposed_execpolicy_amendment.and_then(|prefix| {
let rendered_prefix = strip_bash_lc_and_escape(prefix.command());
if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') {
return None;
}
Some(ApprovalOption {
label: format!(
"Yes, and don't ask again for commands that start with `{rendered_prefix}`"
),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: prefix,
}),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))],
})
}))
.chain([ApprovalOption {
label: "No, and tell Codex what to do differently".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Abort),
display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
}])
.collect()
.collect()
}
fn format_additional_permissions_rule(
@@ -669,7 +611,9 @@ mod tests {
use super::*;
use crate::app_event::AppEvent;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::protocol::ExecPolicyAmendment;
use codex_protocol::protocol::NetworkApprovalProtocol;
use codex_protocol::protocol::NetworkPolicyAmendment;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc::unbounded_channel;
@@ -695,9 +639,8 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: Some("reason".to_string()),
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
}
}
@@ -740,11 +683,16 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string()],
reason: None,
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![
"echo".to_string(),
]),
},
ReviewDecision::Abort,
],
network_approval_context: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".to_string(),
])),
proposed_network_policy_amendments: None,
additional_permissions: None,
},
tx,
@@ -781,21 +729,21 @@ mod tests {
id: "test".to_string(),
command: vec!["curl".to_string(), "https://example.com".to_string()],
reason: None,
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
ReviewDecision::Abort,
],
network_approval_context: Some(NetworkApprovalContext {
host: "example.com".to_string(),
protocol: NetworkApprovalProtocol::Https,
}),
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: Some(vec![
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Deny,
},
]),
additional_permissions: None,
},
tx,
@@ -818,9 +766,8 @@ mod tests {
id: "test".into(),
command,
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
};
@@ -850,17 +797,17 @@ mod tests {
protocol: NetworkApprovalProtocol::Https,
};
let options = exec_options(
Some(ExecPolicyAmendment::new(vec!["curl".to_string()])),
Some(vec![
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
&[
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Deny,
},
]),
ReviewDecision::Abort,
],
Some(&network_context),
None,
);
@@ -877,6 +824,29 @@ mod tests {
);
}
#[test]
fn generic_exec_options_can_offer_allow_for_session() {
let options = exec_options(
&[
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::Abort,
],
None,
None,
);
let labels: Vec<String> = options.into_iter().map(|option| option.label).collect();
assert_eq!(
labels,
vec![
"Yes, proceed".to_string(),
"Yes, and don't ask again for this command in this session".to_string(),
"No, and tell Codex what to do differently".to_string(),
]
);
}
#[test]
fn additional_permissions_exec_options_hide_execpolicy_amendment() {
let additional_permissions = PermissionProfile {
@@ -886,7 +856,11 @@ mod tests {
}),
..Default::default()
};
let options = exec_options(None, None, None, Some(&additional_permissions));
let options = exec_options(
&[ReviewDecision::Approved, ReviewDecision::Abort],
None,
Some(&additional_permissions),
);
let labels: Vec<String> = options.into_iter().map(|option| option.label).collect();
assert_eq!(
@@ -906,9 +880,8 @@ mod tests {
id: "test".into(),
command: vec!["cat".into(), "/tmp/readme.txt".into()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: Some(PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![PathBuf::from("/tmp/readme.txt")]),
@@ -946,9 +919,8 @@ mod tests {
id: "test".into(),
command: vec!["cat".into(), "/tmp/readme.txt".into()],
reason: Some("need filesystem access".into()),
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: Some(PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![PathBuf::from("/tmp/readme.txt")]),
@@ -973,21 +945,21 @@ mod tests {
id: "test".into(),
command: vec!["curl".into(), "https://example.com".into()],
reason: Some("network request blocked".into()),
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
ReviewDecision::Abort,
],
network_approval_context: Some(NetworkApprovalContext {
host: "example.com".to_string(),
protocol: NetworkApprovalProtocol::Https,
}),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec!["curl".into()])),
proposed_network_policy_amendments: Some(vec![
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Deny,
},
]),
additional_permissions: None,
};
+4 -2
View File
@@ -1113,9 +1113,11 @@ mod tests {
id: "1".to_string(),
command: vec!["echo".into(), "ok".into()],
reason: None,
available_decisions: vec![
codex_protocol::protocol::ReviewDecision::Approved,
codex_protocol::protocol::ReviewDecision::Abort,
],
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
}
}
+2 -2
View File
@@ -2568,13 +2568,13 @@ impl ChatWidget {
.unwrap_or_else(|_| ev.command.join(" "));
self.notify(Notification::ExecApprovalRequested { command });
let available_decisions = ev.effective_available_decisions();
let request = ApprovalRequest::Exec {
id: ev.effective_approval_id(),
command: ev.command,
reason: ev.reason,
available_decisions,
network_approval_context: ev.network_approval_context,
proposed_execpolicy_amendment: ev.proposed_execpolicy_amendment,
proposed_network_policy_amendments: ev.proposed_network_policy_amendments,
additional_permissions: ev.additional_permissions,
};
self.bottom_pane
+8
View File
@@ -2790,6 +2790,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2839,6 +2840,7 @@ async fn exec_approval_uses_approval_id_when_present() {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
}),
});
@@ -2875,6 +2877,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2929,6 +2932,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -6748,6 +6752,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
])),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -6808,6 +6813,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
])),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -6855,6 +6861,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -7221,6 +7228,7 @@ async fn status_widget_and_approval_modal_snapshot() {
])),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {