diff --git a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json index 84842fd194..84c36edf10 100644 --- a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json @@ -94,6 +94,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json index baedafa403..477109e2b0 100644 --- a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json @@ -94,6 +94,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 4f5410c782..8f30cabaa0 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1426,6 +1426,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "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 e4ced5070c..e934652c4f 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 @@ -3422,6 +3422,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ @@ -8437,6 +8444,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "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 d1f3cbda55..b55ad23eb2 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 @@ -5176,6 +5176,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "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 1f706e5453..eb627bf187 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json @@ -215,6 +215,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json index cd9adf32f5..9fdba52844 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json @@ -215,6 +215,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts index b5193785d8..109f72929c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts @@ -7,4 +7,4 @@ import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; /** * User's decision in response to an ExecApprovalRequest. */ -export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "abort"; +export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "timed_out" | "abort"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts index b98578b206..ae59854bde 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts @@ -5,4 +5,4 @@ /** * [UNSTABLE] Lifecycle state for a guardian approval review. */ -export type GuardianApprovalReviewStatus = "inProgress" | "approved" | "denied" | "aborted"; +export type GuardianApprovalReviewStatus = "inProgress" | "approved" | "denied" | "timedOut" | "aborted"; diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 9853f69f40..b2fce1ec43 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -221,6 +221,9 @@ pub fn guardian_auto_approval_review_notification( codex_protocol::protocol::GuardianAssessmentStatus::Denied => { GuardianApprovalReviewStatus::Denied } + codex_protocol::protocol::GuardianAssessmentStatus::TimedOut => { + GuardianApprovalReviewStatus::TimedOut + } codex_protocol::protocol::GuardianAssessmentStatus::Aborted => { GuardianApprovalReviewStatus::Aborted } @@ -244,6 +247,7 @@ pub fn guardian_auto_approval_review_notification( } codex_protocol::protocol::GuardianAssessmentStatus::Approved | codex_protocol::protocol::GuardianAssessmentStatus::Denied + | codex_protocol::protocol::GuardianAssessmentStatus::TimedOut | codex_protocol::protocol::GuardianAssessmentStatus::Aborted => { ServerNotification::ItemGuardianApprovalReviewCompleted( ItemGuardianApprovalReviewCompletedNotification { diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index b005657968..86432ea3fd 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -408,6 +408,7 @@ impl ThreadHistoryBuilder { GuardianAssessmentStatus::Denied | GuardianAssessmentStatus::Aborted => { CommandExecutionStatus::Declined } + GuardianAssessmentStatus::TimedOut => CommandExecutionStatus::Failed, GuardianAssessmentStatus::Approved => return, }; let Some(item) = build_item_from_guardian_event(payload, status) else { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 66ac1a6020..33aa6a607f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1061,6 +1061,7 @@ impl From for CommandExecutionApprovalDecision { }, CoreReviewDecision::Abort => Self::Cancel, CoreReviewDecision::Denied => Self::Decline, + CoreReviewDecision::TimedOut => Self::Decline, } } } @@ -4552,6 +4553,7 @@ pub enum GuardianApprovalReviewStatus { InProgress, Approved, Denied, + TimedOut, Aborted, } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index ce71e8974d..46a56cebbe 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -333,6 +333,7 @@ pub(crate) async fn apply_bespoke_event_handling( if matches!( assessment.status, codex_protocol::protocol::GuardianAssessmentStatus::Denied + | codex_protocol::protocol::GuardianAssessmentStatus::TimedOut | codex_protocol::protocol::GuardianAssessmentStatus::Aborted ) && let Some(completion_item) = pending_command_execution { @@ -3017,6 +3018,9 @@ mod tests { Some(codex_protocol::protocol::GuardianUserAuthorization::Low), Some("too risky".to_string()), ), + GuardianAssessmentStatus::TimedOut => { + (None, None, Some("review timed out".to_string())) + } GuardianAssessmentStatus::Aborted => (None, None, None), }; GuardianAssessmentEvent { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 1e07c6d1ff..0a5483685d 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -711,7 +711,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::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string() } }; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 46654e35f6..8377856826 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -977,7 +977,7 @@ async fn mcp_tool_approval_decision_from_guardian( | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept, ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession, - ReviewDecision::Denied => McpToolApprovalDecision::Decline { + ReviewDecision::Denied | ReviewDecision::TimedOut => McpToolApprovalDecision::Decline { message: Some(guardian_rejection_message(sess, call_id).await), }, ReviewDecision::Abort => McpToolApprovalDecision::Decline { message: None }, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index ffa536c39c..4d2f159683 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -486,7 +486,7 @@ impl NetworkApprovalService { PendingApprovalDecision::Deny } }, - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { if routes_approval_to_guardian(&turn_context) { if let Some(owner_call) = owner_call.as_ref() { let message = diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index c05009ff16..4477a98d07 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -147,7 +147,7 @@ impl ToolOrchestrator { otel.tool_decision(otel_tn, otel_ci, &decision, otel_source); match decision { - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { let reason = if routes_approval_to_guardian(turn_ctx) { guardian_rejection_message(tool_ctx.session.as_ref(), &tool_ctx.call_id) .await @@ -301,7 +301,9 @@ impl ToolOrchestrator { otel.tool_decision(otel_tn, otel_ci, &decision, otel_source); match decision { - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied + | ReviewDecision::TimedOut + | ReviewDecision::Abort => { let reason = if routes_approval_to_guardian(turn_ctx) { guardian_rejection_message( tool_ctx.session.as_ref(), 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 47d1b62539..5495b151f7 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -469,7 +469,7 @@ impl CoreShellActionProvider { EscalationDecision::deny(Some("User denied execution".to_string())) } }, - ReviewDecision::Denied => { + ReviewDecision::Denied | ReviewDecision::TimedOut => { let message = if routes_approval_to_guardian(&self.turn) { guardian_rejection_message(self.session.as_ref(), &self.call_id) .await diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 557f3856fc..0597f1c0fa 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -105,6 +105,7 @@ pub enum GuardianAssessmentStatus { InProgress, Approved, Denied, + TimedOut, Aborted, } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 44693033d8..7a6e563182 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3517,6 +3517,9 @@ pub enum ReviewDecision { #[default] Denied, + /// Automatic approval review timed out before reaching a decision. + TimedOut, + /// User has denied this command and the agent should not do anything until /// the user's next command. Abort, @@ -3537,6 +3540,7 @@ impl ReviewDecision { NetworkPolicyRuleAction::Deny => "denied_with_network_policy_deny", }, ReviewDecision::Denied => "denied", + ReviewDecision::TimedOut => "timed_out", ReviewDecision::Abort => "abort", } } diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index 03449b902a..b1393e95e0 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -258,6 +258,7 @@ fn file_change_decision(decision: &ReviewDecision) -> Result Ok(FileChangeApprovalDecision::Accept), ReviewDecision::ApprovedForSession => Ok(FileChangeApprovalDecision::AcceptForSession), ReviewDecision::Denied => Ok(FileChangeApprovalDecision::Decline), + ReviewDecision::TimedOut => Ok(FileChangeApprovalDecision::Decline), ReviewDecision::Abort => Ok(FileChangeApprovalDecision::Cancel), ReviewDecision::ApprovedExecpolicyAmendment { .. } => { Err("execpolicy amendment is not a valid file change approval decision".to_string()) diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 82e74888f9..96b86aacf6 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -277,7 +277,9 @@ impl ApprovalOverlay { }; let granted_permissions = match decision { ReviewDecision::Approved | ReviewDecision::ApprovedForSession => permissions.clone(), - ReviewDecision::Denied | ReviewDecision::Abort => Default::default(), + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { + Default::default() + } ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => Default::default(), }; @@ -720,6 +722,7 @@ fn exec_options( display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('d'))], }), + ReviewDecision::TimedOut => None, ReviewDecision::Abort => Some(ApprovalOption { label: "No, and tell Codex what to do differently".to_string(), decision: ApprovalDecision::Review(ReviewDecision::Abort), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 74b2032892..f6d0dc2630 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6889,6 +6889,9 @@ impl ChatWidget { codex_app_server_protocol::GuardianApprovalReviewStatus::Denied => { GuardianAssessmentStatus::Denied } + codex_app_server_protocol::GuardianApprovalReviewStatus::TimedOut => { + GuardianAssessmentStatus::TimedOut + } codex_app_server_protocol::GuardianApprovalReviewStatus::Aborted => { GuardianAssessmentStatus::Aborted } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 987005db02..2f5591a933 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -892,6 +892,18 @@ pub fn new_approval_decision_cell( }; ("✗ ".red(), summary) } + TimedOut => { + let snippet = Span::from(exec_snippet(&command)).dim(); + ( + "✗ ".red(), + vec![ + "Review ".into(), + "timed out".bold(), + " before codex could run ".into(), + snippet, + ], + ) + } Abort => { let snippet = Span::from(exec_snippet(&command)).dim(); (