Honor Daybreak settings in TUI continuations and background tasks (#49859)

## Why

Cyber-policy refusal guidance did not distinguish whether Daybreak was on or off, and policy continuations and background task turns omitted `cyberAccessProgram`.

## What changed

- Show refusal guidance for the current Daybreak setting and offer eligible users an “Enable Daybreak” picker for their next request. Persist the selection without automatically retrying the refused request, and suppress the picker during history replay and when viewing parent-owned threads.
- Select and validate the cyber access program for policy continuations and background task turns using account eligibility, model support, and the thread's Daybreak setting.
- Resolve new background threads' Daybreak defaults from effective configuration unless launch flags or a selected profile explicitly override them. Use the target thread's saved setting for follow-up messages.

## Testing

Add unit, snapshot, and request-recording coverage for refusal guidance, picker behavior, continuation access programs, and background tasks retaining Daybreak after configuration changes.

GitOrigin-RevId: 94dee39c067e8eea5791cb31d5fe65fc600e5a18
This commit is contained in:
Eric Traut
2026-10-01 04:24:20 +00:00
committed by copyberry
parent e1522188e9
commit cd4a9cbd25
15 changed files with 431 additions and 62 deletions
@@ -114,6 +114,23 @@ 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 cyber_access_program = match crate::daybreak::program_for_turn(
&self.chat_widget.model_catalog().models,
self.chat_widget.current_model(),
eligible_account,
enabled,
) {
Ok(program) => program,
Err(message) => {
self.chat_widget.add_error_message(message);
return;
}
};
let config = self.chat_widget.config_ref();
let explicit_profile =
self.runtime_permission_profile_override
@@ -149,6 +166,7 @@ impl App {
approvals_reviewer: Some(config.approvals_reviewer.into()),
sandbox_policy,
permissions,
cyber_access_program: cyber_access_program.map(Into::into),
input: vec![UserInput::Text {
text: message.to_string(),
text_elements: Vec::new(),
@@ -3,6 +3,8 @@ use super::session_lifecycle_requests::start_recording_app_server;
use super::*;
use codex_app_server_protocol::MisalignmentErrorDetails;
use codex_app_server_protocol::MisalignmentSteer;
use codex_protocol::openai_models::ModelAccessPrograms;
use codex_protocol::turn_input::CyberAccessProgram;
use pretty_assertions::assert_eq;
fn policy_error() -> AppServerTurnError {
@@ -37,7 +39,11 @@ fn error_notification(
#[tokio::test]
async fn misalignment_continuation_requires_current_review_and_submits_once() -> Result<()> {
for (reject, preserve) in [(false, false), (false, true), (true, false)] {
for (reject, preserve, daybreak) in [
(false, false, false),
(false, true, true),
(true, false, false),
] {
let (mut app, mut rx, _) = make_test_app_with_channels().await;
let (mut server, requests, proxy) = start_recording_app_server(
&app.config,
@@ -57,6 +63,32 @@ async fn misalignment_continuation_requires_current_review_and_submits_once() ->
session.active_permission_profile = None;
app.active_thread_id = Some(thread_id);
app.chat_widget.handle_thread_session(session);
if !reject {
app.chat_widget.update_account_state(
/*status_account_display*/ None, /*plan_type*/ None,
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
);
app.chat_widget.open_model_popup();
let request_id = std::iter::from_fn(|| rx.try_recv().ok())
.find_map(|event| match event {
AppEvent::FetchModels { request_id } => Some(request_id),
_ => None,
})
.expect("model catalog request");
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
model.model = app.chat_widget.current_model().to_string();
model.available_access_programs = Some(ModelAccessPrograms {
cyber: vec![
CyberAccessProgram::Standard,
CyberAccessProgram::DaybreakBlue,
],
});
app.chat_widget
.on_models_loaded(request_id, Ok(vec![model]));
app.chat_widget
.handle_key_event(KeyEvent::from(KeyCode::Esc));
app.chat_widget.set_daybreak_enabled(daybreak);
}
app.runtime_permission_profile_override = Some(if preserve {
RuntimePermissionProfileOverride::from_restored_config(app.chat_widget.config_ref())
} else {
@@ -194,6 +226,11 @@ async fn misalignment_continuation_requires_current_review_and_submits_once() ->
approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(config.approvals_reviewer.into()),
sandbox_policy: (!preserve).then(|| config.legacy_sandbox_policy().into()),
cyber_access_program: Some(if daybreak {
CyberAccessProgram::DaybreakBlue.into()
} else {
CyberAccessProgram::Standard.into()
}),
input: vec![codex_app_server_protocol::UserInput::Text {
text: "Continue **only** within the requested scope.\nDo not edit files."
.to_string(),
@@ -1746,8 +1746,37 @@ async fn embedded_server_rejects_unowned_dynamic_tool_calls() -> Result<()> {
async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespace() -> Result<()> {
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
let codex_home = tempdir()?;
let backend = wiremock::MockServer::start().await;
let backend_url = format!("{}/backend-api", backend.uri());
app.config.codex_home = codex_home.path().to_path_buf().abs();
app.config.sqlite = SqliteConfig::new_for_testing(codex_home.path().abs());
let mut catalog = codex_models_manager::bundled_models_response()?;
for model in &mut catalog.models {
model.available_access_programs =
Some(codex_protocol::openai_models::ModelAccessPrograms {
cyber: vec![codex_protocol::turn_input::CyberAccessProgram::DaybreakBlue],
});
}
let catalog_path = codex_home.path().join("models.json");
std::fs::write(&catalog_path, serde_json::to_vec(&catalog)?)?;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
"daybreak = true\nmodel_catalog_json = {:?}\nchatgpt_base_url = {backend_url:?}\ncli_auth_credentials_store = \"file\"\n",
catalog_path.display().to_string()
),
)?;
app.config.chatgpt_base_url = backend_url;
app.config.model_catalog = Some(catalog);
app.config.cli_auth_credentials_store_mode = codex_login::AuthCredentialsStoreMode::File;
app_test_support::write_chatgpt_auth(
codex_home.path(),
app_test_support::ChatGptAuthFixture::new("test-token")
.chatgpt_user_id("test-user")
.plan_type("plus"),
codex_login::AuthCredentialsStoreMode::File,
)
.expect("write fixture auth");
app.config
.permissions
.set_permission_profile(PermissionProfile::workspace_write_with(
@@ -1941,7 +1970,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
params: codex_app_server_protocol::ThreadMetadataUpdateParams {
thread_id: creation_source.to_string(),
project_id: Some(project.project.id.clone()),
daybreak_enabled: None,
daybreak_enabled: Some(false),
git_info: None,
},
})
@@ -2026,6 +2055,10 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
.expect("background task creation")["projectId"],
project.project.id
);
assert_eq!(
recorded_params(&requests, "thread/start").last().unwrap()["daybreakEnabled"],
true
);
assert_eq!(
recorded_params(&requests, "thread/start")
.last()
@@ -2039,6 +2072,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
let turn = recorded_params(&requests, "turn/start")
.pop()
.expect("background task turn request");
assert_eq!(turn["cyberAccessProgram"], "daybreakBlue");
assert_eq!(turn["input"], serde_json::json!([]));
assert_eq!(
turn["toolOutput"],
@@ -2066,6 +2100,13 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
1
);
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("daybreak = true", "daybreak = false"),
)?;
spawn_approved_task_tool_call(
&app,
&app_server,
@@ -2077,7 +2118,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
namespace: Some("codex_tui".to_string()),
tool: "send_message_to_thread".to_string(),
arguments: serde_json::json!({
"threadId": creation_source,
"threadId": created_thread_id,
"prompt": "Follow <up> & report"
}),
},
@@ -2092,7 +2133,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
else {
panic!("expected follow-up task registration before its next turn")
};
assert_eq!(ThreadId::from_string(&thread.id)?, creation_source);
assert_eq!(ThreadId::from_string(&thread.id)?, created_thread_id);
assert_eq!(recorded_params(&requests, "turn/start").len(), 1);
Box::pin(app.handle_event(
&mut tui,
@@ -2113,6 +2154,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
};
assert!(response.success, "{response:?}");
let turn = &recorded_params(&requests, "turn/start")[1];
assert_eq!(turn["cyberAccessProgram"], "daybreakBlue");
assert_eq!(turn["input"], serde_json::json!([]));
assert_eq!(
turn["toolOutput"],
+18 -2
View File
@@ -475,12 +475,28 @@ impl AppServerSession {
})
})
.transpose()?;
let thread_start_params = thread_start_params_from_config(
let mut thread_start_params = thread_start_params_from_config(
&config,
self.thread_params_mode(),
self.remote_cwd_override(),
/*session_start_source*/ None,
);
let daybreak_launch_override = config
.config_layer_stack
.layers_high_to_low()
.find(|layer| layer.config.get("daybreak").is_some())
.is_some_and(|layer| {
matches!(
layer.name,
codex_config::ConfigLayerSource::SessionFlags
| codex_config::ConfigLayerSource::User {
profile: Some(_),
..
}
)
});
thread_start_params.daybreak_enabled =
daybreak_launch_override.then_some(config.daybreak_enabled);
self.dynamic_tool_mcp = Some(Arc::new(
DynamicToolMcpServer::start(
self.request_handle(),
@@ -1761,7 +1777,7 @@ pub(crate) fn status_account_display_from_auth_mode(
}
}
fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
pub(crate) fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
let upgrade = model.upgrade.map(|upgrade_id| {
let upgrade_info = model.upgrade_info.clone();
ModelUpgrade {
@@ -0,0 +1,11 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "render_bottom_popup(&chat, 80)"
---
Turn on Daybreak for your next request?
› 1. Enable Daybreak
2. Not now
enter select · esc back
@@ -0,0 +1,13 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
assertion_line: 195
expression: "format!(\"{}\\n{}\", lines_to_single_string(cells.last().unwrap()),\nrender_bottom_popup(&chat, 80))"
---
ⓘ This content can’t be shown
Daybreak is currently off. If you’re doing authorized security work, turn it
on and try your request again.
Learn more: https://help.openai.com/en/articles/20001326
› Viewing sub-agent — direct input is disabled
GPT-6.1-Sol default · /tmp/project
@@ -123,13 +123,6 @@ 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(Some("gpt-5.6-sol")).await;
chat.has_chatgpt_account = true;
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
model.model = "gpt-5.6-sol".into();
model.available_access_programs = Some(codex_protocol::openai_models::ModelAccessPrograms {
cyber: vec![codex_protocol::turn_input::CyberAccessProgram::Standard],
});
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![model]));
handle_error(
&mut chat,
@@ -146,6 +139,69 @@ async fn app_server_cyber_policy_error_renders_dedicated_notice() {
assert!(!rendered.contains("server fallback message"));
}
#[tokio::test]
async fn daybreak_refusal_offers_enable_for_the_next_turn() {
let (mut chat, mut events, mut ops) = make_chatwidget_manual_with_auth(
Some("gpt-5.6-sol"),
/*has_chatgpt_account*/ true,
/*has_codex_backend_auth*/ true,
FrameRequester::test_dummy(),
)
.await;
let thread_id = ThreadId::new();
chat.thread_id = Some(thread_id);
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
model.model = "gpt-5.6-sol".into();
model.available_access_programs = Some(codex_protocol::openai_models::ModelAccessPrograms {
cyber: vec![codex_protocol::turn_input::CyberAccessProgram::DaybreakBlue],
});
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![model]));
chat.thread_usage.replaying_turn_completion = true;
chat.on_cyber_policy_error();
let cells = drain_insert_history(&mut events);
assert!(lines_to_single_string(&cells[0]).contains("Daybreak is currently off"));
assert!(chat.bottom_pane.no_modal_or_popup_active());
chat.thread_usage.replaying_turn_completion = false;
chat.daybreak_enabled = true;
chat.on_cyber_policy_error();
let cells = drain_insert_history(&mut events);
assert!(lines_to_single_string(&cells[0]).contains("even when Daybreak is on"));
assert!(chat.bottom_pane.no_modal_or_popup_active());
chat.daybreak_enabled = false;
chat.on_cyber_policy_error();
assert_chatwidget_snapshot!(
"daybreak_refusal_enable_picker",
render_bottom_popup(&chat, /*width*/ 80)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(
std::iter::from_fn(|| events.try_recv().ok()).any(|event| matches!(event,
AppEvent::PersistDaybreakSelection { thread_id: id, enabled: true } if id == thread_id
))
);
assert!(ops.try_recv().is_err());
chat.set_parent_owned_thread();
handle_error(
&mut chat,
"server fallback message",
Some(CodexErrorInfo::CyberPolicy),
);
let cells = drain_insert_history(&mut events);
assert!(chat.bottom_pane.no_modal_or_popup_active());
assert_chatwidget_snapshot!(
"daybreak_refusal_parent_owned",
normalize_snapshot_paths(format!(
"{}\n{}",
lines_to_single_string(cells.last().unwrap()),
render_bottom_popup(&chat, /*width*/ 80)
))
);
}
#[tokio::test]
async fn app_server_model_verification_renders_warning() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
+38 -5
View File
@@ -392,14 +392,47 @@ 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 notice = crate::daybreak::notice_for_setting(
&self.model_catalog.models,
self.current_model(),
self.daybreak_enabled && can_enable_daybreak,
can_enable_daybreak,
);
self.input_queue.submit_pending_steers_after_interrupt = false;
self.finalize_turn();
let notice = if self.has_chatgpt_account && self.config.model_provider_id == "openai" {
crate::daybreak::notice_for_model(&self.model_catalog.models, self.current_model())
} else {
crate::daybreak::Notice::Apply
};
self.add_to_history(history_cell::new_cyber_policy_error_event(notice));
if notice == crate::daybreak::Notice::Disabled
&& !self.thread_usage.replaying_turn_completion
&& !self.blocks_direct_input
&& let Some(thread_id) = self.thread_id
{
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Turn on Daybreak for your next request?".into()),
items: vec![
SelectionItem {
name: "Enable Daybreak".into(),
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::PersistDaybreakSelection {
thread_id,
enabled: true,
})
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: "Not now".into(),
dismiss_on_select: true,
..Default::default()
},
],
..SelectionViewParams::picker()
});
self.defer_input_until_settings_applied();
}
self.request_redraw();
// After an error ends the turn, try sending the next queued input.
+28 -18
View File
@@ -32,31 +32,41 @@ pub(crate) fn program_for_turn(
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Notice {
Apply,
Astra,
#[default]
Limited,
Disabled,
Enabled,
}
pub(crate) fn notice_for_model(models: &[ModelPreset], model: &str) -> Notice {
let programs = models
.iter()
.find(|entry| entry.model == model)
.and_then(|entry| entry.available_access_programs.as_ref());
if matches!(model, "gpt-6-astra" | "gpt-6-astra-wm")
&& programs.is_some_and(|programs| {
programs.cyber.contains(&CyberAccessProgram::Standard) && programs.daybreak().is_none()
})
{
return Notice::Astra;
pub(crate) fn notice_for_setting(
models: &[ModelPreset],
model: &str,
enabled: bool,
can_enable_daybreak: bool,
) -> Notice {
if enabled {
return Notice::Enabled;
}
if programs.and_then(ModelAccessPrograms::daybreak).is_some() {
Notice::Limited
} else {
Notice::Apply
if can_enable_daybreak {
let programs = models
.iter()
.find(|entry| entry.model == model)
.and_then(|entry| entry.available_access_programs.as_ref());
if matches!(model, "gpt-6-astra" | "gpt-6-astra-wm")
&& programs.is_some_and(|programs| {
programs.cyber.contains(&CyberAccessProgram::Standard)
&& programs.daybreak().is_none()
})
{
return Notice::Astra;
}
if programs.and_then(ModelAccessPrograms::daybreak).is_some() {
return Notice::Disabled;
}
}
Notice::Apply
}
pub(crate) fn available(models: &[ModelPreset]) -> bool {
+52 -14
View File
@@ -48,7 +48,12 @@ fn refusal_guidance_uses_the_model_catalog() {
] {
model.model = name.into();
assert_eq!(
notice_for_model(std::slice::from_ref(&model), name),
notice_for_setting(
std::slice::from_ref(&model),
name,
/*enabled*/ false,
/*can_enable_daybreak*/ true,
),
expected
);
}
@@ -59,20 +64,53 @@ fn refusal_guidance_uses_the_model_catalog() {
.cyber
.push(CyberAccessProgram::DaybreakBlue);
assert_eq!(
notice_for_model(std::slice::from_ref(&model), &model.model),
Notice::Limited
notice_for_setting(
std::slice::from_ref(&model),
&model.model,
/*enabled*/ false,
/*can_enable_daybreak*/ true,
),
Notice::Disabled
);
let mut other = model.clone();
assert_eq!(
notice_for_setting(
std::slice::from_ref(&model),
&model.model,
/*enabled*/ true,
/*can_enable_daybreak*/ true,
),
Notice::Enabled
);
model.available_access_programs.as_mut().unwrap().cyber =
vec![CyberAccessProgram::DaybreakBlue];
assert_eq!(
notice_for_setting(
std::slice::from_ref(&model),
&model.model,
/*enabled*/ false,
/*can_enable_daybreak*/ true
),
Notice::Disabled
);
let other = model.clone();
model.model = "gpt-5.6-sol".into();
other.model = "another-model".into();
model.available_access_programs.as_mut().unwrap().cyber = vec![CyberAccessProgram::Standard];
for programs in [None, other.available_access_programs.clone()] {
other.available_access_programs = programs;
assert_eq!(
notice_for_model(&[model.clone(), other.clone()], &model.model),
Notice::Apply
);
}
assert_eq!(notice_for_model(&[], "gpt-6-astra"), Notice::Apply);
assert_eq!(
notice_for_setting(
&[model.clone(), other],
&model.model,
/*enabled*/ false,
/*can_enable_daybreak*/ true
),
Notice::Apply
);
assert_eq!(
notice_for_setting(
&[],
"gpt-6-astra",
/*enabled*/ false,
/*can_enable_daybreak*/ true
),
Notice::Apply
);
}
+78
View File
@@ -4,6 +4,7 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use codex_app_server_client::AppServerRequestHandle;
use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::Account;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
use codex_app_server_protocol::DynamicToolCallParams;
@@ -12,7 +13,11 @@ use codex_app_server_protocol::DynamicToolFunctionSpec;
use codex_app_server_protocol::DynamicToolNamespaceSpec;
use codex_app_server_protocol::DynamicToolNamespaceTool;
use codex_app_server_protocol::DynamicToolSpec;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::ImageReference;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::SandboxPolicy;
@@ -480,6 +485,19 @@ async fn execute_inner(
thread_start_params.cwd = Some(source_thread.cwd.to_string_lossy().into_owned());
thread_start_params.project_id = source_thread.project_id.clone();
thread_start_params.ephemeral = Some(source_thread.ephemeral);
if thread_start_params.daybreak_enabled.is_none() {
let defaults = crate::config_update::read_effective_config(
handle.clone(),
source_thread.cwd.to_string_lossy().into_owned(),
)
.await
.map_err(|error| error.to_string())?;
thread_start_params.daybreak_enabled = defaults
.config
.additional
.get("daybreak")
.and_then(Value::as_bool);
}
thread_start_params.history_mode = (source_thread.history_mode
== ThreadHistoryMode::Paginated)
.then_some(ThreadHistoryMode::Paginated);
@@ -533,7 +551,15 @@ async fn execute_inner(
.await
.map_err(|error| error.to_string())?;
let thread_id = started.thread.id.clone();
let daybreak_enabled = started.thread.daybreak_enabled.unwrap_or(false);
register_background_thread(app_event_tx, started.thread, task_tools_available).await?;
let cyber_access_program = background_turn_program(
&handle,
&started.model,
&started.model_provider,
daybreak_enabled,
)
.await?;
if let Some(title) = arguments.title
&& let Err(error) = request::<ThreadSetNameResponse>(&handle, |request_id| {
ClientRequest::ThreadSetName {
@@ -555,6 +581,7 @@ async fn execute_inner(
prompt,
/*model*/ None,
sandbox_policy,
cyber_access_program,
)
.await?;
Ok(json!({"threadId": thread_id}))
@@ -669,12 +696,20 @@ async fn execute_inner(
},
)
.await?;
let daybreak_enabled = thread.daybreak_enabled.unwrap_or(false);
register_background_thread(
app_event_tx,
resumed.thread,
/*task_tools_available*/ false,
)
.await?;
let cyber_access_program = background_turn_program(
&handle,
arguments.model.as_deref().unwrap_or(&resumed.model),
&resumed.model_provider,
daybreak_enabled,
)
.await?;
start_turn(
&handle,
&arguments.thread_id,
@@ -682,6 +717,7 @@ async fn execute_inner(
prompt,
arguments.model,
/*sandbox_policy*/ None,
cyber_access_program,
)
.await?;
Ok(json!({"threadId": arguments.thread_id}))
@@ -1187,6 +1223,7 @@ async fn start_turn(
prompt: String,
model: Option<String>,
sandbox_policy: Option<SandboxPolicy>,
cyber_access_program: Option<codex_app_server_protocol::CyberAccessProgram>,
) -> Result<TurnStartResponse, String> {
request(handle, |request_id| ClientRequest::TurnStart {
request_id,
@@ -1202,12 +1239,53 @@ async fn start_turn(
})),
model,
sandbox_policy,
cyber_access_program,
..TurnStartParams::default()
},
})
.await
}
async fn background_turn_program(
handle: &AppServerRequestHandle,
model: &str,
provider: &str,
enabled: bool,
) -> Result<Option<codex_app_server_protocol::CyberAccessProgram>, String> {
let (account, models) = tokio::join!(
request::<GetAccountResponse>(handle, |request_id| ClientRequest::GetAccount {
request_id,
params: GetAccountParams {
refresh_token: false
},
}),
request::<ModelListResponse>(handle, |request_id| ClientRequest::ModelList {
request_id,
params: ModelListParams {
cursor: None,
limit: None,
include_hidden: Some(true)
},
}),
);
let eligible = provider == "openai"
&& matches!(
account.ok().and_then(|response| response.account),
Some(Account::Chatgpt { .. })
);
let models = models
.map(|response| {
response
.data
.into_iter()
.map(crate::app_server_session::model_preset_from_api_model)
.collect::<Vec<_>>()
})
.unwrap_or_default();
crate::daybreak::program_for_turn(&models, model, eligible, enabled)
.map(|program| program.map(Into::into))
}
fn thread_summary(thread: &Thread) -> Value {
json!({
"id": thread.id,
+6 -2
View File
@@ -180,12 +180,16 @@ pub(crate) fn new_cyber_policy_error_event(
),
],
),
Notice::Disabled => (
"Daybreak is currently off. If you’re doing authorized security work, turn it on and try your request again.",
&[("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL)],
),
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.",
Notice::Enabled => (
"Some cybersecurity requests are still limited, even when Daybreak is on.",
&[("Learn more", SAFETY_ACCESS_BLOCK_LEARN_MORE_URL)],
),
};
@@ -0,0 +1,12 @@
---
source: tui/src/history_cell/tests.rs
expression: rendered
---
ⓘ This content can’t be shown
Daybreak is currently off. If you’re doing authorized security work, turn it
on and try your request again.
Learn more: https://help.openai.com/en/articles/20001326
ⓘ This content can’t be shown
Some cybersecurity requests are still limited, even when Daybreak is on.
Learn more: https://help.openai.com/en/articles/20001326
@@ -1,7 +0,0 @@
---
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
+11 -3
View File
@@ -916,9 +916,17 @@ fn cyber_policy_error_event_astra_snapshot() {
}
#[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");
fn cyber_policy_error_event_available_snapshot() {
let rendered = [
crate::daybreak::Notice::Disabled,
crate::daybreak::Notice::Enabled,
]
.into_iter()
.map(|notice| {
render_lines(&new_cyber_policy_error_event(notice).display_lines(/*width*/ 80)).join("\n")
})
.collect::<Vec<_>>()
.join("\n\n");
insta::assert_snapshot!(rendered);
}