diff --git a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json index 84842fd194..5492ed8c0a 100644 --- a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json @@ -33,6 +33,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User has approved this command and wants to execute a different argv vector instead of the original one-time request.", + "properties": { + "approved_override_command": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "approved_override_command" + ], + "title": "ApprovedOverrideCommandReviewDecision", + "type": "object" + }, { "additionalProperties": false, "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.", diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 2c146b9522..893e3005d3 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -228,6 +228,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User approved the command, but wants to execute a different argv vector.", + "properties": { + "acceptWithOverrideCommand": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "acceptWithOverrideCommand" + ], + "title": "AcceptWithOverrideCommandCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json index 0b7986fba9..427a554e7a 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json @@ -10,6 +10,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User approved the command, but wants to execute a different argv vector.", + "properties": { + "acceptWithOverrideCommand": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "acceptWithOverrideCommand" + ], + "title": "AcceptWithOverrideCommandCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json index baedafa403..3776abf1ec 100644 --- a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json @@ -33,6 +33,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User has approved this command and wants to execute a different argv vector instead of the original one-time request.", + "properties": { + "approved_override_command": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "approved_override_command" + ], + "title": "ApprovedOverrideCommandReviewDecision", + "type": "object" + }, { "additionalProperties": false, "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.", diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index a00871971f..8b5e7ceeff 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -294,6 +294,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User approved the command, but wants to execute a different argv vector.", + "properties": { + "acceptWithOverrideCommand": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "acceptWithOverrideCommand" + ], + "title": "AcceptWithOverrideCommandCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 720f7b0e70..0d7dbc17f2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1639,6 +1639,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User approved the command, but wants to execute a different argv vector.", + "properties": { + "acceptWithOverrideCommand": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "acceptWithOverrideCommand" + ], + "title": "AcceptWithOverrideCommandCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ @@ -3392,6 +3417,31 @@ ], "type": "string" }, + { + "additionalProperties": false, + "description": "User has approved this command and wants to execute a different argv vector instead of the original one-time request.", + "properties": { + "approved_override_command": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "required": [ + "approved_override_command" + ], + "title": "ApprovedOverrideCommandReviewDecision", + "type": "object" + }, { "additionalProperties": false, "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.", diff --git a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts index b5193785d8..127b222c4a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts @@ -7,4 +7,4 @@ import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; /** * User's decision in response to an ExecApprovalRequest. */ -export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "abort"; +export type ReviewDecision = "approved" | { "approved_override_command": { command: Array, } } | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "abort"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts index c022030a1e..2e3ef76a04 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts @@ -4,4 +4,4 @@ import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | { "applyNetworkPolicyAmendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "decline" | "cancel"; +export type CommandExecutionApprovalDecision = "accept" | { "acceptWithOverrideCommand": { command: Array, } } | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | { "applyNetworkPolicyAmendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "decline" | "cancel"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a074ae6479..caadd4ac2d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -959,6 +959,8 @@ pub struct ConfigEdit { pub enum CommandExecutionApprovalDecision { /// User approved the command. Accept, + /// User approved the command, but wants to execute a different argv vector. + AcceptWithOverrideCommand { command: Vec }, /// User approved the command and future prompts in the same session-scoped /// approval cache should run without prompting. AcceptForSession, @@ -981,6 +983,9 @@ impl From for CommandExecutionApprovalDecision { fn from(value: CoreReviewDecision) -> Self { match value { CoreReviewDecision::Approved => Self::Accept, + CoreReviewDecision::ApprovedOverrideCommand { command } => { + Self::AcceptWithOverrideCommand { command } + } CoreReviewDecision::ApprovedExecpolicyAmendment { proposed_execpolicy_amendment, } => Self::AcceptWithExecpolicyAmendment { @@ -6033,6 +6038,27 @@ mod tests { ); } + #[test] + fn command_execution_request_approval_response_accepts_override_command() { + let response = serde_json::from_value::(json!({ + "decision": { + "acceptWithOverrideCommand": { + "command": ["echo", "hi"] + } + } + })) + .expect("override command response should deserialize"); + + assert_eq!( + response, + CommandExecutionRequestApprovalResponse { + decision: CommandExecutionApprovalDecision::AcceptWithOverrideCommand { + command: vec!["echo".to_string(), "hi".to_string()], + }, + } + ); + } + #[test] fn permissions_request_approval_response_accepts_partial_macos_grants() { let cases = vec![ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 4dcf93bc21..7368a25981 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -897,7 +897,7 @@ When an upstream HTTP status is available (for example, from the Responses API o Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing. - Requests include `threadId` and `turnId`—use them to scope UI state to the active conversation. -- Respond with a single `{ "decision": ... }` payload. Command approvals support `accept`, `acceptForSession`, `acceptWithExecpolicyAmendment`, `applyNetworkPolicyAmendment`, `decline`, or `cancel`. The server resumes or declines the work and ends the item with `item/completed`. +- Respond with a single `{ "decision": ... }` payload. Command approvals support `accept`, `acceptWithOverrideCommand`, `acceptForSession`, `acceptWithExecpolicyAmendment`, `applyNetworkPolicyAmendment`, `decline`, or `cancel`. The server resumes or declines the work and ends the item with `item/completed`. ### Command execution approvals @@ -905,7 +905,7 @@ 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; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. 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" }`. +3. Client response — for example `{ "decision": "accept" }`, `{ "decision": { "acceptWithOverrideCommand": { "command": ["echo", "hi"] } } }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`. 4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt. 5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 4afd4cc244..5628edc080 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2506,6 +2506,19 @@ async fn on_command_execution_request_approval_response( let (decision, completion_status) = match decision { CommandExecutionApprovalDecision::Accept => (ReviewDecision::Approved, None), + CommandExecutionApprovalDecision::AcceptWithOverrideCommand { command } => { + if command.is_empty() { + error!( + "failed to deserialize CommandExecutionRequestApprovalResponse: override command cannot be empty" + ); + ( + ReviewDecision::Denied, + Some(CommandExecutionStatus::Declined), + ) + } else { + (ReviewDecision::ApprovedOverrideCommand { command }, None) + } + } CommandExecutionApprovalDecision::AcceptForSession => { (ReviewDecision::ApprovedForSession, None) } diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 19982349be..9900fea43e 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -687,6 +687,7 @@ pub(crate) fn build_guardian_mcp_tool_review_request( fn mcp_tool_approval_decision_from_guardian(decision: ReviewDecision) -> McpToolApprovalDecision { match decision { ReviewDecision::Approved + | ReviewDecision::ApprovedOverrideCommand { .. } | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept, ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession, diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 12fb904f4d..b10758d2fc 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -215,6 +215,7 @@ impl ToolHandler for ApplyPatchHandler { turn: turn.clone(), call_id: call_id.clone(), tool_name: tool_name.to_string(), + command_override: None, }; let out = orchestrator .run( @@ -319,6 +320,7 @@ pub(crate) async fn intercept_apply_patch( turn: turn.clone(), call_id: call_id.to_string(), tool_name: tool_name.to_string(), + command_override: None, }; let out = orchestrator .run( diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index a3014a8ed4..5fe2546de2 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -467,6 +467,7 @@ impl ShellHandler { turn: turn.clone(), call_id: call_id.clone(), tool_name, + command_override: None, }; let out = orchestrator .run( diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 4f5eed182e..f27c761acb 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -389,7 +389,9 @@ impl NetworkApprovalService { let mut cache_session_deny = false; let resolved = match approval_decision { - ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => { + ReviewDecision::Approved + | ReviewDecision::ApprovedOverrideCommand { .. } + | ReviewDecision::ApprovedExecpolicyAmendment { .. } => { PendingApprovalDecision::AllowOnce } ReviewDecision::ApprovedForSession => PendingApprovalDecision::AllowForSession, diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index e41b90b4d3..ecfdd61065 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -52,6 +52,7 @@ impl ToolOrchestrator { req: &Rq, tool_ctx: &ToolCtx, attempt: &SandboxAttempt<'_>, + command_override: Option>, has_managed_network_requirements: bool, ) -> (Result, Option) where @@ -70,6 +71,7 @@ impl ToolOrchestrator { turn: tool_ctx.turn.clone(), call_id: tool_ctx.call_id.clone(), tool_name: tool_ctx.tool_name.clone(), + command_override, }; let run_result = tool.run(req, attempt, &attempt_tool_ctx).await; @@ -113,6 +115,7 @@ impl ToolOrchestrator { let otel_ci = &tool_ctx.call_id; let otel_user = ToolDecisionSource::User; let otel_cfg = ToolDecisionSource::Config; + let mut command_override: Option> = None; // 1) Approval let mut already_approved = false; @@ -134,10 +137,14 @@ impl ToolOrchestrator { call_id: &tool_ctx.call_id, retry_reason: reason, network_approval_context: None, + command_override: None, }; let decision = tool.start_approval_async(req, approval_ctx).await; otel.tool_decision(otel_tn, otel_ci, &decision, otel_user.clone()); + if let Some(command) = decision.override_command() { + command_override = Some(command.to_vec()); + } match decision { ReviewDecision::Denied | ReviewDecision::Abort => { @@ -149,6 +156,7 @@ impl ToolOrchestrator { return Err(ToolError::Rejected(reason)); } ReviewDecision::Approved + | ReviewDecision::ApprovedOverrideCommand { .. } | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::ApprovedForSession => {} ReviewDecision::NetworkPolicyAmendment { @@ -207,6 +215,7 @@ impl ToolOrchestrator { req, tool_ctx, &initial_attempt, + command_override.clone(), has_managed_network_requirements, ) .await; @@ -283,10 +292,14 @@ impl ToolOrchestrator { call_id: &tool_ctx.call_id, retry_reason: Some(retry_reason), network_approval_context: network_approval_context.clone(), + command_override: command_override.clone(), }; let decision = tool.start_approval_async(req, approval_ctx).await; otel.tool_decision(otel_tn, otel_ci, &decision, otel_user); + if let Some(command) = decision.override_command() { + command_override = Some(command.to_vec()); + } match decision { ReviewDecision::Denied | ReviewDecision::Abort => { @@ -298,6 +311,7 @@ impl ToolOrchestrator { return Err(ToolError::Rejected(reason)); } ReviewDecision::Approved + | ReviewDecision::ApprovedOverrideCommand { .. } | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::ApprovedForSession => {} ReviewDecision::NetworkPolicyAmendment { @@ -334,6 +348,7 @@ impl ToolOrchestrator { req, tool_ctx, &escalated_attempt, + command_override, has_managed_network_requirements, ) .await; diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index b2d6ef68bd..ba7ab83666 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -144,7 +144,10 @@ impl Approvable for ShellRuntime { ctx: ApprovalCtx<'a>, ) -> BoxFuture<'a, ReviewDecision> { let keys = self.approval_keys(req); - let command = req.command.clone(); + let command = ctx + .command_override + .clone() + .unwrap_or_else(|| req.command.clone()); let cwd = req.cwd.clone(); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); @@ -221,8 +224,9 @@ impl ToolRuntime for ShellRuntime { ctx: &ToolCtx, ) -> Result { let session_shell = ctx.session.user_shell(); + let base_command = ctx.command_override.as_ref().unwrap_or(&req.command); let command = maybe_wrap_shell_lc_with_snapshot( - &req.command, + base_command, session_shell.as_ref(), &req.cwd, &req.explicit_env_overrides, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 1ff4ef4fb6..95d31a2135 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -547,6 +547,12 @@ impl CoreShellActionProvider { EscalationDecision::run() } } + ReviewDecision::ApprovedOverrideCommand { .. } => { + EscalationDecision::deny(Some( + "command override is not supported for execve approvals" + .to_string(), + )) + } ReviewDecision::ApprovedForSession => { // Currently, we only add session approvals for // skill scripts because we are storing only the diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 7f0032c8d4..ce5d9a48b8 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -115,7 +115,10 @@ impl Approvable for UnifiedExecRuntime<'_> { let session = ctx.session; let turn = ctx.turn; let call_id = ctx.call_id.to_string(); - let command = req.command.clone(); + let command = ctx + .command_override + .clone() + .unwrap_or_else(|| req.command.clone()); let cwd = req.cwd.clone(); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); @@ -192,7 +195,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result { - let base_command = &req.command; + let base_command = ctx.command_override.as_ref().unwrap_or(&req.command); let session_shell = ctx.session.user_shell(); let command = maybe_wrap_shell_lc_with_snapshot( base_command, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index e8ff5a1b80..cae41c4c53 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -118,6 +118,7 @@ pub(crate) struct ApprovalCtx<'a> { pub call_id: &'a str, pub retry_reason: Option, pub network_approval_context: Option, + pub command_override: Option>, } // Specifies what tool orchestrator should do with a given tool call. @@ -301,6 +302,7 @@ pub(crate) struct ToolCtx { pub turn: Arc, pub call_id: String, pub tool_name: String, + pub command_override: Option>, } #[derive(Debug)] diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 95b328a827..a40f5fd7bb 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -618,6 +618,7 @@ impl UnifiedExecProcessManager { turn: context.turn.clone(), call_id: context.call_id.clone(), tool_name: "exec_command".to_string(), + command_override: None, }; orchestrator .run( diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 8ac5242dc5..f253cb5ff6 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3105,6 +3105,10 @@ pub enum ReviewDecision { /// User has approved this command and the agent should execute it. Approved, + /// User has approved this command and wants to execute a different argv + /// vector instead of the original one-time request. + ApprovedOverrideCommand { command: Vec }, + /// User has approved this command and wants to apply the proposed execpolicy /// amendment so future matching commands are permitted. ApprovedExecpolicyAmendment { @@ -3133,11 +3137,19 @@ pub enum ReviewDecision { } impl ReviewDecision { + pub fn override_command(&self) -> Option<&[String]> { + match self { + ReviewDecision::ApprovedOverrideCommand { command } => Some(command.as_slice()), + _ => None, + } + } + /// Returns an opaque version of the decision without PII. We can't use an ignored flag /// on `serde` because the serialization is required by some surfaces. pub fn to_opaque_string(&self) -> &'static str { match self { ReviewDecision::Approved => "approved", + ReviewDecision::ApprovedOverrideCommand { .. } => "approved_with_command_override", ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment", ReviewDecision::ApprovedForSession => "approved_for_session", ReviewDecision::NetworkPolicyAmendment {