diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index a12721c12f..ea6ab7f963 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -606,6 +606,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", 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 02f6e514c1..b29284db0b 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 @@ -7573,6 +7573,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", 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 e5add5a626..7e5d4ca22d 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 @@ -3691,6 +3691,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json index 101cd1d778..36721e3704 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json @@ -11,6 +11,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 8f2b83b537..a909e681ee 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -34,6 +34,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 8d3f71feea..5876704afc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -115,6 +115,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index ed476d2c2a..98aef0c946 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 3d66bfef44..bcebfc360c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 13cf57b6ab..16a20062b3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 6fc3dbbc12..2fb3538f85 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -115,6 +115,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 3ecebc79c4..a27e02cca5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 4b07755c40..2ece3204c0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -115,6 +115,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 3132907fef..c53a7f8937 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 0a14a81681..5451f70940 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -37,6 +37,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 40dc021878..bc7a0e1b6c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -34,6 +34,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 4e4505228a..48e799fd15 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -34,6 +34,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 927306c05f..de4b894c06 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -34,6 +34,7 @@ "usageLimitExceeded", "serverOverloaded", "cyberPolicy", + "misalignmentPolicyViolation", "internalServerError", "unauthorized", "badRequest", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index ae26f19844..14d34815a2 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ 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 a78af9ad45..d9f5acc201 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CodexErrorInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CodexErrorInfo.ts index ec50328e1d..142048048f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CodexErrorInfo.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CodexErrorInfo.ts @@ -9,4 +9,4 @@ import type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; * When an upstream HTTP status is available (for example, from the Responses API or a provider), * it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. */ -export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other"; +export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" | "misalignmentPolicyViolation" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other"; 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 340c0ab496..ddcf232b47 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs @@ -80,6 +80,7 @@ pub enum CodexErrorInfo { UsageLimitExceeded, ServerOverloaded, CyberPolicy, + MisalignmentPolicyViolation, HttpConnectionFailed { #[serde(rename = "httpStatusCode")] #[ts(rename = "httpStatusCode")] @@ -126,6 +127,9 @@ impl From for CodexErrorInfo { CoreCodexErrorInfo::UsageLimitExceeded => CodexErrorInfo::UsageLimitExceeded, CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded, CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy, + CoreCodexErrorInfo::MisalignmentPolicyViolation => { + CodexErrorInfo::MisalignmentPolicyViolation + } CoreCodexErrorInfo::HttpConnectionFailed { http_status_code } => { CodexErrorInfo::HttpConnectionFailed { http_status_code } } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3fcdd5f05c..d9a534a22f 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1662,6 +1662,7 @@ There are additional item-specific events: - `ContextWindowExceeded` - `SessionBudgetExceeded` - `UsageLimitExceeded` +- `misalignmentPolicyViolation`: a non-retryable request blocked by the misalignment policy - `HttpConnectionFailed { httpStatusCode? }`: upstream HTTP failures including 4xx/5xx - `ResponseStreamConnectionFailed { httpStatusCode? }`: failure to connect to the response SSE stream - `ResponseStreamDisconnected { httpStatusCode? }`: disconnect of the response SSE stream in the middle of a turn before completion diff --git a/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs b/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs new file mode 100644 index 0000000000..375b49dd07 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs @@ -0,0 +1,149 @@ +use anyhow::Result; +use app_test_support::MockResponsesConfig; +use app_test_support::TestAppServer; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::CodexErrorInfo; +use codex_app_server_protocol::ErrorNotification; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnCompletedNotification; +use codex_app_server_protocol::TurnError; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use pretty_assertions::assert_eq; +use tempfile::TempDir; +use tokio::time::timeout; +use wiremock::ResponseTemplate; + +const MESSAGE: &str = "This request violated the misalignment policy."; + +#[tokio::test] +async fn streamed_policy_violation_completes_turn_with_typed_terminal_error() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = responses::sse_response(responses::sse(vec![ + responses::ev_response_created("resp-misalignment"), + serde_json::json!({ + "type": "response.failed", + "response": { + "id": "resp-misalignment", + "status": "failed", + "error": { + "type": "invalid_request_error", + "code": "misalignment_policy_violation", + "message": MESSAGE + } + } + }), + ])); + + assert_policy_violation_completes_turn_with_typed_terminal_error(response).await +} + +#[tokio::test] +async fn http_400_policy_violation_completes_turn_with_typed_terminal_error() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = ResponseTemplate::new(400).set_body_json(serde_json::json!({ + "error": { + "type": "invalid_request_error", + "code": "misalignment_policy_violation", + "message": MESSAGE, + } + })); + + assert_policy_violation_completes_turn_with_typed_terminal_error(response).await +} + +#[tokio::test] +async fn http_403_policy_violation_completes_turn_with_typed_terminal_error() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = ResponseTemplate::new(403).set_body_json(serde_json::json!({ + "error": { + "type": "invalid_request_error", + "code": "misalignment_policy_violation", + "message": MESSAGE, + } + })); + + assert_policy_violation_completes_turn_with_typed_terminal_error(response).await +} + +async fn assert_policy_violation_completes_turn_with_typed_terminal_error( + response: ResponseTemplate, +) -> Result<()> { + let server = responses::start_mock_server().await; + let response_mock = responses::mount_response_once(&server, response).await; + + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; + + let mut app_server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized() + .await?; + let ThreadStartResponse { thread, .. } = app_server + .start_thread(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let TurnStartResponse { turn } = app_server + .request(|request_id| ClientRequest::TurnStart { + request_id, + params: TurnStartParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: vec![UserInput::Text { + text: "trigger policy violation".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }, + }) + .await?; + + let error: ErrorNotification = timeout( + std::time::Duration::from_secs(10), + app_server.read_notification("error"), + ) + .await??; + assert_eq!( + error, + ErrorNotification { + error: TurnError { + message: MESSAGE.to_string(), + codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), + additional_details: None, + }, + will_retry: false, + thread_id: thread.id.clone(), + turn_id: turn.id.clone(), + } + ); + + let completed: TurnCompletedNotification = timeout( + std::time::Duration::from_secs(10), + app_server.read_notification("turn/completed"), + ) + .await??; + assert_eq!(completed.thread_id, thread.id); + assert_eq!(completed.turn.id, turn.id); + assert_eq!(completed.turn.status, TurnStatus::Failed); + assert_eq!( + completed.turn.error, + Some(TurnError { + message: MESSAGE.to_string(), + codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), + additional_details: None, + }) + ); + response_mock.single_request(); + + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index d473fcfab5..553819b7dd 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -45,6 +45,7 @@ mod mcp_server_elicitation; mod mcp_server_status; mod mcp_tool; mod memory_reset; +mod misalignment_policy; mod model_auto_review; mod model_list; mod model_provider_capabilities_read; diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 6a7d8a8d5d..712d63984c 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2493,6 +2493,7 @@ fn websocket_error_detail(err: &ApiError) -> String { | ApiError::RateLimit(_) | ApiError::InvalidRequest { .. } | ApiError::CyberPolicy { .. } + | 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 37271934b4..aa536e0a25 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -48,6 +48,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr { ApiError::CyberPolicy { message } => { CodexErr::new(CodexErrorDetails::CyberPolicy { message }) } + ApiError::MisalignmentPolicyViolation { message } => { + CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { message }) + } ApiError::Transport(transport) => match transport { TransportError::Http { status, @@ -70,6 +73,26 @@ pub fn map_api_error(err: ApiError) -> CodexErr { return CodexErr::ServerOverloaded; } + if (status == http::StatusCode::BAD_REQUEST + || status == http::StatusCode::FORBIDDEN) + && let Ok(parsed) = serde_json::from_str::(&body_text) + && let Some(error) = parsed.get("error") + && error.get("code").and_then(Value::as_str) + == Some(MISALIGNMENT_POLICY_VIOLATION_ERROR_CODE) + { + let message = error + .get("message") + .and_then(Value::as_str) + .filter(|message| !message.trim().is_empty()) + .map(str::to_string) + .unwrap_or_else(|| { + MISALIGNMENT_POLICY_VIOLATION_FALLBACK_MESSAGE.to_string() + }); + return CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { + message, + }); + } + if status == http::StatusCode::BAD_REQUEST { if let Ok(parsed) = serde_json::from_str::(&body_text) && let Some(error) = parsed.get("error") @@ -167,6 +190,9 @@ 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 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."; const CLOUDFLARE_BLOCKED_MESSAGE: &str = "Access blocked by Cloudflare. This usually happens when connecting from a restricted region"; diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index 391b4f5ea8..62e153be6c 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -146,6 +146,39 @@ fn map_api_error_uses_cyber_policy_fallback_for_missing_message() { ); } +#[test] +fn map_api_error_maps_misalignment_policy_violation_from_400_body() { + assert_misalignment_policy_violation_from_http_body(http::StatusCode::BAD_REQUEST); +} + +#[test] +fn map_api_error_maps_misalignment_policy_violation_from_403_body() { + assert_misalignment_policy_violation_from_http_body(http::StatusCode::FORBIDDEN); +} + +fn assert_misalignment_policy_violation_from_http_body(status: http::StatusCode) { + let body = serde_json::json!({ + "error": { + "message": "This request violated the misalignment policy.", + "type": "invalid_request_error", + "code": "misalignment_policy_violation" + } + }) + .to_string(); + let err = map_api_error(ApiError::Transport(TransportError::Http { + status, + url: Some("http://example.com/v1/responses".to_string()), + headers: None, + body: Some(body), + })); + + let CodexErrorDetails::MisalignmentPolicyViolation { message } = err.details() else { + panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); + }; + assert_eq!(message, "This request violated the misalignment policy."); + assert!(!err.is_retryable()); +} + #[test] fn map_api_error_keeps_unknown_400_errors_generic() { let body = serde_json::json!({ diff --git a/codex-rs/codex-api/src/error.rs b/codex-rs/codex-api/src/error.rs index c6cb5fd405..7929326aad 100644 --- a/codex-rs/codex-api/src/error.rs +++ b/codex-rs/codex-api/src/error.rs @@ -29,6 +29,8 @@ pub enum ApiError { InvalidRequest { message: String }, #[error("cyber policy: {message}")] CyberPolicy { message: String }, + #[error("misalignment policy violation: {message}")] + MisalignmentPolicyViolation { message: String }, #[error("server overloaded")] ServerOverloaded, } diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index 233186dc6d..3712b28c78 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -420,6 +420,15 @@ 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("misalignment_policy_violation") { + let message = error + .message + .filter(|message| !message.trim().is_empty()) + .unwrap_or_else(|| { + "This request was blocked due to a misalignment policy violation." + .to_string() + }); + response_error = ApiError::MisalignmentPolicyViolation { message }; } else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy")) { let message = error @@ -1148,6 +1157,51 @@ mod tests { } } + #[tokio::test] + async fn misalignment_policy_violation_error_is_fatal() { + let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_fatal_misalignment","object":"response","status":"failed","error":{"type":"invalid_request_error","code":"misalignment_policy_violation","message":"This request violated the misalignment policy."}}}"#; + + let sse = format!("event: response.failed\ndata: {raw_error}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + + assert_eq!(events.len(), 1); + match &events[0] { + Err(ApiError::MisalignmentPolicyViolation { message }) => { + assert_eq!(message, "This request violated the misalignment policy."); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[tokio::test] + async fn misalignment_policy_violation_uses_fallback_for_blank_message() { + for message in ["", " "] { + let raw_error = serde_json::json!({ + "type": "response.failed", + "response": { + "id": "resp_fatal_misalignment", + "status": "failed", + "error": { + "type": "invalid_request_error", + "code": "misalignment_policy_violation", + "message": message, + }, + }, + }); + let sse = format!("event: response.failed\ndata: {raw_error}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + + assert_eq!(events.len(), 1); + match &events[0] { + Err(ApiError::MisalignmentPolicyViolation { message }) => assert_eq!( + message, + "This request was blocked due to a misalignment policy violation." + ), + other => panic!("unexpected event: {other:?}"), + } + } + } + #[tokio::test] async fn content_policy_errors_without_type_are_invalid_requests() { for (code, expected_message) in [ diff --git a/codex-rs/protocol/src/error.rs b/codex-rs/protocol/src/error.rs index 33ef62273a..f503093d92 100644 --- a/codex-rs/protocol/src/error.rs +++ b/codex-rs/protocol/src/error.rs @@ -132,6 +132,8 @@ pub enum CodexErrorDetails { ServerOverloaded, #[error("{message}")] CyberPolicy { message: String }, + #[error("{message}")] + MisalignmentPolicyViolation { message: String }, #[error("{0}")] ResponseStreamFailed(ResponseStreamFailed), #[error("{0}")] @@ -383,7 +385,8 @@ impl CodexErr { | CodexErrorDetails::SessionConfiguredNotFirstEvent | CodexErrorDetails::UsageLimitReached(_) | CodexErrorDetails::ServerOverloaded - | CodexErrorDetails::CyberPolicy { .. } => false, + | CodexErrorDetails::CyberPolicy { .. } + | CodexErrorDetails::MisalignmentPolicyViolation { .. } => false, CodexErrorDetails::Stream(..) | CodexErrorDetails::Timeout | CodexErrorDetails::RequestTimeout @@ -426,6 +429,9 @@ impl CodexErr { | CodexErrorDetails::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded, CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded, CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy, + CodexErrorDetails::MisalignmentPolicyViolation { .. } => { + CodexErrorInfo::MisalignmentPolicyViolation + } CodexErrorDetails::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code: self.http_status_code_value(), }, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index bd5579987b..c8b5992b7a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1770,6 +1770,7 @@ pub enum CodexErrorInfo { UsageLimitExceeded, ServerOverloaded, CyberPolicy, + MisalignmentPolicyViolation, HttpConnectionFailed { http_status_code: Option, }, @@ -1808,6 +1809,7 @@ impl CodexErrorInfo { | Self::UsageLimitExceeded | Self::ServerOverloaded | Self::CyberPolicy + | Self::MisalignmentPolicyViolation | Self::HttpConnectionFailed { .. } | Self::ResponseStreamConnectionFailed { .. } | Self::InternalServerError diff --git a/codex-rs/response-debug-context/src/lib.rs b/codex-rs/response-debug-context/src/lib.rs index cee5799bab..822210d144 100644 --- a/codex-rs/response-debug-context/src/lib.rs +++ b/codex-rs/response-debug-context/src/lib.rs @@ -83,6 +83,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::MisalignmentPolicyViolation { .. } => "misalignment policy violation".to_string(), ApiError::ServerOverloaded => "server overloaded".to_string(), } }