Add safety check notification and error handling (#19055)

Adds a new app-server notification that fires when a user account has
been flagged for potential safety reasons.
This commit is contained in:
Eric Traut
2026-04-22 22:24:12 -07:00
committed by GitHub
parent 02170996e6
commit bbff4ee61a
61 changed files with 1414 additions and 15 deletions

View File

@@ -632,6 +632,17 @@ pub fn ev_response_created(id: &str) -> Value {
})
}
pub fn ev_model_verification_metadata(id: &str, verifications: Vec<&str>) -> Value {
serde_json::json!({
"type": "response.metadata",
"sequence_number": 1,
"response_id": id,
"metadata": {
"openai_verification_recommendation": verifications,
}
})
}
pub fn ev_completed_with_tokens(id: &str, total_tokens: i64) -> Value {
serde_json::json!({
"type": "response.completed",

View File

@@ -2,13 +2,16 @@ use anyhow::Result;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_model_verification_metadata;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_response_once;
use core_test_support::responses::mount_response_sequence;
@@ -20,9 +23,14 @@ use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use wiremock::ResponseTemplate;
const SERVER_MODEL: &str = "gpt-5.2";
const REQUESTED_MODEL: &str = "gpt-5.3-codex";
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.";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> Result<()> {
@@ -113,6 +121,57 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cyber_policy_response_emits_typed_error_without_retry() -> 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,
"type": "invalid_request",
"param": null,
"code": "cyber_policy"
}
}));
let mock = mount_response_once(&server, response).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger cyber policy error".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.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));
mock.single_request();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn response_model_field_mismatch_emits_warning_when_header_matches_requested() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -318,3 +377,150 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_verification_emits_structured_event_without_reroute_or_warning() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = sse_response(sse(vec![
ev_response_created("resp-1"),
ev_model_verification_metadata("resp-1", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
core_test_support::responses::ev_completed("resp-1"),
]));
let _mock = mount_response_once(&server, response).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let mut verification_count = 0;
let mut reroute_count = 0;
let mut warning_count = 0;
let mut warning_item_count = 0;
loop {
let event = wait_for_event(&test.codex, |_| true).await;
match event {
EventMsg::ModelVerification(event) => {
assert_eq!(
event.verifications,
vec![ModelVerification::TrustedAccessForCyber]
);
verification_count += 1;
}
EventMsg::Warning(_) => warning_count += 1,
EventMsg::ModelReroute(_) => reroute_count += 1,
EventMsg::RawResponseItem(raw)
if matches!(
&raw.item,
ResponseItem::Message { content, .. }
if content.iter().any(|item| matches!(
item,
ContentItem::InputText { text } if text.starts_with("Warning: ")
))
) =>
{
warning_item_count += 1;
}
EventMsg::TurnComplete(_) => break,
_ => {}
}
}
assert_eq!(verification_count, 1);
assert_eq!(reroute_count, 0);
assert_eq!(warning_count, 0);
assert_eq!(warning_item_count, 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_verification_only_emits_once_per_turn() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let tool_args = serde_json::json!({
"command": "echo hello",
"timeout_ms": 1_000
});
let first_response = sse_response(sse(vec![
ev_response_created("resp-1"),
ev_function_call(
"call-1",
"shell_command",
&serde_json::to_string(&tool_args)?,
),
ev_model_verification_metadata("resp-1", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
core_test_support::responses::ev_completed("resp-1"),
]));
let second_response = sse_response(sse(vec![
ev_response_created("resp-2"),
ev_model_verification_metadata("resp-2", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
ev_assistant_message("msg-1", "done"),
core_test_support::responses::ev_completed("resp-2"),
]));
let _mock = mount_response_sequence(&server, vec![first_response, second_response]).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger follow-up model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let mut verification_count = 0;
loop {
let event = wait_for_event(&test.codex, |_| true).await;
match event {
EventMsg::ModelVerification(_) => verification_count += 1,
EventMsg::Warning(warning) if warning.message.contains("high-risk cyber activity") => {
panic!("model verification should not emit a warning event");
}
EventMsg::TurnComplete(_) => break,
_ => {}
}
}
assert_eq!(verification_count, 1);
Ok(())
}