mirror of
https://github.com/openai/codex.git
synced 2026-11-11 14:32:25 +00:00
Expose resumable misalignment details through app server (#40952)
## Why App-server clients need the explanation and steering instruction returned with a misalignment policy violation to offer a user-confirmed continuation. Missing or incomplete details must continue to behave as a terminal block. ## What changed - Parse optional misalignment classification, explanation, and steering details from streamed and HTTP Responses errors and propagate them through core errors. - Include the details in live app-server `error` and `turn/completed` payloads and export the corresponding protocol schemas and TypeScript types. - Keep explanations and steering messages out of serialized rollout events and redact them from debug output. - Document how clients can resume with `turn/start` after user confirmation. ## Testing - Cover streamed, HTTP, and WebSocket-wrapped errors, including malformed and classification-only details. - Verify live app-server notifications expose resumable details without writing the explanation or steering message to the rollout. GitOrigin-RevId: 329258a444c2cd91d0c57ab1720830b33ddcfac5
This commit is contained in:
1 parent
0e9a2bae52
commit
7276d67081
63 files changed
+1604
-15
No files matched your search
@@ -9,6 +9,7 @@ use crate::telemetry::SseTelemetry;
|
||||
use codex_client::ByteStream;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::MisalignmentErrorDetails;
|
||||
use codex_protocol::protocol::ModelVerification;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
@@ -107,6 +108,8 @@ struct Error {
|
||||
message: Option<String>,
|
||||
plan_type: Option<String>,
|
||||
resets_at: Option<i64>,
|
||||
#[serde(default)]
|
||||
misalignment: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -428,7 +431,12 @@ pub fn process_responses_event(
|
||||
"This request was blocked due to a misalignment policy violation."
|
||||
.to_string()
|
||||
});
|
||||
response_error = ApiError::MisalignmentPolicyViolation { message };
|
||||
response_error = ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment: error.misalignment.and_then(|details| {
|
||||
serde_json::from_value::<MisalignmentErrorDetails>(details).ok()
|
||||
}),
|
||||
};
|
||||
} else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy"))
|
||||
{
|
||||
let message = error
|
||||
@@ -1212,8 +1220,12 @@ mod tests {
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
Err(ApiError::MisalignmentPolicyViolation { message }) => {
|
||||
Err(ApiError::MisalignmentPolicyViolation {
|
||||
message,
|
||||
misalignment,
|
||||
}) => {
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(misalignment, &None);
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
@@ -1239,7 +1251,7 @@ mod tests {
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
Err(ApiError::MisalignmentPolicyViolation { message }) => assert_eq!(
|
||||
Err(ApiError::MisalignmentPolicyViolation { message, .. }) => assert_eq!(
|
||||
message,
|
||||
"This request was blocked due to a misalignment policy violation."
|
||||
),
|
||||
@@ -1248,6 +1260,78 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_policy_violation_preserves_public_continuation_details() {
|
||||
let raw_error = json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp_fatal_misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"misalignment": {
|
||||
"error_type": "future_safety_category",
|
||||
"detailed_explanation": "The agent attempted an external transfer.",
|
||||
"steer": { "message": "Do not transfer the user's files." }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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,
|
||||
misalignment,
|
||||
}) => {
|
||||
assert_eq!(message, "This request violated the misalignment policy.");
|
||||
assert_eq!(
|
||||
misalignment,
|
||||
&Some(MisalignmentErrorDetails {
|
||||
error_type: Some("future_safety_category".to_string()),
|
||||
detailed_explanation: Some(
|
||||
"The agent attempted an external transfer.".to_string()
|
||||
),
|
||||
steer: Some(codex_protocol::protocol::MisalignmentSteer {
|
||||
message: "Do not transfer the user's files.".to_string(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_misalignment_details_preserve_the_fatal_policy_error() {
|
||||
let raw_error = json!({
|
||||
"type": "response.failed",
|
||||
"response": {
|
||||
"id": "resp_fatal_misalignment",
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "misalignment_policy_violation",
|
||||
"message": "This request violated the misalignment policy.",
|
||||
"misalignment": { "steer": { "message": 42 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
let sse = format!("event: response.failed\ndata: {raw_error}\n\n");
|
||||
let events = collect_events(&[sse.as_bytes()]).await;
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_matches!(
|
||||
&events[0],
|
||||
Err(ApiError::MisalignmentPolicyViolation {
|
||||
misalignment: None,
|
||||
..
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_policy_errors_without_type_are_invalid_requests() {
|
||||
for (code, expected_message) in [
|
||||
@@ -1913,6 +1997,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
|
||||
let delay = try_parse_retry_after(&err);
|
||||
@@ -1927,6 +2012,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
let delay = try_parse_retry_after(&err);
|
||||
assert_eq!(delay, Some(Duration::from_secs_f64(1.898)));
|
||||
@@ -1940,6 +2026,7 @@ mod tests {
|
||||
code: Some("rate_limit_exceeded".to_string()),
|
||||
plan_type: None,
|
||||
resets_at: None,
|
||||
misalignment: None,
|
||||
};
|
||||
let delay = try_parse_retry_after(&err);
|
||||
assert_eq!(delay, Some(Duration::from_secs(35)));
|
||||
|
||||
Reference in new issue
Block a user