feat(core): plumb distinct approval ids for command approvals (#12051)

zsh fork PR stack:
- https://github.com/openai/codex/pull/12051 👈 
- https://github.com/openai/codex/pull/12052

With upcoming support for a fork of zsh that allows us to intercept
`execve` and run execpolicy checks for each subcommand as part of a
`CommandExecution`, it will be possible for there to be multiple
approval requests for a shell command like `/path/to/zsh -lc 'git status
&& rg \"TODO\" src && make test'`.

To support that, this PR introduces a new `approval_id` field across
core, protocol, and app-server so that we can associate approvals
properly for subcommands.
This commit is contained in:
Owen Lin
2026-02-17 17:55:57 -08:00
committed by GitHub
parent b3a8571219
commit db4d2599b5
33 changed files with 331 additions and 114 deletions

View File

@@ -214,76 +214,88 @@ pub(crate) async fn apply_bespoke_event_handling(
});
}
},
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id,
turn_id,
command,
cwd,
reason,
proposed_execpolicy_amendment,
parsed_cmd,
..
}) => match api_version {
ApiVersion::V1 => {
let params = ExecCommandApprovalParams {
conversation_id,
call_id: call_id.clone(),
command,
cwd,
reason,
parsed_cmd,
};
let rx = outgoing
.send_request(ServerRequestPayload::ExecCommandApproval(params))
.await;
tokio::spawn(async move {
on_exec_approval_response(call_id, event_turn_id, rx, conversation).await;
});
}
ApiVersion::V2 => {
let item_id = call_id.clone();
let command_actions = parsed_cmd
.iter()
.cloned()
.map(V2ParsedCommand::from)
.collect::<Vec<_>>();
let command_string = shlex_join(&command);
let proposed_execpolicy_amendment_v2 =
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
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(),
reason,
command: Some(command_string.clone()),
cwd: Some(cwd.clone()),
command_actions: Some(command_actions.clone()),
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
};
let rx = outgoing
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
params,
))
.await;
tokio::spawn(async move {
on_command_execution_request_approval_response(
event_turn_id,
EventMsg::ExecApprovalRequest(ev) => {
let approval_id_for_op = ev.effective_approval_id();
let ExecApprovalRequestEvent {
call_id,
approval_id,
turn_id,
command,
cwd,
reason,
proposed_execpolicy_amendment,
parsed_cmd,
..
} = ev;
match api_version {
ApiVersion::V1 => {
let params = ExecCommandApprovalParams {
conversation_id,
item_id,
command_string,
call_id: call_id.clone(),
approval_id,
command,
cwd,
command_actions,
rx,
conversation,
outgoing,
)
.await;
});
reason,
parsed_cmd,
};
let rx = outgoing
.send_request(ServerRequestPayload::ExecCommandApproval(params))
.await;
tokio::spawn(async move {
on_exec_approval_response(
approval_id_for_op,
event_turn_id,
rx,
conversation,
)
.await;
});
}
ApiVersion::V2 => {
let command_actions = parsed_cmd
.iter()
.cloned()
.map(V2ParsedCommand::from)
.collect::<Vec<_>>();
let command_string = shlex_join(&command);
let proposed_execpolicy_amendment_v2 =
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
let params = CommandExecutionRequestApprovalParams {
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
item_id: call_id.clone(),
approval_id: approval_id.clone(),
reason,
command: Some(command_string.clone()),
cwd: Some(cwd.clone()),
command_actions: Some(command_actions.clone()),
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
};
let rx = outgoing
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
params,
))
.await;
tokio::spawn(async move {
on_command_execution_request_approval_response(
event_turn_id,
conversation_id,
approval_id,
call_id,
command_string,
cwd,
command_actions,
rx,
conversation,
outgoing,
thread_state.clone(),
)
.await;
});
}
}
},
}
EventMsg::RequestUserInput(request) => {
if matches!(api_version, ApiVersion::V2) {
let questions = request
@@ -933,6 +945,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,
@@ -1020,6 +1040,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()
@@ -1739,6 +1767,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: Option<String>,
item_id: String,
command: String,
cwd: PathBuf,
@@ -1746,6 +1775,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 {
@@ -1794,7 +1824,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, approval_id is null.
// For zsh-fork subcommand approvals, approval_id is present and
// item_id points to the parent command item.
if approval_id.is_some() {
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(),
@@ -1811,7 +1858,7 @@ async fn on_command_execution_request_approval_response(
if let Err(err) = conversation
.submit(Op::ExecApproval {
id: item_id,
id: approval_id.unwrap_or_else(|| item_id.clone()),
turn_id: Some(event_turn_id),
decision,
})