From 4e9a1a907385fa04d0b6b30dc2afd152eba2de90 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sat, 15 Aug 2026 01:19:37 +0000 Subject: [PATCH] Preserve HTTP fallback for delegated sessions (#38681) ## Why Responses WebSocket fallback is session-scoped. A delegated session created after its parent switched to HTTP could otherwise make another WebSocket connection attempt. ## What changed Disable WebSocket support for a delegated session when the parent session has already fallen back to HTTP. ## Testing Add a guardian review regression test that verifies only the parent's initial WebSocket attempt occurs and the guardian request uses the HTTP response path. GitOrigin-RevId: 1345f8fe739f55975aef432045348eae5a9278d6 --- codex-rs/core/src/codex_delegate.rs | 4 + codex-rs/core/tests/suite/guardian_review.rs | 77 ++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 5b170720b9..1cec86480d 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -66,6 +66,10 @@ pub(crate) async fn run_codex_thread_interactive( )); } config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); + config.model_provider.supports_websockets &= parent_session + .services + .model_client + .responses_websocket_enabled(); let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 757990ba2e..d607619cef 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -72,6 +72,11 @@ use std::sync::Mutex; use std::time::Duration; use tempfile::TempDir; use test_case::test_case; +use wiremock::Mock; +use wiremock::ResponseTemplate; +use wiremock::http::Method; +use wiremock::matchers::method; +use wiremock::matchers::path_regex; const CURRENT_TIME_AT: i64 = 1_781_717_655; @@ -112,6 +117,78 @@ impl TimeProvider for RecordingTimeProvider { } } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn guardian_session_inherits_parent_http_fallback() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_wine_exec!( + Ok(()), + "Guardian approval actions require host-native paths" + ); + + let server = start_mock_server().await; + Mock::given(method("GET")) + .and(path_regex(".*/responses$")) + .respond_with(ResponseTemplate::new(426)) + .mount(&server) + .await; + + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_function_call( + "call", + "exec_command", + r#"{"cmd":"true","sandbox_permissions":"require_escalated","justification":"test"}"#, + ), + ev_completed("parent-tool"), + ]), + sse(vec![ + ev_assistant_message("guardian", r#"{"outcome":"deny"}"#), + ev_completed("guardian-review"), + ]), + sse(vec![ev_completed("parent-complete")]), + ], + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.model_provider.supports_websockets = true; + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.approvals_reviewer = ApprovalsReviewer::User; + }); + let test = builder.build_with_auto_env(&server).await?; + + test.codex + .start_or_steer_turn( + TurnInputRequest::user_input(vec![UserInput::Text { + text: "run a command".into(), + text_elements: Vec::new(), + }]) + .with_thread_settings(ThreadSettingsOverrides { + approvals_reviewer: Some(ApprovalsReviewer::AutoReview), + ..Default::default() + }), + ) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let requests = server.received_requests().await.unwrap_or_default(); + let websocket_attempts = requests + .iter() + .filter(|request| request.method == Method::GET) + .count(); + assert_eq!(websocket_attempts, 1); + assert!(responses.requests().iter().any(|request| { + request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian") + })); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[test_case(CodexAuth::from_api_key("test-api-key"), "gpt-5.6-luna"; "api_key_uses_luna_with_responses_lite")] #[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "codex-auto-review"; "chatgpt_uses_codex_auto_review")]