From 90b95081778afbe69407c63be62d51489945ebdd Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Fri, 12 Jun 2026 02:58:18 -0700 Subject: [PATCH] Add deterministic Guardian review mode --- .../analytics/src/analytics_client_tests.rs | 1 + codex-rs/analytics/src/events.rs | 5 ++ .../schema/json/ServerNotification.json | 3 +- .../codex_app_server_protocol.schemas.json | 3 +- .../codex_app_server_protocol.v2.schemas.json | 3 +- ...anApprovalReviewCompletedNotification.json | 3 +- .../typescript/v2/AutoReviewDecisionSource.ts | 2 +- .../src/protocol/v2/item.rs | 2 + codex-rs/app-server/README.md | 2 +- codex-rs/core/src/guardian/metrics.rs | 15 ++++ codex-rs/core/src/guardian/mod.rs | 2 + codex-rs/core/src/guardian/prompt.rs | 1 + codex-rs/core/src/guardian/review.rs | 83 ++++++++++++++----- codex-rs/core/src/guardian/tests.rs | 56 +++++++++++++ codex-rs/core/src/mcp_tool_call.rs | 2 + codex-rs/core/src/session/mcp.rs | 1 + codex-rs/core/src/tools/network_approval.rs | 2 + .../core/src/tools/runtimes/apply_patch.rs | 12 ++- codex-rs/core/src/tools/runtimes/shell.rs | 2 + .../tools/runtimes/shell/unix_escalation.rs | 2 + .../core/src/tools/runtimes/unified_exec.rs | 2 + codex-rs/protocol/src/approvals.rs | 1 + .../tui/src/chatwidget/protocol_requests.rs | 3 + 23 files changed, 177 insertions(+), 31 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index c0983a3dba..89b66680f5 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -1983,6 +1983,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { risk_level: None, user_authorization: None, outcome: None, + decision_source: None, guardian_thread_id: None, guardian_session_kind: None, guardian_model: None, diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 2df113a43a..6d05644ca7 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -30,6 +30,7 @@ use codex_plugin::PluginTelemetryMetadata; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::SandboxPermissions; +use codex_protocol::protocol::GuardianAssessmentDecisionSource; use codex_protocol::protocol::GuardianAssessmentOutcome; use codex_protocol::protocol::GuardianCommandSource; use codex_protocol::protocol::GuardianRiskLevel; @@ -270,6 +271,7 @@ pub struct GuardianReviewEventParams { pub risk_level: Option, pub user_authorization: Option, pub outcome: Option, + pub decision_source: Option, pub guardian_thread_id: Option, pub guardian_session_kind: Option, pub guardian_model: Option, @@ -343,6 +345,7 @@ impl GuardianReviewTrackContext { risk_level: result.risk_level, user_authorization: result.user_authorization, outcome: result.outcome, + decision_source: result.decision_source, guardian_thread_id: result.guardian_thread_id, guardian_session_kind: result.guardian_session_kind, guardian_model: result.guardian_model, @@ -379,6 +382,7 @@ pub struct GuardianReviewAnalyticsResult { pub risk_level: Option, pub user_authorization: Option, pub outcome: Option, + pub decision_source: Option, pub guardian_thread_id: Option, pub guardian_session_kind: Option, pub guardian_model: Option, @@ -399,6 +403,7 @@ impl GuardianReviewAnalyticsResult { risk_level: None, user_authorization: None, outcome: None, + decision_source: None, guardian_thread_id: None, guardian_session_kind: None, guardian_model: None, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 1c37ea6f33..fc8c8703a8 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -545,7 +545,8 @@ "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": [ - "agent" + "agent", + "deterministic" ], "type": "string" }, 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 9aedb8a7ea..862e6a64bb 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 @@ -6777,7 +6777,8 @@ "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": [ - "agent" + "agent", + "deterministic" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ebe1efe26d..fb3311f6e2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1046,7 +1046,8 @@ "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": [ - "agent" + "agent", + "deterministic" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json index a366c99a41..c9a6f17c78 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json @@ -61,7 +61,8 @@ "AutoReviewDecisionSource": { "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", "enum": [ - "agent" + "agent", + "deterministic" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AutoReviewDecisionSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AutoReviewDecisionSource.ts index 8806981237..7dbd4d5cee 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AutoReviewDecisionSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AutoReviewDecisionSource.ts @@ -5,4 +5,4 @@ /** * [UNSTABLE] Source that produced a terminal approval auto-review decision. */ -export type AutoReviewDecisionSource = "agent"; +export type AutoReviewDecisionSource = "agent" | "deterministic"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index f556890a96..b10b572a09 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -425,12 +425,14 @@ pub enum GuardianApprovalReviewStatus { /// [UNSTABLE] Source that produced a terminal approval auto-review decision. pub enum AutoReviewDecisionSource { Agent, + Deterministic, } impl From for AutoReviewDecisionSource { fn from(value: CoreGuardianAssessmentDecisionSource) -> Self { match value { CoreGuardianAssessmentDecisionSource::Agent => Self::Agent, + CoreGuardianAssessmentDecisionSource::Deterministic => Self::Deterministic, } } } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index cc36bb4518..15532e39f1 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1328,7 +1328,7 @@ All items emit shared lifecycle events: - `item/autoApprovalReview/started` — [UNSTABLE] temporary auto-review notification carrying `{threadId, turnId, targetItemId, review, action}` when approval auto-review begins. This shape is expected to change soon. - `item/autoApprovalReview/completed` — [UNSTABLE] temporary auto-review notification carrying `{threadId, turnId, targetItemId, review, action}` when approval auto-review resolves. This shape is expected to change soon. -`review` is [UNSTABLE] and currently has `{status, riskLevel?, userAuthorization?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `riskLevel` is one of `"low"`, `"medium"`, `"high"`, or `"critical"` when present. `userAuthorization` is one of `"unknown"`, `"low"`, `"medium"`, or `"high"` when present. `action` is a tagged union with `type: "command" | "execve" | "applyPatch" | "networkAccess" | "mcpToolCall"`. Command-like actions include a `source` discriminator (`"shell"` or `"unifiedExec"`). These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the auto-review app protocol is still being designed. +`review` is [UNSTABLE] and currently has `{status, riskLevel?, userAuthorization?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `riskLevel` is one of `"low"`, `"medium"`, `"high"`, or `"critical"` when present. `userAuthorization` is one of `"unknown"`, `"low"`, `"medium"`, or `"high"` when present. Completed notifications also carry `decisionSource`, currently `"agent"` or `"deterministic"`. `action` is a tagged union with `type: "command" | "execve" | "applyPatch" | "networkAccess" | "mcpToolCall"`. Command-like actions include a `source` discriminator (`"shell"` or `"unifiedExec"`). These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the auto-review app protocol is still being designed. There are additional item-specific events: diff --git a/codex-rs/core/src/guardian/metrics.rs b/codex-rs/core/src/guardian/metrics.rs index 9b9d35a526..bf6a6ad3e8 100644 --- a/codex-rs/core/src/guardian/metrics.rs +++ b/codex-rs/core/src/guardian/metrics.rs @@ -13,6 +13,7 @@ use codex_otel::GUARDIAN_REVIEW_TOKEN_USAGE_METRIC; use codex_otel::GUARDIAN_REVIEW_TTFT_DURATION_METRIC; use codex_otel::SessionTelemetry; use codex_otel::sanitize_metric_tag_value; +use codex_protocol::protocol::GuardianAssessmentDecisionSource; use codex_protocol::protocol::GuardianAssessmentOutcome; use codex_protocol::protocol::GuardianRiskLevel; use codex_protocol::protocol::GuardianUserAuthorization; @@ -115,6 +116,10 @@ fn guardian_review_metric_tags( user_authorization_tag(result.user_authorization).to_string(), ), ("outcome", outcome_tag(result.outcome).to_string()), + ( + "decision_source", + decision_source_tag(result.decision_source).to_string(), + ), ( "guardian_model", result @@ -231,6 +236,14 @@ fn outcome_tag(outcome: Option) -> &'static str { } } +fn decision_source_tag(decision_source: Option) -> &'static str { + match decision_source { + Some(GuardianAssessmentDecisionSource::Agent) => "agent", + Some(GuardianAssessmentDecisionSource::Deterministic) => "deterministic", + None => "none", + } +} + #[cfg(test)] mod tests { use super::*; @@ -340,6 +353,7 @@ mod tests { risk_level: Some(GuardianRiskLevel::Low), user_authorization: Some(GuardianUserAuthorization::High), outcome: Some(GuardianAssessmentOutcome::Allow), + decision_source: Some(GuardianAssessmentDecisionSource::Agent), guardian_session_kind: Some(GuardianReviewSessionKind::TrunkReused), guardian_model: Some("gpt-5.4 guardian".to_string()), guardian_reasoning_effort: Some("low".to_string()), @@ -382,6 +396,7 @@ mod tests { "delegated_subagent".to_string() ), ("decision".to_string(), "approved".to_string()), + ("decision_source".to_string(), "agent".to_string()), ("failure_reason".to_string(), "none".to_string()), ("guardian_model".to_string(), "gpt-5.4_guardian".to_string()), ("guardian_reasoning_effort".to_string(), "low".to_string()), diff --git a/codex-rs/core/src/guardian/mod.rs b/codex-rs/core/src/guardian/mod.rs index b06a2daad3..0f8e8ae020 100644 --- a/codex-rs/core/src/guardian/mod.rs +++ b/codex-rs/core/src/guardian/mod.rs @@ -29,6 +29,7 @@ pub(crate) use approval_request::GuardianMcpAnnotations; pub(crate) use approval_request::GuardianNetworkAccessTrigger; #[cfg(test)] pub(crate) use approval_request::guardian_approval_request_to_json; +pub(crate) use review::GuardianReviewMode; pub(crate) use review::guardian_rejection_message; pub(crate) use review::guardian_timeout_message; pub(crate) use review::is_guardian_reviewer_source; @@ -66,6 +67,7 @@ pub(crate) struct GuardianAssessment { pub(crate) user_authorization: codex_protocol::protocol::GuardianUserAuthorization, pub(crate) outcome: GuardianAssessmentOutcome, pub(crate) rationale: String, + pub(crate) source: GuardianAssessmentDecisionSource, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/codex-rs/core/src/guardian/prompt.rs b/codex-rs/core/src/guardian/prompt.rs index 008e979574..84594bb589 100644 --- a/codex-rs/core/src/guardian/prompt.rs +++ b/codex-rs/core/src/guardian/prompt.rs @@ -619,6 +619,7 @@ pub(crate) fn parse_guardian_assessment(text: Option<&str>) -> anyhow::Result format!( + GuardianAssessmentDecisionSource::Agent + | GuardianAssessmentDecisionSource::Deterministic => format!( "This action was rejected due to unacceptable risk.\nReason: {}\n{}", rejection.rationale.trim(), GUARDIAN_REJECTION_INSTRUCTIONS @@ -99,6 +100,20 @@ pub(super) enum GuardianReviewOutcome { Error(GuardianReviewError), } +#[derive(Debug)] +pub(crate) enum GuardianReviewMode { + Agent, + #[cfg_attr(not(test), allow(dead_code))] + Deterministic(GuardianAssessment), +} + +struct GuardianReviewExecution { + retry_reason: Option, + review_mode: GuardianReviewMode, + approval_request_source: GuardianApprovalRequestSource, + external_cancel: Option, +} + #[derive(Debug)] pub(super) enum GuardianReviewError { PromptBuild { @@ -277,10 +292,14 @@ async fn run_guardian_review( turn: Arc, review_id: String, request: GuardianApprovalRequest, - retry_reason: Option, - approval_request_source: GuardianApprovalRequestSource, - external_cancel: Option, + execution: GuardianReviewExecution, ) -> ReviewDecision { + let GuardianReviewExecution { + retry_reason, + review_mode, + approval_request_source, + external_cancel, + } = execution; let target_item_id = guardian_request_target_item_id(&request).map(str::to_string); let assessment_turn_id = guardian_request_turn_id(&request, &turn.sub_id).to_string(); let action_summary = guardian_assessment_action(&request); @@ -354,18 +373,26 @@ async fn run_guardian_review( return ReviewDecision::Abort; } - let schema = guardian_output_schema(); let terminal_action = action_summary.clone(); - let (outcome, analytics_result) = Box::pin(run_guardian_review_session_with_retry( - session.clone(), - turn.clone(), - request, - retry_reason.clone(), - schema, - external_cancel, - GUARDIAN_REVIEW_MAX_ATTEMPTS, - )) - .await; + let (outcome, analytics_result) = match review_mode { + GuardianReviewMode::Deterministic(assessment) => ( + GuardianReviewOutcome::Completed(assessment), + GuardianReviewAnalyticsResult::without_session(), + ), + GuardianReviewMode::Agent => { + let schema = guardian_output_schema(); + Box::pin(run_guardian_review_session_with_retry( + session.clone(), + turn.clone(), + request, + retry_reason.clone(), + schema, + external_cancel, + GUARDIAN_REVIEW_MAX_ATTEMPTS, + )) + .await + } + }; let completed_at_ms = now_unix_timestamp_ms(); let (assessment, count_denial_for_circuit_breaker) = match outcome { @@ -391,6 +418,7 @@ async fn run_guardian_review( risk_level: Some(assessment.risk_level), user_authorization: Some(assessment.user_authorization), outcome: Some(assessment.outcome), + decision_source: Some(assessment.source), ..analytics_result }, completed_at_ms.try_into().unwrap_or_default(), @@ -512,6 +540,7 @@ async fn run_guardian_review( user_authorization: GuardianUserAuthorization::Unknown, outcome: GuardianAssessmentOutcome::Deny, rationale, + source: GuardianAssessmentDecisionSource::Agent, }, false, ) @@ -553,7 +582,7 @@ async fn run_guardian_review( } else { let rejection = GuardianRejection { rationale: assessment.rationale.clone(), - source: GuardianAssessmentDecisionSource::Agent, + source: assessment.source, }; rationales.insert(review_id.clone(), rejection); } @@ -571,7 +600,7 @@ async fn run_guardian_review( risk_level: Some(assessment.risk_level), user_authorization: Some(assessment.user_authorization), rationale: Some(assessment.rationale.clone()), - decision_source: Some(GuardianAssessmentDecisionSource::Agent), + decision_source: Some(assessment.source), action: terminal_action, }), ) @@ -596,6 +625,7 @@ pub(crate) async fn review_approval_request( turn: &Arc, review_id: String, request: GuardianApprovalRequest, + review_mode: GuardianReviewMode, retry_reason: Option, ) -> ReviewDecision { // Box the delegated review future so callers do not inline the entire @@ -605,9 +635,12 @@ pub(crate) async fn review_approval_request( Arc::clone(turn), review_id, request, - retry_reason, - GuardianApprovalRequestSource::MainTurn, - /*external_cancel*/ None, + GuardianReviewExecution { + retry_reason, + review_mode, + approval_request_source: GuardianApprovalRequestSource::MainTurn, + external_cancel: None, + }, )) .await } @@ -626,9 +659,12 @@ pub(crate) async fn review_approval_request_with_cancel( Arc::clone(turn), review_id, request, - retry_reason, - approval_request_source, - Some(cancel_token), + GuardianReviewExecution { + retry_reason, + review_mode: GuardianReviewMode::Agent, + approval_request_source, + external_cancel: Some(cancel_token), + }, ) .await } @@ -948,6 +984,7 @@ mod review_tests { user_authorization: GuardianUserAuthorization::Unknown, outcome: GuardianAssessmentOutcome::Deny, rationale: "deny".to_string(), + source: GuardianAssessmentDecisionSource::Agent, }; let transient_error_info = [ CodexErrorInfo::ServerOverloaded, diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 1698ea0c68..904addfad6 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -42,6 +42,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GranularApprovalConfig; +use codex_protocol::protocol::GuardianAssessmentDecisionSource; use codex_protocol::protocol::GuardianAssessmentStatus; use codex_protocol::protocol::GuardianRiskLevel; use codex_protocol::protocol::GuardianUserAuthorization; @@ -1120,6 +1121,51 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() { assert!(warnings.is_empty()); } +#[tokio::test] +async fn deterministic_guardian_review_emits_terminal_assessment_without_model_review() { + let (session, turn, rx) = crate::session::tests::make_session_and_context_with_rx().await; + + let decision = review_approval_request( + &session, + &turn, + "review-deterministic-guardian".to_string(), + GuardianApprovalRequest::ApplyPatch { + id: "patch-1".to_string(), + cwd: test_path_buf("/tmp").abs(), + files: vec![test_path_buf("/tmp/guardian.txt").abs()], + patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch" + .to_string(), + }, + GuardianReviewMode::Deterministic(GuardianAssessment { + risk_level: GuardianRiskLevel::Low, + user_authorization: GuardianUserAuthorization::High, + outcome: GuardianAssessmentOutcome::Allow, + rationale: "Exact deterministic policy allowed this action.".to_string(), + source: GuardianAssessmentDecisionSource::Deterministic, + }), + /*retry_reason*/ None, + ) + .await; + + assert_eq!(decision, ReviewDecision::Approved); + + let mut assessments = Vec::new(); + while let Ok(event) = rx.try_recv() { + if let EventMsg::GuardianAssessment(event) = event.msg { + assessments.push(event); + } + } + + assert_eq!(assessments.len(), 2); + assert_eq!(assessments[0].status, GuardianAssessmentStatus::InProgress); + assert_eq!(assessments[0].decision_source, None); + assert_eq!(assessments[1].status, GuardianAssessmentStatus::Approved); + assert_eq!( + assessments[1].decision_source, + Some(GuardianAssessmentDecisionSource::Deterministic) + ); +} + #[test] fn guardian_timeout_message_distinguishes_timeout_from_policy_denial() { let message = guardian_timeout_message(); @@ -1279,6 +1325,7 @@ fn parse_guardian_assessment_extracts_embedded_json() { user_authorization: GuardianUserAuthorization::Low, outcome: GuardianAssessmentOutcome::Allow, rationale: "ok".to_string(), + source: GuardianAssessmentDecisionSource::Agent, } ); } @@ -1295,6 +1342,7 @@ fn parse_guardian_assessment_treats_bare_allow_as_low_risk() { user_authorization: GuardianUserAuthorization::Unknown, outcome: GuardianAssessmentOutcome::Allow, rationale: "Auto-review returned a low-risk allow decision.".to_string(), + source: GuardianAssessmentDecisionSource::Agent, } ); } @@ -1311,6 +1359,7 @@ fn parse_guardian_assessment_treats_bare_deny_as_high_risk() { user_authorization: GuardianUserAuthorization::Unknown, outcome: GuardianAssessmentOutcome::Deny, rationale: "Auto-review returned a deny decision without a rationale.".to_string(), + source: GuardianAssessmentDecisionSource::Agent, } ); } @@ -2077,6 +2126,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> additional_permissions: None, justification: Some("Need to push the reviewed docs fix.".to_string()), }, + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; @@ -2210,6 +2260,7 @@ async fn guardian_review_does_not_retry_missing_assessment_payload() -> anyhow:: &turn, "review-missing-assessment".to_string(), guardian_shell_request("shell-missing-assessment"), + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; @@ -2314,6 +2365,7 @@ async fn guardian_review_exhausts_three_failures_with_one_terminal_event() -> an &turn, "review-exhausted-retry".to_string(), guardian_shell_request("shell-exhausted-retry"), + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; @@ -2365,6 +2417,7 @@ async fn guardian_review_does_not_retry_valid_denial() -> anyhow::Result<()> { &turn, "review-valid-denial".to_string(), guardian_shell_request("shell-valid-denial"), + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; @@ -2468,6 +2521,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> &turn, "review-shell-guardian-1".to_string(), initial_request, + GuardianReviewMode::Agent, /*retry_reason*/ None ) .await, @@ -2522,6 +2576,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> &turn_for_second, "review-shell-guardian-2".to_string(), second_request, + GuardianReviewMode::Agent, Some("trunk follow-up".to_string()), ) .await @@ -2569,6 +2624,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> &turn, "review-shell-guardian-3".to_string(), third_request, + GuardianReviewMode::Agent, Some("parallel follow-up".to_string()), ) .await; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index e3f7d5faf5..0d4516d8f8 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -9,6 +9,7 @@ use crate::config::edit::ConfigEditsBuilder; use crate::connectors; use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianMcpAnnotations; +use crate::guardian::GuardianReviewMode; use crate::guardian::guardian_rejection_message; use crate::guardian::guardian_timeout_message; use crate::guardian::new_guardian_review_id; @@ -1213,6 +1214,7 @@ async fn maybe_request_mcp_tool_approval( turn_context, review_id.clone(), build_guardian_mcp_tool_review_request(call_id, invocation, metadata), + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 5e52e6d18e..9d4c6f4060 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -466,6 +466,7 @@ async fn review_guardian_mcp_elicitation( &turn_context, review_id.clone(), guardian_request, + crate::guardian::GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 9cc808a5b6..eddc9bc13e 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -1,5 +1,6 @@ use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianNetworkAccessTrigger; +use crate::guardian::GuardianReviewMode; use crate::guardian::guardian_rejection_message; use crate::guardian::guardian_timeout_message; use crate::guardian::new_guardian_review_id; @@ -514,6 +515,7 @@ impl NetworkApprovalService { port: key.port, trigger: owner_call.as_ref().map(|call| call.trigger.clone()), }, + GuardianReviewMode::Agent, Some(policy_denial_message.clone()), ) .await diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index c9a5e316e4..836fcaa6bc 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -5,6 +5,7 @@ //! sandboxing enforced by the explicit filesystem sandbox context. use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; +use crate::guardian::GuardianReviewMode; use crate::guardian::review_approval_request; use crate::session::turn_context::TurnEnvironment; use crate::tools::hook_names::HookToolName; @@ -144,8 +145,15 @@ impl Approvable for ApplyPatchRuntime { Box::pin(async move { if let Some(review_id) = guardian_review_id { let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id); - return review_approval_request(session, turn, review_id, action, retry_reason) - .await; + return review_approval_request( + session, + turn, + review_id, + action, + GuardianReviewMode::Agent, + retry_reason, + ) + .await; } if req.permissions_preapproved && retry_reason.is_none() { return ReviewDecision::Approved; diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 13b65f0443..5fa4ac333d 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -12,6 +12,7 @@ use crate::command_canonicalization::canonicalize_command_for_approval; use crate::exec::ExecCapturePolicy; use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianNetworkAccessTrigger; +use crate::guardian::GuardianReviewMode; use crate::guardian::review_approval_request; use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; @@ -160,6 +161,7 @@ impl Approvable for ShellRuntime { additional_permissions: req.additional_permissions.clone(), justification: req.justification.clone(), }, + GuardianReviewMode::Agent, retry_reason, ) .await; 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 960e1d3943..abf969c231 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -4,6 +4,7 @@ use crate::exec::ExecExpiration; use crate::exec::cancel_when_either; use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; +use crate::guardian::GuardianReviewMode; use crate::guardian::guardian_rejection_message; use crate::guardian::guardian_timeout_message; use crate::guardian::new_guardian_review_id; @@ -472,6 +473,7 @@ impl CoreShellActionProvider { cwd: workdir.clone(), additional_permissions, }, + GuardianReviewMode::Agent, /*retry_reason*/ None, ) .await; diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 250965b67c..7ced21e288 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -9,6 +9,7 @@ use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianNetworkAccessTrigger; +use crate::guardian::GuardianReviewMode; use crate::guardian::review_approval_request; use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecServerEnvConfig; @@ -173,6 +174,7 @@ impl Approvable for UnifiedExecRuntime<'_> { justification: req.justification.clone(), tty: req.tty, }, + GuardianReviewMode::Agent, retry_reason, ) .await; diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index ace096359c..fd088d1b6e 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -122,6 +122,7 @@ pub enum GuardianAssessmentStatus { #[serde(rename_all = "snake_case")] pub enum GuardianAssessmentDecisionSource { Agent, + Deterministic, } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/tui/src/chatwidget/protocol_requests.rs b/codex-rs/tui/src/chatwidget/protocol_requests.rs index 5b53f44fdf..f6e80a07d8 100644 --- a/codex-rs/tui/src/chatwidget/protocol_requests.rs +++ b/codex-rs/tui/src/chatwidget/protocol_requests.rs @@ -127,6 +127,9 @@ impl ChatWidget { codex_app_server_protocol::AutoReviewDecisionSource::Agent => { GuardianAssessmentDecisionSource::Agent } + codex_app_server_protocol::AutoReviewDecisionSource::Deterministic => { + GuardianAssessmentDecisionSource::Deterministic + } }), action: action.into(), });