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
@@ -400,6 +400,9 @@ fn server_notification_thread_target(
}
ServerNotification::ContextCompacted(notification) => Some(notification.thread_id.as_str()),
ServerNotification::ModelRerouted(notification) => Some(notification.thread_id.as_str()),
ServerNotification::ModelVerification(notification) => {
Some(notification.thread_id.as_str())
}
ServerNotification::ThreadRealtimeStarted(notification) => {
Some(notification.thread_id.as_str())
}
+52
View File
@@ -97,6 +97,7 @@ use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::ModelVerification as AppServerModelVerification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadItem;
@@ -194,6 +195,8 @@ use codex_protocol::protocol::McpStartupStatus;
use codex_protocol::protocol::McpStartupUpdateEvent;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
#[cfg(test)]
use codex_protocol::protocol::ModelVerification as CoreModelVerification;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::PatchApplyBeginEvent;
use codex_protocol::protocol::RateLimitReachedType;
@@ -256,6 +259,7 @@ const MULTI_AGENT_ENABLE_TITLE: &str = "Enable subagents?";
const MULTI_AGENT_ENABLE_YES: &str = "Yes, enable";
const MULTI_AGENT_ENABLE_NO: &str = "Not now";
const MULTI_AGENT_ENABLE_NOTICE: &str = "Subagents will be enabled in the next session.";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING: &str = "Your account was flagged for potentially high-risk cyber activity. Requests may be slower while additional verification is applied. To regain faster access, apply for trusted access: https://chatgpt.com/cyber or learn more: https://developers.openai.com/codex/concepts/cyber-safety";
const MEMORIES_DOC_URL: &str = "https://developers.openai.com/codex/memories";
const MEMORIES_ENABLE_TITLE: &str = "Enable memories?";
const MEMORIES_ENABLE_YES: &str = "Yes, enable";
@@ -651,6 +655,15 @@ fn app_server_rate_limit_error_kind(info: &AppServerCodexErrorInfo) -> Option<Ra
}
}
#[cfg(test)]
fn is_core_cyber_policy_error(info: &CoreCodexErrorInfo) -> bool {
matches!(info, CoreCodexErrorInfo::CyberPolicy)
}
fn is_app_server_cyber_policy_error(info: &AppServerCodexErrorInfo) -> bool {
matches!(info, AppServerCodexErrorInfo::CyberPolicy)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum ExternalEditorState {
#[default]
@@ -3040,6 +3053,16 @@ impl ChatWidget {
self.maybe_send_next_queued_input();
}
fn on_cyber_policy_error(&mut self) {
self.submit_pending_steers_after_interrupt = false;
self.finalize_turn();
self.add_to_history(history_cell::new_cyber_policy_error_event());
self.request_redraw();
// After an error ends the turn, try sending the next queued input.
self.maybe_send_next_queued_input();
}
fn workspace_owner_usage_nudge_enabled(&self) -> bool {
self.config
.features
@@ -3105,6 +3128,11 @@ impl ChatWidget {
.as_ref()
.is_some_and(|info| self.handle_app_server_steer_rejected_error(info))
{
} else if codex_error_info
.as_ref()
.is_some_and(is_app_server_cyber_policy_error)
{
self.on_cyber_policy_error();
} else if let Some(info) = codex_error_info
.as_ref()
.and_then(app_server_rate_limit_error_kind)
@@ -3125,6 +3153,19 @@ impl ChatWidget {
self.request_redraw();
}
#[cfg(test)]
fn on_core_model_verification(&mut self, verifications: &[CoreModelVerification]) {
if verifications.contains(&CoreModelVerification::TrustedAccessForCyber) {
self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING);
}
}
fn on_app_server_model_verification(&mut self, verifications: &[AppServerModelVerification]) {
if verifications.contains(&AppServerModelVerification::TrustedAccessForCyber) {
self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING);
}
}
/// Record one MCP startup update, promoting it into either the active startup
/// round or a buffered "next" round.
///
@@ -6579,6 +6620,9 @@ impl ChatWidget {
self.refresh_skills_for_current_cwd(/*force_reload*/ true);
}
ServerNotification::ModelRerouted(_) => {}
ServerNotification::ModelVerification(notification) => {
self.on_app_server_model_verification(&notification.verifications)
}
ServerNotification::Warning(notification) => self.on_warning(notification.message),
ServerNotification::GuardianWarning(notification) => {
self.on_warning(notification.message)
@@ -7084,6 +7128,9 @@ impl ChatWidget {
| EventMsg::GuardianWarning(WarningEvent { message }) => self.on_warning(message),
EventMsg::GuardianAssessment(ev) => self.on_guardian_assessment(ev),
EventMsg::ModelReroute(_) => {}
EventMsg::ModelVerification(event) => {
self.on_core_model_verification(&event.verifications)
}
EventMsg::Error(ErrorEvent {
message,
codex_error_info,
@@ -7092,6 +7139,11 @@ impl ChatWidget {
.as_ref()
.is_some_and(|info| self.handle_steer_rejected_error(info))
{
} else if codex_error_info
.as_ref()
.is_some_and(is_core_cyber_policy_error)
{
self.on_cyber_policy_error();
} else if let Some(kind) = codex_error_info
.as_ref()
.and_then(core_rate_limit_error_kind)
+5
View File
@@ -70,6 +70,8 @@ pub(super) use codex_app_server_protocol::MarketplaceInterface;
pub(super) use codex_app_server_protocol::McpServerStartupState;
pub(super) use codex_app_server_protocol::McpServerStatusDetail;
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
pub(super) use codex_app_server_protocol::ModelVerification as AppServerModelVerification;
pub(super) use codex_app_server_protocol::ModelVerificationNotification;
pub(super) use codex_app_server_protocol::PatchApplyStatus as AppServerPatchApplyStatus;
pub(super) use codex_app_server_protocol::PatchChangeKind;
pub(super) use codex_app_server_protocol::PermissionsRequestApprovalParams as AppServerPermissionsRequestApprovalParams;
@@ -147,6 +149,7 @@ pub(super) use codex_protocol::protocol::CodexErrorInfo;
pub(super) use codex_protocol::protocol::CollabAgentSpawnBeginEvent;
pub(super) use codex_protocol::protocol::CollabAgentSpawnEndEvent;
pub(super) use codex_protocol::protocol::CreditsSnapshot;
pub(super) use codex_protocol::protocol::ErrorEvent;
pub(super) use codex_protocol::protocol::Event;
pub(super) use codex_protocol::protocol::EventMsg;
pub(super) use codex_protocol::protocol::ExecApprovalRequestEvent;
@@ -169,6 +172,8 @@ pub(super) use codex_protocol::protocol::ItemCompletedEvent;
pub(super) use codex_protocol::protocol::McpStartupCompleteEvent;
pub(super) use codex_protocol::protocol::McpStartupStatus;
pub(super) use codex_protocol::protocol::McpStartupUpdateEvent;
pub(super) use codex_protocol::protocol::ModelVerification as CoreModelVerification;
pub(super) use codex_protocol::protocol::ModelVerificationEvent;
pub(super) use codex_protocol::protocol::NonSteerableTurnKind;
pub(super) use codex_protocol::protocol::Op;
pub(super) use codex_protocol::protocol::PatchApplyBeginEvent;
@@ -678,6 +678,71 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
assert!(!chat.bottom_pane.is_task_running());
}
#[tokio::test]
async fn live_app_server_cyber_policy_error_renders_dedicated_notice() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.handle_server_notification(
ServerNotification::TurnStarted(TurnStartedNotification {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
);
drain_insert_history(&mut rx);
chat.handle_server_notification(
ServerNotification::Error(ErrorNotification {
error: AppServerTurnError {
message: "server fallback message".to_string(),
codex_error_info: Some(CodexErrorInfo::CyberPolicy.into()),
additional_details: None,
},
will_retry: false,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(rendered.contains("This chat was flagged for possible cybersecurity risk"));
assert!(rendered.contains("Trusted Access for Cyber"));
assert!(!rendered.contains("server fallback message"));
assert!(!chat.bottom_pane.is_task_running());
}
#[tokio::test]
async fn live_app_server_model_verification_renders_warning() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.handle_server_notification(
ServerNotification::ModelVerification(ModelVerificationNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
verifications: vec![AppServerModelVerification::TrustedAccessForCyber],
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(rendered.contains("flagged for potentially high-risk cyber activity"));
assert!(rendered.contains("slower while additional verification"));
assert!(rendered.contains("https://chatgpt.com/cyber"));
}
#[tokio::test]
async fn live_app_server_invalid_thread_name_update_is_ignored() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -28,6 +28,45 @@ async fn token_count_none_resets_context_indicator() {
assert_eq!(chat.bottom_pane.context_window_percent(), None);
}
#[tokio::test]
async fn core_cyber_policy_error_renders_dedicated_notice() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
chat.handle_codex_event(Event {
id: "cyber-policy".into(),
msg: EventMsg::Error(ErrorEvent {
message: "server fallback message".to_string(),
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
}),
});
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(rendered.contains("This chat was flagged for possible cybersecurity risk"));
assert!(rendered.contains("Trusted Access for Cyber"));
assert!(!rendered.contains("server fallback message"));
}
#[tokio::test]
async fn core_model_verification_renders_warning() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
chat.handle_codex_event(Event {
id: "model-verification".into(),
msg: EventMsg::ModelVerification(ModelVerificationEvent {
verifications: vec![CoreModelVerification::TrustedAccessForCyber],
}),
});
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(rendered.contains("flagged for potentially high-risk cyber activity"));
assert!(rendered.contains("slower while additional verification"));
assert!(rendered.contains("https://chatgpt.com/cyber"));
}
#[tokio::test]
async fn context_indicator_shows_used_tokens_when_window_unknown() {
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("unknown-model")).await;
+58
View File
@@ -1832,6 +1832,50 @@ pub(crate) fn new_warning_event(message: String) -> PrefixedWrappedHistoryCell {
PrefixedWrappedHistoryCell::new(message.yellow(), "".yellow(), " ")
}
const TRUSTED_ACCESS_FOR_CYBER_URL: &str = "https://chatgpt.com/cyber";
#[derive(Debug)]
pub(crate) struct CyberPolicyNoticeCell;
pub(crate) fn new_cyber_policy_error_event() -> CyberPolicyNoticeCell {
CyberPolicyNoticeCell
}
impl HistoryCell for CyberPolicyNoticeCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(
vec![
"".cyan(),
"This chat was flagged for possible cybersecurity risk".bold(),
]
.into(),
);
let wrap_width = width.saturating_sub(2).max(1) as usize;
let body = Line::from(vec![
" If this seems wrong, try rephrasing your request. To get authorized for security work, join the "
.dim(),
"Trusted Access for Cyber".cyan().underlined(),
" program.".dim(),
]);
let wrapped = adaptive_wrap_line(
&body,
RtOptions::new(wrap_width).subsequent_indent(" ".into()),
);
push_owned_lines(&wrapped, &mut lines);
lines.push(
vec![
" ".into(),
TRUSTED_ACCESS_FOR_CYBER_URL.cyan().underlined(),
]
.into(),
);
lines
}
}
#[derive(Debug)]
pub(crate) struct DeprecationNoticeCell {
summary: String,
@@ -3255,6 +3299,20 @@ mod tests {
insta::assert_snapshot!(rendered);
}
#[test]
fn cyber_policy_error_event_snapshot() {
let cell = new_cyber_policy_error_event();
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn cyber_policy_error_event_narrow_snapshot() {
let cell = new_cyber_policy_error_event();
let rendered = render_lines(&cell.display_lines(/*width*/ 36)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn ps_output_long_command_snapshot() {
let cell = new_unified_exec_processes_output(vec![UnifiedExecProcessDetails {
@@ -0,0 +1,12 @@
---
source: tui/src/history_cell.rs
assertion_line: 3312
expression: rendered
---
ⓘ This chat was flagged for possible cybersecurity risk
If this seems wrong, try
rephrasing your request. To get
authorized for security work,
join the Trusted Access for
Cyber program.
https://chatgpt.com/cyber
@@ -0,0 +1,9 @@
---
source: tui/src/history_cell.rs
assertion_line: 3305
expression: rendered
---
ⓘ This chat was flagged for possible cybersecurity risk
If this seems wrong, try rephrasing your request. To get authorized for
security work, join the Trusted Access for Cyber program.
https://chatgpt.com/cyber