mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Tailor TUI cyber refusal notices to Daybreak eligibility (#42667)
## What changed - Prefetch ChatGPT account eligibility in the background and cache it across TUI widget replacements. - Show an application link when Daybreak is available but not enabled, an Astra-specific explanation for unsupported models, and neutral copy when access is already present or eligibility cannot be determined. - Refresh eligibility after account changes and reconnects without changing the selected model or delaying refusal handling. ## Testing - Cover eligibility responses, account changes, model-specific notices, fallback behavior, and rendered notice snapshots. GitOrigin-RevId: 7ae8613a946cb46b8f0b46bf47e5e15ae258ed86
This commit is contained in:
@@ -225,6 +225,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
ServerNotification::AccountUpdated(notification) => {
|
||||
self.chat_widget.cyber_policy_notice = Default::default();
|
||||
self.rate_limit_hard_stop_generation =
|
||||
self.rate_limit_hard_stop_generation.wrapping_add(1);
|
||||
self.rate_limit_refresh_state.invalidate_recovery();
|
||||
@@ -252,6 +253,13 @@ impl App {
|
||||
.is_some_and(AuthMode::has_chatgpt_account),
|
||||
has_codex_backend_auth,
|
||||
);
|
||||
if self.chat_widget.has_chatgpt_account() {
|
||||
crate::daybreak::prefetch_notice(
|
||||
&self.config,
|
||||
app_server_client,
|
||||
self.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ServerNotification::ExternalAgentConfigImportCompleted(notification) => {
|
||||
|
||||
@@ -244,6 +244,7 @@ impl App {
|
||||
self.rate_limit_refresh_state.invalidate_recovery();
|
||||
session.inherit_task_tool_capabilities(app_server);
|
||||
*app_server = session;
|
||||
self.chat_widget.cyber_policy_notice = Default::default();
|
||||
self.chat_widget.requires_openai_auth = bootstrap.requires_openai_auth;
|
||||
self.chat_widget.remote_connection =
|
||||
crate::status::remote_connection::remote_connection_status_value(
|
||||
@@ -389,6 +390,13 @@ impl App {
|
||||
bootstrap.has_chatgpt_account,
|
||||
matches!(bootstrap.auth_mode, Some(TelemetryAuthMode::Chatgpt)),
|
||||
);
|
||||
if self.chat_widget.has_chatgpt_account() {
|
||||
crate::daybreak::prefetch_notice(
|
||||
&self.config,
|
||||
app_server,
|
||||
self.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
}
|
||||
self.feedback_audience = bootstrap.feedback_audience;
|
||||
self.chat_widget.add_info_message(
|
||||
"Reconnected. No input was resent. Review uncertain submissions before retrying; recovered queues remain paused.".into(), /*hint*/ None,
|
||||
|
||||
@@ -475,6 +475,7 @@ impl App {
|
||||
/// This helper copies every known nickname/role from `AgentNavigationState` into the
|
||||
/// replacement widget so that replayed collab items render agent names immediately.
|
||||
pub(super) fn replace_chat_widget(&mut self, mut chat_widget: ChatWidget) {
|
||||
chat_widget.cyber_policy_notice = self.chat_widget.cyber_policy_notice.clone();
|
||||
self.commit_animation = None;
|
||||
// Transfer the last-written terminal title to the replacement widget
|
||||
// so it knows what OSC title is currently displayed. Without this, the
|
||||
|
||||
@@ -707,6 +707,11 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
// already has data and available reset credits can be surfaced, without
|
||||
// delaying the initial frame render.
|
||||
if requires_openai_auth && has_chatgpt_account {
|
||||
crate::daybreak::prefetch_notice(
|
||||
&app.config,
|
||||
&app_server,
|
||||
app.chat_widget.cyber_policy_notice.clone(),
|
||||
);
|
||||
let reset_hint_request_id = app.chat_widget.start_rate_limit_reset_startup_check();
|
||||
app.refresh_rate_limits(
|
||||
&app_server,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! App-level orchestration tests for the TUI.
|
||||
|
||||
#[path = "tests/daybreak_tests.rs"]
|
||||
mod daybreak_tests;
|
||||
|
||||
#[path = "tests/advanced_reasoning_tests.rs"]
|
||||
mod advanced_reasoning_tests;
|
||||
#[path = "tests/agents_navigation_tests.rs"]
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
use super::*;
|
||||
use crate::daybreak::Notice;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::ErrorNotification;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn cyber_refusal_reads_eligibility_without_changing_the_model() -> Result<()> {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
let backend = wiremock::MockServer::start().await;
|
||||
app.config.chatgpt_base_url = backend.uri();
|
||||
app.config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
|
||||
write_chatgpt_auth(
|
||||
&app.config.codex_home,
|
||||
ChatGptAuthFixture::new("test-token")
|
||||
.account_id("account")
|
||||
.chatgpt_user_id("user"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("write synthetic auth");
|
||||
let server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
|
||||
let thread_id = ThreadId::new();
|
||||
app.enqueue_primary_thread_session(
|
||||
test_thread_session(thread_id, app.config.cwd.to_path_buf()),
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
for (status, enrolled, model, replace_account, expected) in [
|
||||
(200, true, "gpt-5.6-sol", false, Notice::Limited),
|
||||
(200, false, "gpt-5.6-sol", false, Notice::Apply),
|
||||
(503, false, "gpt-5.6-sol", false, Notice::Limited),
|
||||
(200, true, "gpt-6-astra", false, Notice::Astra),
|
||||
(200, false, "gpt-6-astra", false, Notice::Astra),
|
||||
(503, false, "gpt-6-astra", false, Notice::Astra),
|
||||
(200, true, "gpt-6-astra-wm", false, Notice::Astra),
|
||||
(200, true, "unmapped-model", false, Notice::Limited),
|
||||
(200, false, "gpt-5.6-sol", true, Notice::Limited),
|
||||
] {
|
||||
backend.reset().await;
|
||||
let codex_home = app.config.codex_home.clone();
|
||||
let response = wiremock::ResponseTemplate::new(status)
|
||||
.set_delay(if model == "gpt-5.6-sol" && enrolled { Duration::from_secs(1) } else { Duration::ZERO })
|
||||
.set_body_json(json!({"programs": [{"program":"cyber", "state": if enrolled { "active" } else { "inactive" }, "grants": if enrolled { json!([{"level":"tac2"}]) } else { json!([]) }}]}));
|
||||
wiremock::Mock::given(wiremock::matchers::path("/accounts/verified_access"))
|
||||
.and(wiremock::matchers::header(
|
||||
"authorization",
|
||||
"Bearer test-token",
|
||||
))
|
||||
.respond_with(move |_: &wiremock::Request| {
|
||||
if replace_account {
|
||||
write_chatgpt_auth(
|
||||
&codex_home,
|
||||
ChatGptAuthFixture::new("test-token").account_id("other-account"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
response.clone()
|
||||
})
|
||||
.expect(1)
|
||||
.mount(&backend)
|
||||
.await;
|
||||
// Account discovery starts before a subsequent model switch to Sol.
|
||||
app.chat_widget.set_model("gpt-6-astra");
|
||||
app.handle_app_server_event(
|
||||
&server,
|
||||
AppServerEvent::ServerNotification(Box::new(ServerNotification::AccountUpdated(
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
plan_type: None,
|
||||
},
|
||||
))),
|
||||
)
|
||||
.await;
|
||||
if model != "gpt-5.6-sol" || !enrolled {
|
||||
wait_for_notice(&app.chat_widget.cyber_policy_notice).await;
|
||||
}
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.chat_widget.set_model(model);
|
||||
while events.try_recv().is_ok() {}
|
||||
// In the slow Sol case, discovery is still pending: do not block input on it.
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(250),
|
||||
app.handle_app_server_event(
|
||||
&server,
|
||||
AppServerEvent::ServerNotification(Box::new(ServerNotification::Error(
|
||||
ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "server fallback".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn".into(),
|
||||
},
|
||||
))),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("refusal must not wait for eligibility");
|
||||
let event = app.active_thread_rx.as_mut().unwrap().recv().await.unwrap();
|
||||
app.handle_thread_event_now(event);
|
||||
let cell = std::iter::from_fn(|| events.try_recv().ok())
|
||||
.find_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => Some(cell),
|
||||
_ => None,
|
||||
})
|
||||
.expect("cyber refusal cell");
|
||||
assert_eq!(
|
||||
cell.display_lines(/*width*/ 80),
|
||||
history_cell::new_cyber_policy_error_event(expected).display_lines(/*width*/ 80),
|
||||
"{model}"
|
||||
);
|
||||
assert_eq!(
|
||||
wait_for_notice(&app.chat_widget.cyber_policy_notice)
|
||||
.await
|
||||
.for_model(model),
|
||||
expected
|
||||
);
|
||||
backend.verify().await;
|
||||
assert_eq!(app.chat_widget.current_model(), model);
|
||||
}
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_notice(cache: &crate::daybreak::NoticeCache) -> Notice {
|
||||
tokio::time::timeout(Duration::from_secs(3), async {
|
||||
loop {
|
||||
if let Some(notice) = cache.get() {
|
||||
return *notice;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("eligibility should finish")
|
||||
}
|
||||
@@ -558,6 +558,7 @@ pub(crate) enum ExternalEditorState {
|
||||
/// (which view gets Ctrl+C), while `ChatWidget` owns process-level decisions such as interrupting
|
||||
/// active work, arming the double-press quit shortcut, and requesting shutdown-first exit.
|
||||
pub(crate) struct ChatWidget {
|
||||
pub(crate) cyber_policy_notice: crate::daybreak::NoticeCache,
|
||||
app_event_tx: AppEventSender,
|
||||
codex_op_target: CodexOpTarget,
|
||||
bottom_pane: BottomPane,
|
||||
|
||||
@@ -98,6 +98,7 @@ impl ChatWidget {
|
||||
pet_http_client.clone(),
|
||||
);
|
||||
let mut widget = Self {
|
||||
cyber_policy_notice: Default::default(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
frame_requester: frame_requester.clone(),
|
||||
codex_op_target,
|
||||
|
||||
@@ -1642,8 +1642,8 @@ async fn live_app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(rendered.contains("This content can't be shown"));
|
||||
assert!(rendered.contains("extra caution with cybersecurity requests"));
|
||||
assert!(rendered.contains("This content can’t be shown"));
|
||||
assert!(rendered.contains("We take extra care with some cybersecurity requests"));
|
||||
assert!(!rendered.contains("server fallback message"));
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
}
|
||||
|
||||
@@ -91,7 +91,10 @@ async fn resumed_session_hides_unknown_token_usage_until_an_update_arrives() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(Some("gpt-5.6-sol")).await;
|
||||
chat.cyber_policy_notice
|
||||
.set(crate::daybreak::Notice::Apply)
|
||||
.unwrap();
|
||||
|
||||
handle_error(
|
||||
&mut chat,
|
||||
@@ -102,29 +105,12 @@ async fn app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(rendered.contains("This content can't be shown"));
|
||||
assert!(rendered.contains("extra caution with cybersecurity requests"));
|
||||
assert!(rendered.contains("openai.com/form/enterprise-trusted-access-for-cyber"));
|
||||
assert!(rendered.contains("This content can’t be shown"));
|
||||
assert!(rendered.contains("We take extra care with some cybersecurity requests"));
|
||||
assert!(rendered.contains("Apply for Daybreak"));
|
||||
assert!(!rendered.contains("server fallback message"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_cyber_policy_error_uses_individual_link_for_personal_plan() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.plan_type = Some(PlanType::Free);
|
||||
chat.has_chatgpt_account = true;
|
||||
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"server fallback message",
|
||||
Some(CodexErrorInfo::CyberPolicy),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
assert!(lines_to_single_string(&cells[0]).contains("https://chatgpt.com/cyber/"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_model_verification_renders_warning() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -395,12 +395,16 @@ impl ChatWidget {
|
||||
pub(super) fn on_cyber_policy_error(&mut self) {
|
||||
self.input_queue.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
let plan_type = if self.has_chatgpt_account {
|
||||
self.plan_type
|
||||
let notice = if self.config.model_provider_id == "openai" {
|
||||
self.cyber_policy_notice
|
||||
.get()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.for_model(self.current_model())
|
||||
} else {
|
||||
None
|
||||
crate::daybreak::Notice::Limited
|
||||
};
|
||||
self.add_to_history(history_cell::new_cyber_policy_error_event(plan_type));
|
||||
self.add_to_history(history_cell::new_cyber_policy_error_event(notice));
|
||||
self.request_redraw();
|
||||
|
||||
// After an error ends the turn, try sending the next queued input.
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
//! Read-only Daybreak eligibility used to choose cyber refusal copy.
|
||||
//! Account-scoped discovery runs in the background; pending/failed reads use neutral copy.
|
||||
//! Astra takes precedence; the TUI does not configure the app’s Daybreak access program.
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use codex_login::CodexAuth;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub(crate) type NoticeCache = Arc<OnceCell<Notice>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum Notice {
|
||||
Apply,
|
||||
Astra,
|
||||
#[default]
|
||||
Limited,
|
||||
}
|
||||
|
||||
pub(crate) fn prefetch_notice(config: &Config, server: &AppServerSession, cache: NoticeCache) {
|
||||
if config.model_provider_id != "openai"
|
||||
|| server.uses_remote_workspace()
|
||||
|| cache.get().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let config = config.clone();
|
||||
let request_handle = server.request_handle();
|
||||
tokio::spawn(async move {
|
||||
cache
|
||||
.get_or_init(|| read_notice(&config, &request_handle))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn read_notice(config: &Config, request_handle: &AppServerRequestHandle) -> Notice {
|
||||
tokio::time::timeout(Duration::from_secs(3), async {
|
||||
// Allow the server to refresh its token before reading local credentials.
|
||||
let status: GetAuthStatusResponse = request_handle
|
||||
.request_typed(ClientRequest::GetAuthStatus {
|
||||
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
|
||||
params: GetAuthStatusParams {
|
||||
include_token: Some(true),
|
||||
refresh_token: Some(false),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.ok()?;
|
||||
let auth = config
|
||||
.auth_config()
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let CodexAuth::Chatgpt(_) = &auth else {
|
||||
return None;
|
||||
};
|
||||
if status.auth_method != Some(AuthMode::Chatgpt)
|
||||
|| status.auth_token.as_deref() != Some(auth.get_token().ok()?.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let client = RouteAwareClientPool::new_without_redirects(
|
||||
config.http_client_factory(),
|
||||
ClientRouteClass::Api,
|
||||
);
|
||||
let url = format!(
|
||||
"{}/accounts/verified_access",
|
||||
config.chatgpt_base_url.trim_end_matches('/')
|
||||
);
|
||||
let response = client
|
||||
.get(url)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||||
.send()
|
||||
.await
|
||||
.ok()?;
|
||||
if !response.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
let access = response.json::<VerifiedAccess>().await.ok()?;
|
||||
let current_auth = config
|
||||
.auth_config()
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
if current_auth.get_token().ok()? != auth.get_token().ok()?
|
||||
|| current_auth.get_account_id() != auth.get_account_id()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(access.notice())
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VerifiedAccess {
|
||||
programs: Vec<Program>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "program", rename_all = "snake_case")]
|
||||
enum Program {
|
||||
Cyber {
|
||||
state: String,
|
||||
grants: Vec<serde::de::IgnoredAny>,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
impl VerifiedAccess {
|
||||
fn notice(&self) -> Notice {
|
||||
let absent = self.programs.iter().all(|program| match program {
|
||||
Program::Cyber { state, grants } => state == "inactive" && grants.is_empty(),
|
||||
Program::Other => true,
|
||||
});
|
||||
if absent {
|
||||
Notice::Apply
|
||||
} else {
|
||||
Notice::Limited
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Notice {
|
||||
pub(crate) fn for_model(self, model: &str) -> Self {
|
||||
match model {
|
||||
// Same identifiers as the app's isDaybreakUnavailableModel.
|
||||
"gpt-6-astra" | "gpt-6-astra-wm" => Self::Astra,
|
||||
"gpt-5.6-sol" => self,
|
||||
// Other model/access-program mappings are not established for the TUI.
|
||||
_ => Self::Limited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "daybreak_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,37 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn refusal_copy_uses_verified_access() {
|
||||
for (programs, expected) in [
|
||||
(json!([]), Notice::Apply),
|
||||
(
|
||||
json!([{"program":"cyber","state":"inactive","grants":[]}]),
|
||||
Notice::Apply,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"unavailable","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"active","grants":[{"level":"tac2"}]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"active","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"inactive","grants":[{"level":"tac2"}]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
(
|
||||
json!([{"program":"cyber","state":"unknown","grants":[]}]),
|
||||
Notice::Limited,
|
||||
),
|
||||
] {
|
||||
let access: VerifiedAccess = serde_json::from_value(json!({"programs": programs})).unwrap();
|
||||
assert_eq!(access.notice(), expected);
|
||||
}
|
||||
}
|
||||
@@ -90,33 +90,55 @@ pub(crate) fn new_warning_event(message: String) -> PrefixedWrappedHistoryCell {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SafetyAccessBlockCell {
|
||||
title: &'static str,
|
||||
body: &'static str,
|
||||
trusted_access_url: &'static str,
|
||||
actions: &'static [(&'static str, &'static str)],
|
||||
}
|
||||
|
||||
const SAFETY_ACCESS_BLOCK_TITLE: &str = "This content can't be shown";
|
||||
const SAFETY_ACCESS_BLOCK_LEARN_MORE_URL: &str = "https://help.openai.com/en/articles/20001326";
|
||||
const CYBER_INDIVIDUAL_TRUSTED_ACCESS_URL: &str = "https://chatgpt.com/cyber/";
|
||||
const CYBER_ENTERPRISE_TRUSTED_ACCESS_URL: &str =
|
||||
"https://openai.com/form/enterprise-trusted-access-for-cyber/";
|
||||
|
||||
pub(crate) fn new_safety_access_block_event() -> SafetyAccessBlockCell {
|
||||
SafetyAccessBlockCell {
|
||||
title: "This content can't be shown",
|
||||
body: "We take extra caution with requests involving biological research and applications that could pose safety risks. Eligible researchers can apply for Trusted Access.",
|
||||
trusted_access_url: "https://chatgpt.com/r/b749fb02595e04c3007a54375f3f4374",
|
||||
actions: &[
|
||||
(
|
||||
"Trusted Access",
|
||||
"https://chatgpt.com/r/b749fb02595e04c3007a54375f3f4374",
|
||||
),
|
||||
("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_cyber_policy_error_event(plan_type: Option<PlanType>) -> SafetyAccessBlockCell {
|
||||
let trusted_access_url = match plan_type {
|
||||
Some(
|
||||
PlanType::Free | PlanType::Go | PlanType::Plus | PlanType::Pro | PlanType::ProLite,
|
||||
) => CYBER_INDIVIDUAL_TRUSTED_ACCESS_URL,
|
||||
_ => CYBER_ENTERPRISE_TRUSTED_ACCESS_URL,
|
||||
pub(crate) fn new_cyber_policy_error_event(
|
||||
notice: crate::daybreak::Notice,
|
||||
) -> SafetyAccessBlockCell {
|
||||
use crate::daybreak::Notice;
|
||||
let (body, actions): (_, &'static [(&str, &str)]) = match notice {
|
||||
Notice::Apply => (
|
||||
"We take extra care with some cybersecurity requests. If you’re doing authorized security work, apply for Daybreak to get broader access.",
|
||||
&[
|
||||
("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL),
|
||||
(
|
||||
"Apply for Daybreak",
|
||||
"https://openai.com/form/enterprise-trusted-access-for-cyber/",
|
||||
),
|
||||
],
|
||||
),
|
||||
Notice::Astra => (
|
||||
"Daybreak isn’t available for Astra. Some cybersecurity requests may still be limited.",
|
||||
&[("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL)],
|
||||
),
|
||||
Notice::Limited => (
|
||||
"We take extra care with some cybersecurity requests.",
|
||||
&[("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL)],
|
||||
),
|
||||
};
|
||||
SafetyAccessBlockCell {
|
||||
body: "We take extra caution with cybersecurity requests. If you’re a security professional, you may be able to apply for Trusted Access.",
|
||||
trusted_access_url,
|
||||
title: "This content can’t be shown",
|
||||
body,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +149,7 @@ impl HistoryCell for SafetyAccessBlockCell {
|
||||
|
||||
fn display_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine> {
|
||||
let mut lines = vec![HyperlinkLine::new(
|
||||
vec!["ⓘ ".cyan(), SAFETY_ACCESS_BLOCK_TITLE.bold()].into(),
|
||||
vec!["ⓘ ".cyan(), self.title.bold()].into(),
|
||||
)];
|
||||
let body = Line::from(vec![" ".into(), self.body.dim()]);
|
||||
let wrap_width = width.saturating_sub(2).max(1) as usize;
|
||||
@@ -139,10 +161,7 @@ impl HistoryCell for SafetyAccessBlockCell {
|
||||
push_owned_lines(&wrapped, &mut wrapped_body);
|
||||
lines.extend(plain_hyperlink_lines(wrapped_body));
|
||||
|
||||
for (label, url) in [
|
||||
("Trusted Access", self.trusted_access_url),
|
||||
("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL),
|
||||
] {
|
||||
for &(label, url) in self.actions {
|
||||
let source = crate::terminal_hyperlinks::annotate_web_urls_in_line(
|
||||
vec![format!(" {label}: ").dim(), url.cyan().underlined()].into(),
|
||||
);
|
||||
@@ -161,13 +180,13 @@ impl HistoryCell for SafetyAccessBlockCell {
|
||||
}
|
||||
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
let trusted_access_url = self.trusted_access_url;
|
||||
vec![
|
||||
Line::from(SAFETY_ACCESS_BLOCK_TITLE),
|
||||
Line::from(self.body),
|
||||
Line::from(format!("Trusted Access: {trusted_access_url}")),
|
||||
Line::from(format!("Learn more: {SAFETY_ACCESS_BLOCK_LEARN_MORE_URL}")),
|
||||
]
|
||||
let mut lines = vec![Line::from(self.title), Line::from(self.body)];
|
||||
lines.extend(
|
||||
self.actions
|
||||
.iter()
|
||||
.map(|(label, target)| Line::from(format!("{label}: {target}"))),
|
||||
);
|
||||
lines
|
||||
}
|
||||
|
||||
fn transcript_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine> {
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
ⓘ This content can’t be shown
|
||||
Daybreak isn’t available for Astra. Some cybersecurity requests may still be
|
||||
limited.
|
||||
Learn more: https://help.openai.com/en/articles/20001326
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
ⓘ This content can't be shown
|
||||
We take extra caution with cybersecurity requests. If you’re a security
|
||||
professional, you may be able to apply for Trusted Access.
|
||||
Trusted Access: https://chatgpt.com/cyber/
|
||||
Learn more: https://help.openai.com/en/articles/20001326
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
ⓘ This content can’t be shown
|
||||
We take extra care with some cybersecurity requests.
|
||||
Learn more: https://help.openai.com/en/articles/20001326
|
||||
+9
-8
@@ -1,16 +1,17 @@
|
||||
---
|
||||
source: tui/src/history_cell/tests.rs
|
||||
assertion_line: 821
|
||||
expression: rendered
|
||||
---
|
||||
ⓘ This content can't be shown
|
||||
We take extra caution with
|
||||
ⓘ This content can’t be shown
|
||||
We take extra care with some
|
||||
cybersecurity requests. If
|
||||
you’re a security professional,
|
||||
you may be able to apply for
|
||||
Trusted Access.
|
||||
Trusted Access: https://
|
||||
openai.com/form/enterprise-
|
||||
trusted-access-for-cyber/
|
||||
you’re doing authorized security
|
||||
work, apply for Daybreak to get
|
||||
broader access.
|
||||
Learn more: https://
|
||||
help.openai.com/en/
|
||||
articles/20001326
|
||||
Apply for Daybreak: https://
|
||||
openai.com/form/enterprise-
|
||||
trusted-access-for-cyber/
|
||||
|
||||
+7
-6
@@ -1,10 +1,11 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 3305
|
||||
source: tui/src/history_cell/tests.rs
|
||||
assertion_line: 774
|
||||
expression: rendered
|
||||
---
|
||||
ⓘ This content can't be shown
|
||||
We take extra caution with cybersecurity requests. If you’re a security
|
||||
professional, you may be able to apply for Trusted Access.
|
||||
Trusted Access: https://openai.com/form/enterprise-trusted-access-for-cyber/
|
||||
ⓘ This content can’t be shown
|
||||
We take extra care with some cybersecurity requests. If you’re doing
|
||||
authorized security work, apply for Daybreak to get broader access.
|
||||
Learn more: https://help.openai.com/en/articles/20001326
|
||||
Apply for Daybreak: https://openai.com/form/enterprise-trusted-access-for-
|
||||
cyber/
|
||||
|
||||
@@ -769,14 +769,21 @@ fn ps_output_multiline_snapshot() {
|
||||
|
||||
#[test]
|
||||
fn cyber_policy_error_event_snapshot() {
|
||||
let cell = new_cyber_policy_error_event(/*plan_type*/ None);
|
||||
let cell = new_cyber_policy_error_event(crate::daybreak::Notice::Apply);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyber_policy_error_event_individual_snapshot() {
|
||||
let cell = new_cyber_policy_error_event(Some(PlanType::Pro));
|
||||
fn cyber_policy_error_event_astra_snapshot() {
|
||||
let cell = new_cyber_policy_error_event(crate::daybreak::Notice::Astra);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyber_policy_error_event_limited_snapshot() {
|
||||
let cell = new_cyber_policy_error_event(crate::daybreak::Notice::Limited);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
@@ -790,7 +797,7 @@ fn safety_access_block_event_snapshot() {
|
||||
|
||||
#[test]
|
||||
fn cyber_policy_error_event_narrow_snapshot() {
|
||||
let cell = new_cyber_policy_error_event(/*plan_type*/ None);
|
||||
let cell = new_cyber_policy_error_event(crate::daybreak::Notice::Apply);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 36)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ mod collaboration_modes;
|
||||
mod color;
|
||||
mod config_update;
|
||||
pub(crate) mod custom_terminal;
|
||||
mod daybreak;
|
||||
mod experimental_features;
|
||||
mod permission_discovery;
|
||||
mod pets;
|
||||
|
||||
Reference in New Issue
Block a user