From 66561d03012edb96e4ea87513cb10dea4c909986 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 2 Oct 2026 21:14:15 +0000 Subject: [PATCH] Allow API-key accounts to use Daybreak in the TUI (#50433) ## What changed Allow accounts using an API key with the OpenAI provider to access `/daybreak` and Daybreak policy notices. Model availability and the connected server still determine program access. Use the same eligibility checks for turn submission, continuation, and retries. For API-key accounts, send the selected `cyberAccessProgram` when Daybreak is enabled and omit it when Daybreak is off. ## Testing Extend session lifecycle coverage to check API-key turn parameters with Daybreak enabled and disabled. Add an API-key `/daybreak` help snapshot and update the signed-out error snapshot. GitOrigin-RevId: ab6d4313c430f316ef9835d4facb878aab0b9725 --- codex-rs/tui/src/app/daybreak.rs | 3 +-- codex-rs/tui/src/app/misalignment_policy.rs | 3 +-- codex-rs/tui/src/app/safety_buffering.rs | 3 +-- .../app/tests/session_lifecycle_requests.rs | 18 +++++++++++++++++- codex-rs/tui/src/app/thread_routing.rs | 3 +-- codex-rs/tui/src/chatwidget/settings.rs | 17 ++++++++++++++++- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 4 ++-- ...et__tests__slash_daybreak_api_key_help.snap | 5 +++++ ...dget__tests__slash_daybreak_signed_out.snap | 2 +- .../tui/src/chatwidget/tests/slash_commands.rs | 14 ++++++++++++++ codex-rs/tui/src/chatwidget/turn_runtime.rs | 5 ++--- codex-rs/tui/src/daybreak.rs | 2 +- 12 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_api_key_help.snap diff --git a/codex-rs/tui/src/app/daybreak.rs b/codex-rs/tui/src/app/daybreak.rs index faba647db2..5287a738cb 100644 --- a/codex-rs/tui/src/app/daybreak.rs +++ b/codex-rs/tui/src/app/daybreak.rs @@ -13,8 +13,7 @@ impl App { return; } if enabled - && (!self.chat_widget.has_chatgpt_account() - || self.chat_widget.config_ref().model_provider_id != "openai" + && (!self.chat_widget.daybreak_turn_eligible(enabled) || !crate::daybreak::available(&self.chat_widget.model_catalog().models)) { self.chat_widget.add_error_message( diff --git a/codex-rs/tui/src/app/misalignment_policy.rs b/codex-rs/tui/src/app/misalignment_policy.rs index c4cb4aff42..6db5022463 100644 --- a/codex-rs/tui/src/app/misalignment_policy.rs +++ b/codex-rs/tui/src/app/misalignment_policy.rs @@ -114,11 +114,10 @@ impl App { let Some(message) = review.continuation_message() else { return; }; - let eligible_account = self.chat_widget.has_chatgpt_account() - && self.chat_widget.config_ref().model_provider_id == "openai"; let enabled = self.chat_widget.daybreak_enabled && !self.chat_widget.side_conversation_active() && !self.side_threads.contains_key(&review.thread_id); + let eligible_account = self.chat_widget.daybreak_turn_eligible(enabled); let cyber_access_program = match crate::daybreak::program_for_turn( &self.chat_widget.model_catalog().models, self.chat_widget.current_model(), diff --git a/codex-rs/tui/src/app/safety_buffering.rs b/codex-rs/tui/src/app/safety_buffering.rs index a29a065b49..e03ea7689f 100644 --- a/codex-rs/tui/src/app/safety_buffering.rs +++ b/codex-rs/tui/src/app/safety_buffering.rs @@ -85,11 +85,10 @@ impl App { .add_error_message(format!("Failed to retry with a faster model: {err}")); return; } - let eligible_account = self.chat_widget.has_chatgpt_account() - && self.chat_widget.config_ref().model_provider_id == "openai"; let daybreak_enabled = self.chat_widget.daybreak_enabled && !self.chat_widget.side_conversation_active() && !self.side_threads.contains_key(&thread_id); + let eligible_account = self.chat_widget.daybreak_turn_eligible(daybreak_enabled); if let Err(message) = crate::daybreak::program_for_turn( &self.chat_widget.model_catalog().models, &model, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index b57bbdcd6b..8214c6882e 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -168,14 +168,30 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> { background_thread_id, crate::app::side::SideThreadState::new(thread_id), ); - app.submit_thread_op(&mut server, background_thread_id, turn) + app.submit_thread_op(&mut server, background_thread_id, turn.clone()) .await?; let turns = recorded_params(&requests, "turn/start"); assert_eq!(turns.len(), 3); assert_eq!(turns[0]["cyberAccessProgram"], "standard"); assert_eq!(turns[1]["cyberAccessProgram"], "daybreakBlue"); assert_eq!(turns[2]["cyberAccessProgram"], "standard"); + app.chat_widget.update_account_state( + Some(crate::status::StatusAccountDisplay::ApiKey), + /*plan_type*/ None, + /*has_chatgpt_account*/ false, + /*has_codex_backend_auth*/ false, + ); + assert!( + !app.chat_widget + .set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ false,) + ); + app.submit_thread_op(&mut server, thread_id, turn.clone()) + .await?; app.chat_widget.set_daybreak_enabled(/*enabled*/ false); + app.submit_thread_op(&mut server, thread_id, turn).await?; + let turns = recorded_params(&requests, "turn/start"); + assert_eq!(turns[3]["cyberAccessProgram"], "daybreakBlue"); + assert!(turns[4]["cyberAccessProgram"].is_null()); while events.try_recv().is_ok() {} let missing_thread_id = ThreadId::new(); diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 1f43097122..9a6adc013d 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -825,11 +825,10 @@ impl App { } } if should_start_turn { - let eligible_account = self.chat_widget.has_chatgpt_account() - && self.chat_widget.config_ref().model_provider_id == "openai"; let enabled = self.chat_widget.daybreak_enabled && !self.chat_widget.side_conversation_active() && !self.side_threads.contains_key(&thread_id); + let eligible_account = self.chat_widget.daybreak_turn_eligible(enabled); let cyber_access_program = match crate::daybreak::program_for_turn( &self.chat_widget.model_catalog().models, model, diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 5280aaac46..c612d098b0 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -11,10 +11,25 @@ impl ChatWidget { .set_daybreak_command_description(self.daybreak_command_description()); } + /// UI eligibility only; the catalog and server still determine model/program access. + pub(crate) fn daybreak_account_eligible(&self) -> bool { + self.config.model_provider_id == "openai" + && (self.has_chatgpt_account + || matches!( + self.status_account_display, + Some(StatusAccountDisplay::ApiKey) + )) + } + + /// API-key turns need no explicit program while Daybreak is off. + pub(crate) fn daybreak_turn_eligible(&self, enabled: bool) -> bool { + self.daybreak_account_eligible() && (self.has_chatgpt_account || enabled) + } + pub(super) fn daybreak_command_description(&self) -> Option<&'static str> { if self.daybreak_enabled { Some("Disable broader access for cybersecurity work") - } else if !self.has_chatgpt_account || self.config.model_provider_id != "openai" { + } else if !self.daybreak_account_eligible() { None } else { match crate::daybreak::availability(&self.model_catalog.models) { diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 264165df32..fc23f85fb9 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -190,8 +190,8 @@ impl ChatWidget { match cmd { SlashCommand::Daybreak => { if !self.daybreak_enabled { - if !self.has_chatgpt_account || self.config.model_provider_id != "openai" { - self.add_error_message("Daybreak requires a signed-in ChatGPT account and the OpenAI provider.".into()); + if !self.daybreak_account_eligible() { + self.add_error_message("Daybreak requires the OpenAI provider with either ChatGPT sign-in or an API key with Daybreak support enabled.".into()); return; } if !crate::daybreak::available(&self.model_catalog.models) { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_api_key_help.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_api_key_help.snap new file mode 100644 index 0000000000..9582b0c22b --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_api_key_help.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/chatwidget/tests/slash_commands.rs +expression: "render_bottom_popup(&chat, 80).lines().next().unwrap()" +--- +› /daybreak Learn about broader access for cybersecurity work diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_signed_out.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_signed_out.snap index b8d561b412..d95cb4960b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_signed_out.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_daybreak_signed_out.snap @@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests/slash_commands.rs assertion_line: 2194 expression: "lines_to_single_string(&cells[0])" --- -■ Daybreak requires a signed-in ChatGPT account and the OpenAI provider. +■ Daybreak requires the OpenAI provider with either ChatGPT sign-in or an API key with Daybreak support enabled. diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 85ab13b0b2..e5e20e2db9 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -2204,6 +2204,20 @@ async fn slash_daybreak_offers_an_application_when_unavailable() { .cyber .push(codex_protocol::turn_input::CyberAccessProgram::DaybreakBlue); chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![model])); + chat.has_chatgpt_account = false; + chat.status_account_display = Some(StatusAccountDisplay::ApiKey); + chat.set_daybreak_enabled(/*enabled*/ false); + chat.bottom_pane + .set_composer_text("/daybreak".to_string(), Vec::new(), Vec::new()); + assert_chatwidget_snapshot!( + "slash_daybreak_api_key_help", + render_bottom_popup(&chat, /*width*/ 80) + .lines() + .next() + .unwrap() + ); + chat.has_chatgpt_account = true; + chat.status_account_display = None; for (enabled, name) in [ (false, "slash_daybreak_help_enable"), (true, "slash_daybreak_help_disable"), diff --git a/codex-rs/tui/src/chatwidget/turn_runtime.rs b/codex-rs/tui/src/chatwidget/turn_runtime.rs index 84aeef22ea..575410965b 100644 --- a/codex-rs/tui/src/chatwidget/turn_runtime.rs +++ b/codex-rs/tui/src/chatwidget/turn_runtime.rs @@ -392,9 +392,8 @@ impl ChatWidget { } pub(super) fn on_cyber_policy_error(&mut self) { - let can_enable_daybreak = self.has_chatgpt_account - && self.config.model_provider_id == "openai" - && !self.side_conversation_active(); + let can_enable_daybreak = + self.daybreak_account_eligible() && !self.side_conversation_active(); let notice = crate::daybreak::notice_for_setting( &self.model_catalog.models, self.current_model(), diff --git a/codex-rs/tui/src/daybreak.rs b/codex-rs/tui/src/daybreak.rs index 947bd4861b..687a1e8c66 100644 --- a/codex-rs/tui/src/daybreak.rs +++ b/codex-rs/tui/src/daybreak.rs @@ -13,7 +13,7 @@ pub(crate) fn program_for_turn( ) -> Result, String> { if !eligible_account { return if enabled { - Err("Daybreak requires a signed-in ChatGPT account and the OpenAI provider. Turn it off to continue.".into()) + Err("Daybreak requires the OpenAI provider with either ChatGPT sign-in or an API key with Daybreak support enabled. Turn it off to continue.".into()) } else { Ok(None) };