Propagate approval rejection reasons (#34400)

## What changed

- Change `ReviewDecision::Denied` to carry a rejection string and update the generated protocol schemas.
- Preserve specific rejection reasons through command, patch, network, MCP, delegated, and automatic approval flows so tool results can return them to the model.
- Distinguish invalid approval responses from user declines and truncate oversized rejection messages before adding them to model context.

## Testing

- Add coverage for denial serialization, invalid app-server approval responses, dropped automatic reviews, network rejection propagation, and rejection-message truncation.

GitOrigin-RevId: d48a55643aa03c7ecfa08a8e4b669913eec5464a
This commit is contained in:
Dylan Hurd
2026-07-20 19:23:21 +00:00
committed by copyberry
parent 8c41ed33ce
commit e52c35b000
36 changed files with 499 additions and 452 deletions
+15 -5
View File
@@ -28,6 +28,7 @@ use codex_protocol::user_input::UserInput;
use serde_json::Value;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::sync::oneshot;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
@@ -534,7 +535,7 @@ async fn handle_exec_approval(
review_cancel.clone(),
);
await_approval_with_cancel(
async move { review_rx.await.unwrap_or_default() },
receive_approval_review(review_rx),
parent_session,
&approval_id_for_op,
cancel_token,
@@ -643,7 +644,7 @@ async fn handle_patch_approval(
);
Some(
await_approval_with_cancel(
async move { review_rx.await.unwrap_or_default() },
receive_approval_review(review_rx),
parent_session,
&approval_id,
cancel_token,
@@ -758,7 +759,7 @@ async fn maybe_auto_review_mcp_request_user_input(
review_cancel.clone(),
);
let decision = await_approval_with_cancel(
async move { review_rx.await.unwrap_or_default() },
receive_approval_review(review_rx),
parent_session,
&event.call_id,
cancel_token,
@@ -779,7 +780,7 @@ async fn maybe_auto_review_mcp_request_user_input(
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
ReviewDecision::Denied { .. } | ReviewDecision::TimedOut | ReviewDecision::Abort => {
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
}
};
@@ -884,6 +885,12 @@ where
}
}
async fn receive_approval_review(review_rx: oneshot::Receiver<ReviewDecision>) -> ReviewDecision {
review_rx
.await
.unwrap_or_else(|_| ReviewDecision::denied("automatic approval review could not complete"))
}
/// Await an approval decision, aborting on cancellation.
async fn await_approval_with_cancel<F>(
fut: F,
@@ -902,7 +909,10 @@ where
review_cancel_token.cancel();
}
parent_session
.notify_approval(approval_id, codex_protocol::protocol::ReviewDecision::Abort)
.notify_approval(
approval_id,
codex_protocol::protocol::ReviewDecision::Abort,
)
.await;
codex_protocol::protocol::ReviewDecision::Abort
}