mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
feat: include available decisions in command approval requests
This commit is contained in:
@@ -59,7 +59,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.",
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
|
||||
@@ -202,6 +202,85 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User approved the command.",
|
||||
"enum": [
|
||||
"accept"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
|
||||
"properties": {
|
||||
"acceptWithExecpolicyAmendment": {
|
||||
"properties": {
|
||||
"execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"acceptWithExecpolicyAmendment"
|
||||
],
|
||||
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
|
||||
"properties": {
|
||||
"applyNetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"applyNetworkPolicyAmendment"
|
||||
],
|
||||
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The agent will continue the turn.",
|
||||
"enum": [
|
||||
"decline"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The turn will also be immediately interrupted.",
|
||||
"enum": [
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsAutomationValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future identical commands should run without prompting.",
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
|
||||
@@ -1675,6 +1675,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"available_decisions": {
|
||||
"description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ReviewDecision"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
@@ -4995,6 +5005,86 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReviewDecision": {
|
||||
"description": "User's decision in response to an ExecApprovalRequest.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User has approved this command and the agent should execute it.",
|
||||
"enum": [
|
||||
"approved"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
|
||||
"properties": {
|
||||
"approved_execpolicy_amendment": {
|
||||
"properties": {
|
||||
"proposed_execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"proposed_execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"approved_execpolicy_amendment"
|
||||
],
|
||||
"title": "ApprovedExecpolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"title": "NetworkPolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
|
||||
"enum": [
|
||||
"denied"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not do anything until the user's next command.",
|
||||
"enum": [
|
||||
"abort"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReviewFinding": {
|
||||
"description": "A single review finding describing an observed issue or recommendation.",
|
||||
"properties": {
|
||||
@@ -7165,6 +7255,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"available_decisions": {
|
||||
"description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ReviewDecision"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.",
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
|
||||
@@ -268,6 +268,85 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User approved the command.",
|
||||
"enum": [
|
||||
"accept"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
|
||||
"properties": {
|
||||
"acceptWithExecpolicyAmendment": {
|
||||
"properties": {
|
||||
"execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"acceptWithExecpolicyAmendment"
|
||||
],
|
||||
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
|
||||
"properties": {
|
||||
"applyNetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"applyNetworkPolicyAmendment"
|
||||
],
|
||||
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The agent will continue the turn.",
|
||||
"enum": [
|
||||
"decline"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The turn will also be immediately interrupted.",
|
||||
"enum": [
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionRequestApprovalParams": {
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
|
||||
@@ -1442,7 +1442,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future identical commands should run without prompting.",
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
@@ -2843,6 +2843,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"available_decisions": {
|
||||
"description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ReviewDecision"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
@@ -5700,7 +5710,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.",
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
import type { ParsedCommand } from "./ParsedCommand";
|
||||
import type { PermissionProfile } from "./PermissionProfile";
|
||||
import type { ReviewDecision } from "./ReviewDecision";
|
||||
|
||||
export type ExecApprovalRequestEvent = {
|
||||
/**
|
||||
@@ -51,4 +52,11 @@ proposed_network_policy_amendments?: Array<NetworkPolicyAmendment>,
|
||||
/**
|
||||
* Optional additional filesystem permissions requested for this command.
|
||||
*/
|
||||
additional_permissions?: PermissionProfile, parsed_cmd: Array<ParsedCommand>, };
|
||||
additional_permissions?: PermissionProfile,
|
||||
/**
|
||||
* Ordered list of decisions the client may present for this prompt.
|
||||
*
|
||||
* When absent, clients should derive the legacy default set from the
|
||||
* other fields on this request.
|
||||
*/
|
||||
available_decisions?: Array<ReviewDecision>, parsed_cmd: Array<ParsedCommand>, };
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
import type { CommandAction } from "./CommandAction";
|
||||
import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision";
|
||||
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
@@ -49,4 +50,8 @@ proposedExecpolicyAmendment?: ExecPolicyAmendment | null,
|
||||
/**
|
||||
* Optional proposed network policy amendments (allow/deny host) for future requests.
|
||||
*/
|
||||
proposedNetworkPolicyAmendments?: Array<NetworkPolicyAmendment> | null, };
|
||||
proposedNetworkPolicyAmendments?: Array<NetworkPolicyAmendment> | null,
|
||||
/**
|
||||
* Ordered list of decisions the client may present for this prompt.
|
||||
*/
|
||||
availableDecisions?: Array<CommandExecutionApprovalDecision> | null, };
|
||||
|
||||
@@ -1545,6 +1545,7 @@ mod tests {
|
||||
}),
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
available_decisions: None,
|
||||
};
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¶ms);
|
||||
assert_eq!(
|
||||
|
||||
@@ -48,6 +48,7 @@ use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
|
||||
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
|
||||
use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame;
|
||||
use codex_protocol::protocol::RejectConfig as CoreRejectConfig;
|
||||
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
|
||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||
use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies;
|
||||
use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo;
|
||||
@@ -740,7 +741,8 @@ pub struct ConfigEdit {
|
||||
pub enum CommandExecutionApprovalDecision {
|
||||
/// User approved the command.
|
||||
Accept,
|
||||
/// User approved the command and future identical commands should run without prompting.
|
||||
/// User approved the command and future prompts in the same session-scoped
|
||||
/// approval cache should run without prompting.
|
||||
AcceptForSession,
|
||||
/// User approved the command, and wants to apply the proposed execpolicy amendment so future
|
||||
/// matching commands can run without prompting.
|
||||
@@ -757,6 +759,27 @@ pub enum CommandExecutionApprovalDecision {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v2_enum_from_core! {
|
||||
pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol {
|
||||
Http,
|
||||
@@ -3777,6 +3800,11 @@ pub struct CommandExecutionRequestApprovalParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
|
||||
/// Ordered list of decisions the client may present for this prompt.
|
||||
#[experimental("item/commandExecution/requestApproval.availableDecisions")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub available_decisions: Option<Vec<CommandExecutionApprovalDecision>>,
|
||||
}
|
||||
|
||||
impl CommandExecutionRequestApprovalParams {
|
||||
|
||||
@@ -1540,6 +1540,7 @@ impl CodexClient {
|
||||
additional_permissions,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
available_decisions,
|
||||
} = params;
|
||||
|
||||
println!(
|
||||
@@ -1554,6 +1555,9 @@ impl CodexClient {
|
||||
if let Some(network_approval_context) = network_approval_context.as_ref() {
|
||||
println!("< network approval context: {network_approval_context:?}");
|
||||
}
|
||||
if let Some(available_decisions) = available_decisions.as_ref() {
|
||||
println!("< available decisions: {available_decisions:?}");
|
||||
}
|
||||
if let Some(command) = command.as_deref() {
|
||||
println!("< command: {command}");
|
||||
}
|
||||
|
||||
@@ -710,7 +710,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap
|
||||
Order of messages:
|
||||
|
||||
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
|
||||
3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`.
|
||||
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
|
||||
|
||||
|
||||
@@ -336,6 +336,11 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.note_permission_requested(&conversation_id.to_string())
|
||||
.await;
|
||||
let approval_id_for_op = ev.effective_approval_id();
|
||||
let available_decisions = ev
|
||||
.effective_available_decisions()
|
||||
.into_iter()
|
||||
.map(CommandExecutionApprovalDecision::from)
|
||||
.collect::<Vec<_>>();
|
||||
let ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
@@ -432,6 +437,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
additional_permissions,
|
||||
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
|
||||
proposed_network_policy_amendments: proposed_network_policy_amendments_v2,
|
||||
available_decisions: Some(available_decisions),
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
|
||||
|
||||
@@ -986,6 +986,7 @@ mod tests {
|
||||
),
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
available_decisions: None,
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -1047,6 +1048,7 @@ mod tests {
|
||||
),
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
available_decisions: None,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -2589,9 +2589,13 @@ impl Session {
|
||||
|
||||
/// Emit an exec approval request event and await the user's decision.
|
||||
///
|
||||
/// The request is keyed by `call_id` + `approval_id` so matching responses are delivered
|
||||
/// to the correct in-flight turn. If the task is aborted, this returns the
|
||||
/// default `ReviewDecision` (`Denied`).
|
||||
/// The request is keyed by `call_id` + `approval_id` so matching responses
|
||||
/// are delivered to the correct in-flight turn. If the task is aborted,
|
||||
/// this returns the default `ReviewDecision` (`Denied`).
|
||||
///
|
||||
/// Note that if `available_decisions` is `None`, then the other fields will
|
||||
/// be used to derive the available decisions via
|
||||
/// [ExecApprovalRequestEvent::default_available_decisions].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn request_command_approval(
|
||||
&self,
|
||||
@@ -2604,6 +2608,7 @@ impl Session {
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
available_decisions: Option<Vec<ReviewDecision>>,
|
||||
) -> ReviewDecision {
|
||||
// command-level approvals use `call_id`.
|
||||
// `approval_id` is only present for subcommand callbacks (execve intercept)
|
||||
@@ -2637,6 +2642,14 @@ impl Session {
|
||||
},
|
||||
]
|
||||
});
|
||||
let available_decisions = available_decisions.unwrap_or_else(|| {
|
||||
ExecApprovalRequestEvent::default_available_decisions(
|
||||
network_approval_context.as_ref(),
|
||||
proposed_execpolicy_amendment.as_ref(),
|
||||
proposed_network_policy_amendments.as_deref(),
|
||||
additional_permissions.as_ref(),
|
||||
)
|
||||
});
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
@@ -2648,6 +2661,7 @@ impl Session {
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions,
|
||||
available_decisions: Some(available_decisions),
|
||||
parsed_cmd,
|
||||
});
|
||||
self.send_event(turn_context, event).await;
|
||||
|
||||
@@ -322,6 +322,7 @@ async fn handle_exec_approval(
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
..
|
||||
} = event;
|
||||
// Race approval with cancellation and timeout to avoid hangs.
|
||||
@@ -335,6 +336,7 @@ async fn handle_exec_approval(
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
);
|
||||
let decision = await_approval_with_cancel(
|
||||
approval_fut,
|
||||
|
||||
@@ -331,6 +331,7 @@ impl NetworkApprovalService {
|
||||
protocol,
|
||||
};
|
||||
|
||||
let available_decisions = None;
|
||||
let approval_decision = session
|
||||
.request_command_approval(
|
||||
turn_context.as_ref(),
|
||||
@@ -342,6 +343,7 @@ impl NetworkApprovalService {
|
||||
Some(network_approval_context.clone()),
|
||||
None,
|
||||
None,
|
||||
available_decisions,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
let call_id = ctx.call_id.to_string();
|
||||
Box::pin(async move {
|
||||
with_cached_approval(&session.services, "shell", keys, move || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
@@ -163,6 +164,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -194,6 +194,11 @@ impl CoreShellActionProvider {
|
||||
let approval_id = Some(Uuid::new_v4().to_string());
|
||||
Ok(stopwatch
|
||||
.pause_for(async move {
|
||||
let available_decisions = Some(vec![
|
||||
ReviewDecision::Approved,
|
||||
ReviewDecision::ApprovedForSession,
|
||||
ReviewDecision::Abort,
|
||||
]);
|
||||
session
|
||||
.request_command_approval(
|
||||
&turn,
|
||||
@@ -205,6 +210,7 @@ impl CoreShellActionProvider {
|
||||
None,
|
||||
None,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -111,6 +111,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
.or_else(|| req.justification.clone());
|
||||
Box::pin(async move {
|
||||
with_cached_approval(&session.services, "unified_exec", keys, || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
@@ -124,6 +125,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -336,7 +336,7 @@ fn handle_server_request(
|
||||
..
|
||||
} => {
|
||||
let response = codex_app_server_protocol::CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Decline,
|
||||
decision: CommandExecutionApprovalDecision::Cancel,
|
||||
};
|
||||
send_jsonrpc_response(stdin, request_id, response)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub fn start_reader(
|
||||
let command_decision = if auto_approve {
|
||||
CommandExecutionApprovalDecision::Accept
|
||||
} else {
|
||||
CommandExecutionApprovalDecision::Decline
|
||||
CommandExecutionApprovalDecision::Cancel
|
||||
};
|
||||
let file_decision = if auto_approve {
|
||||
FileChangeApprovalDecision::Accept
|
||||
|
||||
@@ -227,6 +227,7 @@ async fn run_codex_tool_session_inner(
|
||||
parsed_cmd,
|
||||
network_approval_context: _,
|
||||
additional_permissions: _,
|
||||
available_decisions: _,
|
||||
} = ev;
|
||||
handle_exec_approval_request(
|
||||
command,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::mcp::RequestId;
|
||||
use crate::models::PermissionProfile;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::protocol::FileChange;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -107,6 +108,13 @@ pub struct ExecApprovalRequestEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
/// Ordered list of decisions the client may present for this prompt.
|
||||
///
|
||||
/// When absent, clients should derive the legacy default set from the
|
||||
/// other fields on this request.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub available_decisions: Option<Vec<ReviewDecision>>,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
}
|
||||
|
||||
@@ -116,6 +124,55 @@ impl ExecApprovalRequestEvent {
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.call_id.clone())
|
||||
}
|
||||
|
||||
pub fn effective_available_decisions(&self) -> Vec<ReviewDecision> {
|
||||
// available_decisions is a new field that may not be populated by older
|
||||
// senders, so we fall back to the legacy logic if it's not present.
|
||||
match &self.available_decisions {
|
||||
Some(decisions) => decisions.clone(),
|
||||
None => Self::default_available_decisions(
|
||||
self.network_approval_context.as_ref(),
|
||||
self.proposed_execpolicy_amendment.as_ref(),
|
||||
self.proposed_network_policy_amendments.as_deref(),
|
||||
self.additional_permissions.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_available_decisions(
|
||||
network_approval_context: Option<&NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
|
||||
proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>,
|
||||
additional_permissions: Option<&PermissionProfile>,
|
||||
) -> Vec<ReviewDecision> {
|
||||
if network_approval_context.is_some() {
|
||||
let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession];
|
||||
if let Some(amendment) = proposed_network_policy_amendments.and_then(|amendments| {
|
||||
amendments
|
||||
.iter()
|
||||
.find(|amendment| amendment.action == NetworkPolicyRuleAction::Allow)
|
||||
}) {
|
||||
decisions.push(ReviewDecision::NetworkPolicyAmendment {
|
||||
network_policy_amendment: amendment.clone(),
|
||||
});
|
||||
}
|
||||
decisions.push(ReviewDecision::Abort);
|
||||
return decisions;
|
||||
}
|
||||
|
||||
if additional_permissions.is_some() {
|
||||
return vec![ReviewDecision::Approved, ReviewDecision::Abort];
|
||||
}
|
||||
|
||||
let mut decisions = vec![ReviewDecision::Approved];
|
||||
if let Some(prefix) = proposed_execpolicy_amendment {
|
||||
decisions.push(ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: prefix.clone(),
|
||||
});
|
||||
}
|
||||
decisions.push(ReviewDecision::Abort);
|
||||
decisions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
|
||||
@@ -2789,8 +2789,8 @@ pub enum ReviewDecision {
|
||||
proposed_execpolicy_amendment: ExecPolicyAmendment,
|
||||
},
|
||||
|
||||
/// User has approved this command and wants to automatically approve any
|
||||
/// future identical instances (`command` and `cwd` match exactly) for the
|
||||
/// User has approved this request and wants future prompts in the same
|
||||
/// session-scoped approval cache to be automatically approved for the
|
||||
/// remainder of the session.
|
||||
ApprovedForSession,
|
||||
|
||||
|
||||
@@ -3563,6 +3563,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(),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -46,6 +46,7 @@ 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>>,
|
||||
@@ -115,15 +116,13 @@ impl ApprovalOverlay {
|
||||
) -> (Vec<ApprovalOption>, SelectionViewParams) {
|
||||
let (options, title) = match &variant {
|
||||
ApprovalVariant::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(),
|
||||
),
|
||||
@@ -365,6 +364,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
|
||||
id,
|
||||
command,
|
||||
reason,
|
||||
available_decisions,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
@@ -397,6 +397,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
|
||||
variant: ApprovalVariant::Exec {
|
||||
id,
|
||||
command,
|
||||
available_decisions,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
@@ -451,11 +452,16 @@ impl From<ApprovalRequest> for ApprovalRequestState {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[allow(dead_code)]
|
||||
enum ApprovalVariant {
|
||||
Exec {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
available_decisions: Vec<ReviewDecision>,
|
||||
#[allow(dead_code)]
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
#[allow(dead_code)]
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
@@ -492,100 +498,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(
|
||||
@@ -695,6 +694,7 @@ 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,
|
||||
@@ -740,6 +740,15 @@ 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(),
|
||||
@@ -781,6 +790,17 @@ 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,
|
||||
@@ -818,6 +838,7 @@ 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,
|
||||
@@ -850,17 +871,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 +898,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 +930,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,6 +954,7 @@ 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,
|
||||
@@ -946,6 +995,7 @@ 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,
|
||||
@@ -973,6 +1023,17 @@ 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,
|
||||
|
||||
@@ -1117,6 +1117,10 @@ 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,
|
||||
|
||||
@@ -2568,10 +2568,12 @@ 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,
|
||||
|
||||
@@ -2784,6 +2784,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 {
|
||||
@@ -2833,6 +2834,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![],
|
||||
}),
|
||||
});
|
||||
@@ -2869,6 +2871,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 {
|
||||
@@ -2923,6 +2926,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 {
|
||||
@@ -6742,6 +6746,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 {
|
||||
@@ -6802,6 +6807,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 {
|
||||
@@ -6849,6 +6855,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 {
|
||||
@@ -7215,6 +7222,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 {
|
||||
|
||||
Reference in New Issue
Block a user