mirror of
https://github.com/openai/codex.git
synced 2026-10-19 08:16:48 +00:00
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:
@@ -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"],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+11
@@ -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
|
||||
+13
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)],
|
||||
),
|
||||
};
|
||||
|
||||
+12
@@ -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
|
||||
-7
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user