mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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(¬ification.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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+12
@@ -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
|
||||
+9
@@ -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
|
||||
Reference in New Issue
Block a user