From fa8cf449858c7fffc83d9e3604894852344962a1 Mon Sep 17 00:00:00 2001 From: Rennie Date: Thu, 17 Sep 2026 21:11:09 +0000 Subject: [PATCH] Preserve bio policy errors as a distinct non-retryable error (#46306) ## Why Streaming `bio_policy` failures were classified as generic invalid requests, losing their policy-specific classification. ## What changed - Add `BioPolicy` errors across the API and core protocol, recognizing streaming failures and HTTP 400 responses, including wrapped WebSocket errors. - Preserve server messages and use a biological-risk fallback when the message is missing or blank. - Treat bio policy errors as non-retryable in core and guardian handling, and classify them in diagnostics and telemetry. - Map `BioPolicy` to `other` in the app-server v2 protocol. ## Testing Add coverage for error classification, message preservation and fallbacks, HTTP and wrapped WebSocket responses, guardian retry decisions, and app-server conversion. Extend the core integration test to verify that bio policy failures emit a typed error and complete the turn after a single request. GitOrigin-RevId: 78c2647e8fc8f80297cb8a23fff06ab141099632 --- .../app-server-exports-stable.json.zst | Bin 152041 -> 152048 bytes .../src/protocol/v2/shared.rs | 1 + .../src/protocol/v2/tests.rs | 1 + codex-rs/cli/src/doctor.rs | 1 + codex-rs/codex-api/src/api_bridge.rs | 20 ++++++-- codex-rs/codex-api/src/api_bridge_tests.rs | 47 ++++++++++++++++++ codex-rs/codex-api/src/error.rs | 2 + codex-rs/codex-api/src/sse/responses.rs | 40 +++++++++++++-- .../tests/suite/safety_check_downgrade.rs | 29 ++++++++--- codex-rs/ext/guardian-reviewer/src/retry.rs | 1 + .../ext/guardian-reviewer/src/retry_tests.rs | 7 +++ .../guardian-v2/src/async_scorer/metrics.rs | 6 +-- .../src/async_scorer/sampler/execution.rs | 3 +- codex-rs/protocol/src/error.rs | 4 ++ codex-rs/protocol/src/protocol.rs | 2 + codex-rs/response-debug-context/src/lib.rs | 6 +-- 16 files changed, 146 insertions(+), 24 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index f046a7024ca1020b21ba431d15f71f4346ee6368..69572ad7e6b57bd3085c99cef1afbd6b3c470a85 100644 GIT binary patch delta 19785 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

w$yi1y7;rynk; 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 diff --git a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs index b5a68a073a..0e045e61e4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs @@ -129,6 +129,7 @@ impl From for CodexErrorInfo { CoreCodexErrorInfo::RateLimitExceeded => CodexErrorInfo::RateLimitExceeded, CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded, CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy, + CoreCodexErrorInfo::BioPolicy => CodexErrorInfo::Other, CoreCodexErrorInfo::MisalignmentPolicyViolation => { CodexErrorInfo::MisalignmentPolicyViolation } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 478903f940..bb94fa5d88 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -4671,6 +4671,7 @@ fn codex_error_info_serializes_http_status_code_in_camel_case() { fn core_error_info_converts_to_camel_case() { for (core, expected) in [ (CoreCodexErrorInfo::CyberPolicy, json!("cyberPolicy")), + (CoreCodexErrorInfo::BioPolicy, json!("other")), ( CoreCodexErrorInfo::RateLimitExceeded, json!("rateLimitExceeded"), diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 5028c711fe..0618a44ee6 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2375,6 +2375,7 @@ fn websocket_error_detail(err: &ApiError) -> String { | ApiError::RateLimit(_) | ApiError::InvalidRequest { .. } | ApiError::CyberPolicy { .. } + | ApiError::BioPolicy { .. } | ApiError::MisalignmentPolicyViolation { .. } | ApiError::ServerOverloaded => format!("handshake error: {err}"), } diff --git a/codex-rs/codex-api/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs index c68a7071a4..d6ff0270ba 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -56,6 +56,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr { ApiError::CyberPolicy { message } => { CodexErr::new(CodexErrorDetails::CyberPolicy { message }) } + ApiError::BioPolicy { message } => CodexErr::new(CodexErrorDetails::BioPolicy { message }), ApiError::MisalignmentPolicyViolation { message, misalignment, @@ -117,16 +118,25 @@ pub fn map_api_error(err: ApiError) -> CodexErr { if status == http::StatusCode::BAD_REQUEST { if let Ok(parsed) = serde_json::from_str::(&body_text) && let Some(error) = parsed.get("error") - && error.get("code").and_then(Value::as_str) - == Some(CYBER_POLICY_ERROR_CODE) + && let Some(code @ (CYBER_POLICY_ERROR_CODE | BIO_POLICY_ERROR_CODE)) = + error.get("code").and_then(Value::as_str) { + let fallback_message = if code == BIO_POLICY_ERROR_CODE { + BIO_POLICY_FALLBACK_MESSAGE + } else { + CYBER_POLICY_FALLBACK_MESSAGE + }; let message = error .get("message") .and_then(Value::as_str) .filter(|message| !message.trim().is_empty()) .map(str::to_string) - .unwrap_or_else(|| CYBER_POLICY_FALLBACK_MESSAGE.to_string()); - CodexErr::new(CodexErrorDetails::CyberPolicy { message }) + .unwrap_or_else(|| fallback_message.to_string()); + if code == BIO_POLICY_ERROR_CODE { + CodexErr::new(CodexErrorDetails::BioPolicy { message }) + } else { + CodexErr::new(CodexErrorDetails::CyberPolicy { message }) + } } else if body_text .contains("The image data you provided does not represent a valid image") { @@ -227,6 +237,8 @@ const X_ERROR_JSON_HEADER: &str = "x-error-json"; const CYBER_POLICY_ERROR_CODE: &str = "cyber_policy"; const CYBER_POLICY_FALLBACK_MESSAGE: &str = "This request has been flagged for possible cybersecurity risk."; +const BIO_POLICY_ERROR_CODE: &str = "bio_policy"; +const BIO_POLICY_FALLBACK_MESSAGE: &str = "This content was flagged for possible biological risk."; const MISALIGNMENT_POLICY_VIOLATION_ERROR_CODE: &str = "misalignment_policy_violation"; const MISALIGNMENT_POLICY_VIOLATION_FALLBACK_MESSAGE: &str = "This request was blocked due to a misalignment policy violation."; diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index de94d21d24..882c1fdf6c 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -181,6 +181,53 @@ fn map_api_error_uses_cyber_policy_fallback_for_missing_message() { ); } +#[test] +fn map_api_error_preserves_bio_policy() { + let err = map_api_error(ApiError::BioPolicy { + message: "This request was blocked by bio policy.".to_string(), + }); + assert_eq!(err.to_codex_protocol_error(), CodexErrorInfo::BioPolicy); + assert_eq!(err.to_string(), "This request was blocked by bio policy."); + assert!(!err.is_retryable()); +} + +#[test] +fn map_api_error_maps_http_and_wrapped_websocket_bio_policy() { + for wrapped in [false, true] { + for message in [ + Some("This request was blocked by bio policy."), + None, + Some(""), + Some(" "), + ] { + let mut body = serde_json::json!({"error": {"code": "bio_policy"}}); + if let Some(message) = message { + body["error"]["message"] = serde_json::json!(message); + } + if wrapped { + body["type"] = serde_json::json!("error"); + body["status"] = serde_json::json!(400); + } + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::BAD_REQUEST, + url: None, + headers: None, + body: Some(body.to_string()), + })); + + let expected = message + .filter(|message| !message.trim().is_empty()) + .unwrap_or("This content was flagged for possible biological risk."); + let CodexErrorDetails::BioPolicy { message } = err.details() else { + panic!("expected CodexErrorDetails::BioPolicy, got {err:?}"); + }; + assert_eq!(message, expected); + assert_eq!(err.to_codex_protocol_error(), CodexErrorInfo::BioPolicy); + assert!(!err.is_retryable()); + } + } +} + #[test] fn map_api_error_maps_misalignment_policy_violation_from_400_body() { assert_misalignment_policy_violation_from_http_body(http::StatusCode::BAD_REQUEST); diff --git a/codex-rs/codex-api/src/error.rs b/codex-rs/codex-api/src/error.rs index d1d83cf401..9c282ba803 100644 --- a/codex-rs/codex-api/src/error.rs +++ b/codex-rs/codex-api/src/error.rs @@ -35,6 +35,8 @@ pub enum ApiError { InvalidRequest { message: String }, #[error("cyber policy: {message}")] CyberPolicy { message: String }, + #[error("bio policy: {message}")] + BioPolicy { message: String }, #[error("misalignment policy violation: {message}")] MisalignmentPolicyViolation { message: String, diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index bb401db1bd..3f867d5d0b 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -429,6 +429,14 @@ pub fn process_responses_event( } else if is_cyber_policy_error(&error) { let message = cyber_policy_message(error.message); response_error = ApiError::CyberPolicy { message }; + } else if error.code.as_deref() == Some("bio_policy") { + let message = error + .message + .filter(|message| !message.trim().is_empty()) + .unwrap_or_else(|| { + "This content was flagged for possible biological risk.".to_string() + }); + response_error = ApiError::BioPolicy { message }; } else if error.code.as_deref() == Some("misalignment_policy_violation") { let message = error .message @@ -443,8 +451,7 @@ pub fn process_responses_event( serde_json::from_value::(details).ok() }), }; - } else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy")) - { + } else if error.code.as_deref() == Some("invalid_prompt") { let message = error .message .unwrap_or_else(|| "Invalid request.".to_string()); @@ -1366,7 +1373,7 @@ mod tests { } #[tokio::test] - async fn content_policy_errors_without_type_are_invalid_requests() { + async fn content_policy_errors_without_type_preserve_their_classification() { for (code, expected_message) in [ ( "invalid_prompt", @@ -1396,8 +1403,9 @@ mod tests { let events = collect_events(&[sse1.as_bytes()]).await; assert_eq!(events.len(), 1); - match &events[0] { - Err(ApiError::InvalidRequest { message }) => { + match (code, &events[0]) { + ("invalid_prompt", Err(ApiError::InvalidRequest { message })) + | ("bio_policy", Err(ApiError::BioPolicy { message })) => { assert_eq!(message, expected_message); } other => panic!("unexpected event for {code}: {other:?}"), @@ -1405,6 +1413,28 @@ mod tests { } } + #[tokio::test] + async fn bio_policy_error_uses_fallback_for_missing_or_blank_message() { + for message in [None, Some(""), Some(" ")] { + let mut event = json!({ + "type": "response.failed", + "response": { "error": { "code": "bio_policy" } }, + }); + if let Some(message) = message { + event["response"]["error"]["message"] = json!(message); + } + let sse = format!("event: response.failed\ndata: {event}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + match events.as_slice() { + [Err(ApiError::BioPolicy { message })] => assert_eq!( + message, + "This content was flagged for possible biological risk." + ), + other => panic!("unexpected events: {other:?}"), + } + } + } + #[tokio::test] async fn table_driven_event_kinds() { struct TestCase { diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index eabc296a76..efd37e51b6 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -38,6 +38,7 @@ const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber"; const CYBER_POLICY_MESSAGE: &str = "This request has been flagged for potentially high-risk cyber activity."; +const BIO_POLICY_MESSAGE: &str = "This request has been flagged for possible biological risk."; fn disabled_text_turn(test: &TestCodex, text: &str) -> TurnInputRequest { let (sandbox_policy, permission_profile) = @@ -105,34 +106,48 @@ async fn openai_model_header_mismatch_emits_warning_event() -> Result<()> { Ok(()) } +#[test_case::test_case("cyber_policy", CYBER_POLICY_MESSAGE, CodexErrorInfo::CyberPolicy; "cyber")] +#[test_case::test_case("bio_policy", BIO_POLICY_MESSAGE, CodexErrorInfo::BioPolicy; "bio")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn cyber_policy_response_emits_typed_error_without_retry() -> Result<()> { +async fn policy_response_emits_typed_error_without_retry( + code: &str, + message: &str, + error_info: CodexErrorInfo, +) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let response = ResponseTemplate::new(400).set_body_json(serde_json::json!({ "error": { - "message": CYBER_POLICY_MESSAGE, + "message": message, "type": "invalid_request", "param": null, - "code": "cyber_policy" + "code": code } })); let mock = mount_response_once(&server, response).await; let mut builder = test_codex().with_model(REQUESTED_MODEL); - let test = builder.build(&server).await?; + let test = builder.build_with_auto_env(&server).await?; test.codex - .start_or_steer_turn(disabled_text_turn(&test, "trigger cyber policy error")) + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "trigger policy error".to_string(), + text_elements: Vec::new(), + }])) .await?; let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; let EventMsg::Error(error) = error else { panic!("expected error event"); }; - assert_eq!(error.message, CYBER_POLICY_MESSAGE); - assert_eq!(error.codex_error_info, Some(CodexErrorInfo::CyberPolicy)); + assert_eq!(error.message, message); + assert_eq!(error.codex_error_info, Some(error_info)); + + let _ = wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; mock.single_request(); diff --git a/codex-rs/ext/guardian-reviewer/src/retry.rs b/codex-rs/ext/guardian-reviewer/src/retry.rs index 79d516215b..e1af53f466 100644 --- a/codex-rs/ext/guardian-reviewer/src/retry.rs +++ b/codex-rs/ext/guardian-reviewer/src/retry.rs @@ -100,6 +100,7 @@ fn should_retry_guardian_review(outcome: &GuardianReviewOutcome) -> bool { | CodexErrorInfo::SessionBudgetExceeded | CodexErrorInfo::UsageLimitExceeded | CodexErrorInfo::CyberPolicy + | CodexErrorInfo::BioPolicy | CodexErrorInfo::MisalignmentPolicyViolation | CodexErrorInfo::Unauthorized | CodexErrorInfo::BadRequest diff --git a/codex-rs/ext/guardian-reviewer/src/retry_tests.rs b/codex-rs/ext/guardian-reviewer/src/retry_tests.rs index ce335db96f..cdc80f2490 100644 --- a/codex-rs/ext/guardian-reviewer/src/retry_tests.rs +++ b/codex-rs/ext/guardian-reviewer/src/retry_tests.rs @@ -92,6 +92,13 @@ fn guardian_review_retry_only_retries_transient_session_and_parse_errors() { )), false, ), + ( + GuardianReviewOutcome::Error(GuardianReviewError::session_with_error_info( + anyhow::anyhow!("bio policy"), + CodexErrorInfo::BioPolicy, + )), + false, + ), ( GuardianReviewOutcome::Error(GuardianReviewError::parse(anyhow::anyhow!("parse"))), true, diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs b/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs index 4ecc681ffe..21fbd94e57 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs @@ -49,9 +49,9 @@ pub(super) fn sampler_failure_reason(error: &LunaSamplerError) -> &'static str { ApiError::Retryable { .. } => "retryable_api_error", ApiError::RateLimitExceeded { .. } | ApiError::RateLimit(_) => "rate_limit", ApiError::InvalidRequest { .. } => "invalid_request", - ApiError::CyberPolicy { .. } | ApiError::MisalignmentPolicyViolation { .. } => { - "policy_error" - } + ApiError::CyberPolicy { .. } + | ApiError::BioPolicy { .. } + | ApiError::MisalignmentPolicyViolation { .. } => "policy_error", ApiError::ServerOverloaded => "server_overloaded", }, } diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs index c9c69bb95c..b8dd61c799 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs @@ -91,7 +91,8 @@ impl SamplingExecution { | ApiError::RateLimit(_) | ApiError::InvalidRequest { .. } | ApiError::MisalignmentPolicyViolation { .. } - | ApiError::CyberPolicy { .. }, + | ApiError::CyberPolicy { .. } + | ApiError::BioPolicy { .. }, ) => false, }; if retryable && *retries < MAX_SAMPLING_RETRIES { diff --git a/codex-rs/protocol/src/error.rs b/codex-rs/protocol/src/error.rs index fad51f2b96..b62943646d 100644 --- a/codex-rs/protocol/src/error.rs +++ b/codex-rs/protocol/src/error.rs @@ -138,6 +138,8 @@ pub enum CodexErrorDetails { #[error("{message}")] CyberPolicy { message: String }, #[error("{message}")] + BioPolicy { message: String }, + #[error("{message}")] MisalignmentPolicyViolation { message: String, misalignment: Option, @@ -394,6 +396,7 @@ impl CodexErr { | CodexErrorDetails::UsageLimitReached(_) | CodexErrorDetails::ServerOverloaded | CodexErrorDetails::CyberPolicy { .. } + | CodexErrorDetails::BioPolicy { .. } | CodexErrorDetails::MisalignmentPolicyViolation { .. } => false, CodexErrorDetails::Stream(..) | CodexErrorDetails::RateLimitExceeded(_) @@ -439,6 +442,7 @@ impl CodexErr { | CodexErrorDetails::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded, CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded, CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy, + CodexErrorDetails::BioPolicy { .. } => CodexErrorInfo::BioPolicy, CodexErrorDetails::MisalignmentPolicyViolation { .. } => { CodexErrorInfo::MisalignmentPolicyViolation } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 355181ff69..38f653b524 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1858,6 +1858,7 @@ pub enum CodexErrorInfo { RateLimitExceeded, ServerOverloaded, CyberPolicy, + BioPolicy, MisalignmentPolicyViolation, HttpConnectionFailed { http_status_code: Option, @@ -1899,6 +1900,7 @@ impl CodexErrorInfo { | Self::RateLimitExceeded | Self::ServerOverloaded | Self::CyberPolicy + | Self::BioPolicy | Self::MisalignmentPolicyViolation | Self::HttpConnectionFailed { .. } | Self::ResponseStreamConnectionFailed { .. } diff --git a/codex-rs/response-debug-context/src/lib.rs b/codex-rs/response-debug-context/src/lib.rs index 4754705490..1a32b91cd9 100644 --- a/codex-rs/response-debug-context/src/lib.rs +++ b/codex-rs/response-debug-context/src/lib.rs @@ -19,10 +19,7 @@ pub struct ResponseDebugContext { pub fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext { let mut context = ResponseDebugContext::default(); - let TransportError::Http { - headers, body: _, .. - } = transport - else { + let TransportError::Http { headers, .. } = transport else { return context; }; @@ -85,6 +82,7 @@ pub fn telemetry_api_error_message(error: &ApiError) -> String { ApiError::RateLimit(_) => "rate limit".to_string(), ApiError::InvalidRequest { .. } => "invalid request".to_string(), ApiError::CyberPolicy { .. } => "cyber policy".to_string(), + ApiError::BioPolicy { .. } => "bio policy".to_string(), ApiError::MisalignmentPolicyViolation { .. } => "misalignment policy violation".to_string(), ApiError::ServerOverloaded => "server overloaded".to_string(), }