Route escalated approval retries through Guardian (#38616)

## What changed

- Bypass extension approval contributors when an approval request includes a retry reason, ensuring the retry is reviewed by Guardian.
- Add coverage showing that Guardian can deny an escalated retry even when an extension contributor would approve it, and that the retry reason is included in the review request.

GitOrigin-RevId: c8f556623f26fea605898d4b8e89679cebd63e65
This commit is contained in:
jif
2026-08-14 17:56:38 +00:00
committed by copyberry
parent 0fce933290
commit fa595fbab8
2 changed files with 67 additions and 0 deletions
+1
View File
@@ -317,6 +317,7 @@ async fn run_guardian_review(
.config_layer_stack
.requirements()
.auto_review_required_for_model(&turn.model_info.slug)
&& reasons.retry.is_none()
&& options
.external_cancel
.as_ref()
+66
View File
@@ -2981,6 +2981,72 @@ async fn guardian_review_does_not_retry_valid_denial() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn escalated_retry_bypasses_extension_approval_and_runs_guardian() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
struct AutoApprovingReviewContributor;
impl codex_extension_api::ApprovalReviewContributor for AutoApprovingReviewContributor {
fn contribute<'a>(
&'a self,
_session_store: &'a codex_extension_api::ExtensionData,
_thread_store: &'a codex_extension_api::ExtensionData,
_prompt: &'a str,
) -> codex_extension_api::ExtensionFuture<'a, Option<ReviewDecision>> {
Box::pin(async move { Some(ReviewDecision::Approved) })
}
}
let server = start_mock_server().await;
let denial = serde_json::json!({
"risk_level": "high",
"user_authorization": "unknown",
"outcome": "deny",
"rationale": "The original attempt was blocked by the sandbox.",
})
.to_string();
let request_log = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-escalated-retry"),
ev_assistant_message("msg-escalated-retry", &denial),
ev_completed("resp-escalated-retry"),
]),
)
.await;
let (mut session, turn) = guardian_test_session_and_turn(&server).await;
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::<Config>::new();
extensions.approval_review_contributor(Arc::new(AutoApprovingReviewContributor));
Arc::get_mut(&mut session)
.expect("session should be uniquely owned")
.services
.extensions = Arc::new(extensions.build());
seed_guardian_parent_history(&session, &turn).await;
let retry_reason = "The sandbox blocked the original command.";
let decision = review_approval_request(
&session,
&turn,
"review-escalated-retry".to_string(),
guardian_shell_request("shell-escalated-retry"),
ApprovalRequestReasons {
approval: None,
retry: Some(retry_reason.to_string()),
},
)
.await;
assert!(matches!(decision, ReviewDecision::Denied { .. }));
assert!(
request_log
.single_request()
.body_contains_text(retry_reason)
);
Ok(())
}
#[tokio::test]
async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> anyhow::Result<()>
{