mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
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
This commit is contained in:
@@ -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::<Value>(&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.";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<MisalignmentErrorDetails>(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 {
|
||||
|
||||
Reference in New Issue
Block a user