Compare commits

...

1 Commits

Author SHA1 Message Date
Owen Lin
57454f6e84 feat(protocol): plumb approval_id through exec approval flow 2026-02-17 12:05:42 -08:00
21 changed files with 143 additions and 26 deletions

View File

@@ -113,6 +113,10 @@
}
},
"properties": {
"approvalId": {
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is the same as `itemId` (one approval callback per command item).\n\nFor zsh-sidecar subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
"type": "string"
},
"command": {
"description": "The command to be executed.",
"type": [
@@ -165,6 +169,7 @@
}
},
"required": [
"approvalId",
"itemId",
"threadId",
"turnId"

View File

@@ -1423,8 +1423,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -1486,6 +1490,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",
@@ -6328,8 +6333,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -6391,6 +6400,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -2044,8 +2044,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -2107,6 +2111,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -179,6 +179,10 @@
},
"CommandExecutionRequestApprovalParams": {
"properties": {
"approvalId": {
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is the same as `itemId` (one approval callback per command item).\n\nFor zsh-sidecar subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
"type": "string"
},
"command": {
"description": "The command to be executed.",
"type": [
@@ -231,6 +235,7 @@
}
},
"required": [
"approvalId",
"itemId",
"threadId",
"turnId"

View File

@@ -2051,6 +2051,10 @@
"CommandExecutionRequestApprovalParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"approvalId": {
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is the same as `itemId` (one approval callback per command item).\n\nFor zsh-sidecar subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
"type": "string"
},
"command": {
"description": "The command to be executed.",
"type": [
@@ -2103,6 +2107,7 @@
}
},
"required": [
"approvalId",
"itemId",
"threadId",
"turnId"
@@ -3436,8 +3441,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -3499,6 +3508,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -1423,8 +1423,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -1486,6 +1490,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -1423,8 +1423,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -1486,6 +1490,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -1423,8 +1423,12 @@
},
{
"properties": {
"approval_id": {
"description": "Identifier for this specific approval callback.",
"type": "string"
},
"call_id": {
"description": "Identifier for the associated exec call, if available.",
"description": "Identifier for the associated command execution item.",
"type": "string"
},
"command": {
@@ -1486,6 +1490,7 @@
}
},
"required": [
"approval_id",
"call_id",
"command",
"cwd",

View File

@@ -7,9 +7,13 @@ import type { ParsedCommand } from "./ParsedCommand";
export type ExecApprovalRequestEvent = {
/**
* Identifier for the associated exec call, if available.
* Identifier for the associated command execution item.
*/
call_id: string,
/**
* Identifier for this specific approval callback.
*/
approval_id: string,
/**
* Turn ID that this command belongs to.
* Uses `#[serde(default)]` for backwards compatibility.

View File

@@ -5,6 +5,17 @@ import type { CommandAction } from "./CommandAction";
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
/**
* Unique identifier for this specific approval callback.
*
* For regular shell/unified_exec approvals, this is the same as
* `itemId` (one approval callback per command item).
*
* For zsh-sidecar subcommand approvals, multiple callbacks can belong to
* one parent `itemId`, so `approvalId` is a distinct opaque callback id
* (a UUID) used to disambiguate routing.
*/
approvalId: string,
/**
* Optional explanatory reason (e.g. request for network access).
*/

View File

@@ -3079,11 +3079,19 @@ pub struct CommandExecutionRequestApprovalParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
/// Unique identifier for this specific approval callback.
///
/// For regular shell/unified_exec approvals, this is the same as
/// `itemId` (one approval callback per command item).
///
/// For zsh-sidecar subcommand approvals, multiple callbacks can belong to
/// one parent `itemId`, so `approvalId` is a distinct opaque callback id
/// (a UUID) used to disambiguate routing.
pub approval_id: String,
/// Optional explanatory reason (e.g. request for network access).
#[ts(optional = nullable)]
pub reason: Option<String>,
/// The command to be executed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub command: Option<String>,
/// The command's working directory.

View File

@@ -232,6 +232,7 @@ pub(crate) async fn apply_bespoke_event_handling(
},
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id,
approval_id,
turn_id,
command,
cwd,
@@ -243,7 +244,7 @@ pub(crate) async fn apply_bespoke_event_handling(
ApiVersion::V1 => {
let params = ExecCommandApprovalParams {
conversation_id,
call_id: call_id.clone(),
call_id: approval_id.clone(),
command,
cwd,
reason,
@@ -253,11 +254,10 @@ pub(crate) async fn apply_bespoke_event_handling(
.send_request(ServerRequestPayload::ExecCommandApproval(params))
.await;
tokio::spawn(async move {
on_exec_approval_response(call_id, event_turn_id, rx, conversation).await;
on_exec_approval_response(approval_id, event_turn_id, rx, conversation).await;
});
}
ApiVersion::V2 => {
let item_id = call_id.clone();
let command_actions = parsed_cmd
.iter()
.cloned()
@@ -270,9 +270,8 @@ pub(crate) async fn apply_bespoke_event_handling(
let params = CommandExecutionRequestApprovalParams {
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
// Until we migrate the core to be aware of a first class CommandExecutionItem
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
item_id: item_id.clone(),
item_id: call_id.clone(),
approval_id: approval_id.clone(),
reason,
command: Some(command_string.clone()),
cwd: Some(cwd.clone()),
@@ -288,13 +287,15 @@ pub(crate) async fn apply_bespoke_event_handling(
on_command_execution_request_approval_response(
event_turn_id,
conversation_id,
item_id,
approval_id,
call_id,
command_string,
cwd,
command_actions,
rx,
conversation,
outgoing,
thread_state.clone(),
)
.await;
});
@@ -949,6 +950,14 @@ pub(crate) async fn apply_bespoke_event_handling(
let cwd = exec_command_begin_event.cwd;
let process_id = exec_command_begin_event.process_id;
{
let mut state = thread_state.lock().await;
state
.turn_summary
.command_execution_started
.insert(item_id.clone());
}
let item = ThreadItem::CommandExecution {
id: item_id,
command,
@@ -1036,6 +1045,14 @@ pub(crate) async fn apply_bespoke_event_handling(
..
} = exec_command_end_event;
{
let mut state = thread_state.lock().await;
state
.turn_summary
.command_execution_started
.remove(&call_id);
}
let status: CommandExecutionStatus = (&status).into();
let command_actions = parsed_cmd
.into_iter()
@@ -1767,6 +1784,7 @@ async fn on_file_change_request_approval_response(
async fn on_command_execution_request_approval_response(
event_turn_id: String,
conversation_id: ThreadId,
approval_id: String,
item_id: String,
command: String,
cwd: PathBuf,
@@ -1774,6 +1792,7 @@ async fn on_command_execution_request_approval_response(
receiver: oneshot::Receiver<ClientRequestResult>,
conversation: Arc<CodexThread>,
outgoing: ThreadScopedOutgoingMessageSender,
thread_state: Arc<Mutex<ThreadState>>,
) {
let response = receiver.await;
let (decision, completion_status) = match response {
@@ -1822,7 +1841,24 @@ async fn on_command_execution_request_approval_response(
}
};
if let Some(status) = completion_status {
let suppress_subcommand_completion_item = {
// For regular shell/unified_exec approvals (non-zsh-fork), approvals are
// command-level so approval_id == item_id. For zsh-fork subcommand
// approvals, approval_id is distinct and item_id is the parent command.
if approval_id != item_id {
let state = thread_state.lock().await;
state
.turn_summary
.command_execution_started
.contains(&item_id)
} else {
false
}
};
if let Some(status) = completion_status
&& !suppress_subcommand_completion_item
{
complete_command_execution_item(
conversation_id,
event_turn_id.clone(),
@@ -1839,7 +1875,7 @@ async fn on_command_execution_request_approval_response(
if let Err(err) = conversation
.submit(Op::ExecApproval {
id: item_id,
id: approval_id,
turn_id: Some(event_turn_id),
decision,
})

View File

@@ -20,6 +20,7 @@ type PendingInterruptQueue = Vec<(
#[derive(Default, Clone)]
pub(crate) struct TurnSummary {
pub(crate) file_change_started: HashSet<String>,
pub(crate) command_execution_started: HashSet<String>,
pub(crate) last_error: Option<TurnError>,
}

View File

@@ -2283,7 +2283,7 @@ impl Session {
/// Emit an exec approval request event and await the user's decision.
///
/// The request is keyed by `call_id` so matching responses are delivered
/// The request is keyed by `approval_id` so matching responses are delivered
/// to the correct in-flight turn. If the task is aborted, this returns the
/// default `ReviewDecision` (`Denied`).
#[allow(clippy::too_many_arguments)]
@@ -2291,6 +2291,7 @@ impl Session {
&self,
turn_context: &TurnContext,
call_id: String,
approval_id: String,
command: Vec<String>,
cwd: PathBuf,
reason: Option<String>,
@@ -2299,7 +2300,6 @@ impl Session {
) -> ReviewDecision {
// Add the tx_approve callback to the map before sending the request.
let (tx_approve, rx_approve) = oneshot::channel();
let approval_id = call_id.clone();
let prev_entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
@@ -2317,6 +2317,7 @@ impl Session {
let parsed_cmd = parse_command(&command);
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id,
approval_id,
turn_id: turn_context.sub_id.clone(),
command,
cwd,

View File

@@ -313,6 +313,7 @@ async fn handle_exec_approval(
) {
let ExecApprovalRequestEvent {
call_id,
approval_id,
command,
cwd,
reason,
@@ -320,11 +321,11 @@ async fn handle_exec_approval(
proposed_execpolicy_amendment,
..
} = event;
let approval_id = call_id.clone();
// Race approval with cancellation and timeout to avoid hangs.
let approval_fut = parent_session.request_command_approval(
parent_ctx,
call_id,
approval_id.clone(),
command,
cwd,
reason,

View File

@@ -247,6 +247,7 @@ impl NetworkApprovalService {
.request_command_approval(
turn_context.as_ref(),
attempt.call_id.clone(),
attempt.call_id.clone(),
attempt.command.clone(),
attempt.cwd.clone(),
Some(format!(

View File

@@ -108,6 +108,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
session
.request_command_approval(
turn,
call_id.clone(),
call_id,
command,
cwd,

View File

@@ -109,6 +109,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
session
.request_command_approval(
turn,
call_id.clone(),
call_id,
command,
cwd,

View File

@@ -1603,7 +1603,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
}
test.codex
.submit(Op::ExecApproval {
id: approval.call_id,
id: approval.approval_id,
turn_id: None,
decision: decision.clone(),
})
@@ -1823,7 +1823,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
test.codex
.submit(Op::ExecApproval {
id: approval.call_id,
id: approval.approval_id,
turn_id: None,
decision: ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: expected_execpolicy_amendment.clone(),
@@ -1988,7 +1988,7 @@ async fn compound_command_with_one_safe_command_still_requires_approval() -> Res
let approval = expect_exec_approval(&test, expected_command.as_str()).await;
test.codex
.submit(Op::ExecApproval {
id: approval.call_id,
id: approval.approval_id,
turn_id: None,
decision: ReviewDecision::Denied,
})

View File

@@ -100,7 +100,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
// Approve via parent using the emitted approval call ID.
test.codex
.submit(Op::ExecApproval {
id: approval.call_id,
id: approval.approval_id,
turn_id: None,
decision: ReviewDecision::Approved,
})

View File

@@ -57,8 +57,10 @@ pub struct NetworkApprovalContext {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecApprovalRequestEvent {
/// Identifier for the associated exec call, if available.
/// Identifier for the associated command execution item.
pub call_id: String,
/// Identifier for this specific approval callback.
pub approval_id: String,
/// Turn ID that this command belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]