mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Gate CLI Daybreak controls and selection behind an opt-in feature (#51207)
## What changed Add `features.cli_daybreak`, disabled by default, to gate Daybreak controls and automatic access-program selection in the TUI and `codex exec`. Apply the gate to resumed sessions and background task turns, and hide `/daybreak` when disabled. Explicit `--cyber-access-program` overrides remain available. ## Testing Extend tests to cover disabled controls, history replay, foreground and background turn requests, exec resume/fork behavior, and explicit access-program overrides with the feature disabled. GitOrigin-RevId: ae96a4a8c7ab97c619716b9f39ea9f295df6e5a6
This commit is contained in:
@@ -791,6 +791,9 @@
|
||||
"chronicle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cli_daybreak": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"code_mode": {
|
||||
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
|
||||
},
|
||||
@@ -6961,6 +6964,9 @@
|
||||
"chronicle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cli_daybreak": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"code_mode": {
|
||||
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
|
||||
},
|
||||
|
||||
@@ -875,7 +875,10 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
cyber_access_program,
|
||||
} = args;
|
||||
|
||||
if config.daybreak_enabled && !matches!(&command, Some(ExecCommand::Review(_))) {
|
||||
if config.features.enabled(Feature::CliDaybreak)
|
||||
&& config.daybreak_enabled
|
||||
&& !matches!(&command, Some(ExecCommand::Review(_)))
|
||||
{
|
||||
anyhow::ensure!(
|
||||
!oss || matches!(
|
||||
&command,
|
||||
@@ -1208,7 +1211,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
} => {
|
||||
let cyber_access_program = match cyber_access_program {
|
||||
Some(program) => Some(program),
|
||||
None => {
|
||||
None if config.features.enabled(Feature::CliDaybreak) => {
|
||||
daybreak::program_for_turn(
|
||||
&client,
|
||||
&mut request_ids,
|
||||
@@ -1218,6 +1221,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let response: TurnStartResponse = send_request_with_response(
|
||||
&client,
|
||||
|
||||
@@ -56,7 +56,10 @@ async fn exec_rejects_daybreak_for_api_keys_and_allows_an_override() -> anyhow::
|
||||
async fn exec_rejects_daybreak_for_non_openai_provider() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let server = responses::start_mock_server().await;
|
||||
std::fs::write(test.home_path().join("config.toml"), "daybreak = true\n")?;
|
||||
std::fs::write(
|
||||
test.home_path().join("config.toml"),
|
||||
"daybreak = true\nfeatures.cli_daybreak = true\n",
|
||||
)?;
|
||||
let response = responses::mount_sse_once(&server, responses::sse_completed("response")).await;
|
||||
|
||||
test.cmd_with_server(&server)
|
||||
@@ -110,7 +113,7 @@ async fn configured_daybreak_exec() -> anyhow::Result<(TestCodexExecBuilder, wir
|
||||
std::fs::write(
|
||||
test.home_path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\ndaybreak = true\n",
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\ndaybreak = true\nfeatures.cli_daybreak = true\n",
|
||||
server.uri(),
|
||||
catalog.display()
|
||||
),
|
||||
@@ -154,7 +157,7 @@ async fn exec_resumes_and_forks_the_saved_daybreak_choice() -> anyhow::Result<()
|
||||
std::fs::write(
|
||||
test.home_path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\n",
|
||||
"cli_auth_credentials_store = 'file'\nchatgpt_base_url = '{}/backend-api'\nmodel_catalog_json = '{}'\nmodel = 'gpt-test'\nfeatures.cli_daybreak = true\n",
|
||||
server.uri(),
|
||||
test.home_path().join("catalog.json").display()
|
||||
),
|
||||
@@ -168,19 +171,26 @@ async fn exec_resumes_and_forks_the_saved_daybreak_choice() -> anyhow::Result<()
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
"standard",
|
||||
Some("standard"),
|
||||
),
|
||||
(
|
||||
vec!["resume", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
Some("daybreak_blue"),
|
||||
),
|
||||
(
|
||||
vec!["fork", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
Some("daybreak_blue"),
|
||||
),
|
||||
(
|
||||
vec!["--ephemeral", "fork", thread_id.as_str(), "continue"],
|
||||
"daybreak_blue",
|
||||
vec![
|
||||
"-c",
|
||||
"features.cli_daybreak=false",
|
||||
"--ephemeral",
|
||||
"fork",
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
@@ -191,7 +201,7 @@ async fn exec_resumes_and_forks_the_saved_daybreak_choice() -> anyhow::Result<()
|
||||
thread_id.as_str(),
|
||||
"continue",
|
||||
],
|
||||
"standard",
|
||||
Some("standard"),
|
||||
),
|
||||
] {
|
||||
let response =
|
||||
@@ -205,8 +215,8 @@ async fn exec_resumes_and_forks_the_saved_daybreak_choice() -> anyhow::Result<()
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": expected})
|
||||
response.single_request().body_json().get("access_programs"),
|
||||
expected.map(|program| json!({"cyber": program})).as_ref()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -225,6 +235,8 @@ async fn exec_explicit_cyber_program_overrides_daybreak_for_one_turn() -> anyhow
|
||||
.args([
|
||||
"--skip-git-repo-check",
|
||||
"--json",
|
||||
"-c",
|
||||
"features.cli_daybreak=false",
|
||||
"--cyber-access-program",
|
||||
"daybreak_red",
|
||||
"hello",
|
||||
@@ -266,14 +278,16 @@ async fn exec_resume_override_does_not_change_a_standard_thread() -> anyhow::Res
|
||||
"--skip-git-repo-check",
|
||||
"--json",
|
||||
"-c",
|
||||
"features.cli_daybreak=false",
|
||||
"-c",
|
||||
"daybreak=false",
|
||||
"hello",
|
||||
])
|
||||
.output()?;
|
||||
let thread_id = started_thread(output)?;
|
||||
assert_eq!(
|
||||
response.single_request().body_json()["access_programs"],
|
||||
json!({"cyber": "standard"})
|
||||
response.single_request().body_json().get("access_programs"),
|
||||
None
|
||||
);
|
||||
for (args, expected) in [
|
||||
(
|
||||
|
||||
@@ -98,6 +98,8 @@ pub enum Feature {
|
||||
ApiKeyModelDiscovery,
|
||||
/// Forward explicit programs with builtin OpenAI API keys.
|
||||
ApiKeyCyberAccessPrograms,
|
||||
/// Enable Daybreak controls and automatic access-program selection in CLI clients.
|
||||
CliDaybreak,
|
||||
/// Deprecated no-op; use `tui.fullscreen_transcript` instead.
|
||||
TranscriptV2,
|
||||
// Stable.
|
||||
@@ -1338,6 +1340,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::CliDaybreak,
|
||||
key: "cli_daybreak",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ApiKeyCyberAccessPrograms,
|
||||
key: "api_key_cyber_access_programs",
|
||||
|
||||
@@ -622,11 +622,13 @@ impl App {
|
||||
app_server_client
|
||||
.thread_tool_transport()
|
||||
.configure(&mut thread_start_params);
|
||||
let features = self.config.features.get().clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let response = crate::dynamic_tools::execute(
|
||||
request_handle,
|
||||
params,
|
||||
thread_start_params,
|
||||
features,
|
||||
status_updates,
|
||||
Some(&app_event_tx),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,13 @@ impl App {
|
||||
thread_id: ThreadId,
|
||||
enabled: bool,
|
||||
) {
|
||||
if self.active_thread_id != Some(thread_id) {
|
||||
if !self
|
||||
.chat_widget
|
||||
.config_ref()
|
||||
.features
|
||||
.enabled(Feature::CliDaybreak)
|
||||
|| self.active_thread_id != Some(thread_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if enabled
|
||||
|
||||
@@ -184,6 +184,7 @@ async fn reconnect_restores_history_permissions_and_resumes_unsent_input() -> Re
|
||||
crate::dynamic_tools_mcp::DynamicToolMcpServer::start(
|
||||
session.request_handle(),
|
||||
codex_app_server_protocol::ThreadStartParams::default(),
|
||||
app.config.features.get().clone(),
|
||||
app.app_event_tx.clone(),
|
||||
app.dynamic_tool_status_updates.clone(),
|
||||
/*managed_requirement*/ None,
|
||||
|
||||
@@ -39,12 +39,16 @@ fn error_notification(
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_continuation_requires_current_review_and_submits_once() -> Result<()> {
|
||||
for (reject, preserve, daybreak) in [
|
||||
(false, false, false),
|
||||
(false, true, true),
|
||||
(true, false, false),
|
||||
for (reject, preserve, daybreak, rollout_enabled) in [
|
||||
(false, false, false, true),
|
||||
(false, true, true, true),
|
||||
(true, false, false, true),
|
||||
(false, false, false, false),
|
||||
(false, true, true, false),
|
||||
] {
|
||||
let (mut app, mut rx, _) = make_test_app_with_channels().await;
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, rollout_enabled);
|
||||
let (mut server, requests, proxy) = start_recording_app_server(
|
||||
&app.config,
|
||||
/*blocked_thread_list*/ None,
|
||||
@@ -226,7 +230,7 @@ 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 {
|
||||
cyber_access_program: rollout_enabled.then_some(if daybreak {
|
||||
CyberAccessProgram::DaybreakBlue.into()
|
||||
} else {
|
||||
CyberAccessProgram::Standard.into()
|
||||
|
||||
@@ -784,6 +784,8 @@ goals = true
|
||||
/*status_account_display*/ None, /*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ false,
|
||||
);
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
app.chat_widget.set_daybreak_enabled(/*enabled*/ true);
|
||||
Box::pin(app.retry_safety_buffered_turn(
|
||||
&mut tui,
|
||||
|
||||
@@ -49,6 +49,9 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
use codex_protocol::turn_input::CyberAccessProgram;
|
||||
|
||||
let (mut app, mut events, mut ops) = make_test_app_with_channels().await;
|
||||
app.config.features.enable(Feature::CliDaybreak)?;
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
let (mut server, requests, proxy) = start_recording_app_server(
|
||||
&app.config,
|
||||
/*blocked_thread_list*/ None,
|
||||
@@ -175,6 +178,18 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
assert_eq!(turns[0]["cyberAccessProgram"], "standard");
|
||||
assert_eq!(turns[1]["cyberAccessProgram"], "daybreakBlue");
|
||||
assert_eq!(turns[2]["cyberAccessProgram"], "standard");
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ false);
|
||||
for target in [thread_id, background_thread_id] {
|
||||
app.submit_thread_op(&mut server, target, turn.clone())
|
||||
.await?;
|
||||
}
|
||||
let turns = recorded_params(&requests, "turn/start");
|
||||
assert!(turns[3]["cyberAccessProgram"].is_null());
|
||||
assert!(turns[4]["cyberAccessProgram"].is_null());
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
app.chat_widget.set_daybreak_enabled(/*enabled*/ true);
|
||||
app.chat_widget.update_account_state(
|
||||
Some(crate::status::StatusAccountDisplay::ApiKey),
|
||||
/*plan_type*/ None,
|
||||
@@ -190,8 +205,8 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
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());
|
||||
assert_eq!(turns[5]["cyberAccessProgram"], "daybreakBlue");
|
||||
assert!(turns[6]["cyberAccessProgram"].is_null());
|
||||
while events.try_recv().is_ok() {}
|
||||
|
||||
let missing_thread_id = ThreadId::new();
|
||||
@@ -1004,11 +1019,13 @@ fn spawn_approved_task_tool_call(
|
||||
app_server
|
||||
.thread_tool_transport()
|
||||
.configure(&mut thread_start_params);
|
||||
let features = app.config.features.get().clone();
|
||||
tokio::spawn(async move {
|
||||
let response = crate::dynamic_tools::execute(
|
||||
request_handle,
|
||||
params,
|
||||
thread_start_params,
|
||||
features,
|
||||
status_updates,
|
||||
Some(&app_event_tx),
|
||||
)
|
||||
@@ -1772,7 +1789,15 @@ async fn embedded_server_rejects_unowned_dynamic_tool_calls() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespace() -> Result<()> {
|
||||
check_dynamic_tool_requests(/*rollout_enabled*/ true).await?;
|
||||
check_dynamic_tool_requests(/*rollout_enabled*/ false).await
|
||||
}
|
||||
|
||||
async fn check_dynamic_tool_requests(rollout_enabled: bool) -> Result<()> {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
app.config
|
||||
.features
|
||||
.set_enabled(Feature::CliDaybreak, rollout_enabled)?;
|
||||
let codex_home = tempdir()?;
|
||||
let backend = wiremock::MockServer::start().await;
|
||||
let backend_url = format!("{}/backend-api", backend.uri());
|
||||
@@ -2085,7 +2110,7 @@ async fn dynamic_tool_requests_ignore_other_namespaces_and_dispatch_tui_namespac
|
||||
);
|
||||
assert_eq!(
|
||||
recorded_params(&requests, "thread/start").last().unwrap()["daybreakEnabled"],
|
||||
true
|
||||
serde_json::json!(rollout_enabled.then_some(true))
|
||||
);
|
||||
assert_eq!(
|
||||
recorded_params(&requests, "thread/start")
|
||||
@@ -2100,7 +2125,10 @@ 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["cyberAccessProgram"],
|
||||
serde_json::json!(rollout_enabled.then_some("daybreakBlue"))
|
||||
);
|
||||
assert_eq!(turn["input"], serde_json::json!([]));
|
||||
assert_eq!(
|
||||
turn["toolOutput"],
|
||||
@@ -2182,7 +2210,10 @@ 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["cyberAccessProgram"],
|
||||
serde_json::json!(rollout_enabled.then_some("daybreakBlue"))
|
||||
);
|
||||
assert_eq!(turn["input"], serde_json::json!([]));
|
||||
assert_eq!(
|
||||
turn["toolOutput"],
|
||||
@@ -2686,6 +2717,7 @@ async fn remote_legacy_history_start_negotiates_once_for_resume_and_fork() -> Re
|
||||
}),
|
||||
},
|
||||
codex_app_server_protocol::ThreadStartParams::default(),
|
||||
app.config.features.get().clone(),
|
||||
status_updates,
|
||||
/*app_event_tx*/ None,
|
||||
)
|
||||
|
||||
@@ -547,6 +547,7 @@ impl AppServerSession {
|
||||
DynamicToolMcpServer::start(
|
||||
self.request_handle(),
|
||||
thread_start_params,
|
||||
config.features.get().clone(),
|
||||
app_event_tx,
|
||||
status_updates,
|
||||
managed_requirement,
|
||||
|
||||
@@ -72,7 +72,8 @@ impl ChatWidget {
|
||||
let connector_scope_changed = previous_thread_id != Some(session.thread_id)
|
||||
|| self.config.cwd.as_path() != session.cwd.as_path();
|
||||
self.thread_id = Some(session.thread_id);
|
||||
self.daybreak_enabled = session.daybreak_enabled
|
||||
self.daybreak_enabled = self.config.features.enabled(Feature::CliDaybreak)
|
||||
&& session.daybreak_enabled
|
||||
&& !matches!(display, SessionConfiguredDisplay::SideConversation);
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.windows_sandbox_local_server
|
||||
|
||||
@@ -6,14 +6,15 @@ use crate::chatwidget::rate_limits::RATE_LIMIT_SWITCH_PROMPT_VIEW_ID;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn set_daybreak_enabled(&mut self, enabled: bool) {
|
||||
self.daybreak_enabled = enabled;
|
||||
self.daybreak_enabled = enabled && self.config.features.enabled(Feature::CliDaybreak);
|
||||
self.bottom_pane
|
||||
.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.config.features.enabled(Feature::CliDaybreak)
|
||||
&& self.config.model_provider_id == "openai"
|
||||
&& (self.has_chatgpt_account
|
||||
|| matches!(
|
||||
self.status_account_display,
|
||||
@@ -111,6 +112,10 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
let enabled = self.config.features.enabled(feature);
|
||||
if feature == Feature::CliDaybreak {
|
||||
self.set_daybreak_enabled(self.daybreak_enabled);
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
if feature == Feature::FastMode {
|
||||
self.refresh_effective_service_tier();
|
||||
self.sync_service_tier_commands();
|
||||
|
||||
@@ -189,6 +189,9 @@ impl ChatWidget {
|
||||
|
||||
match cmd {
|
||||
SlashCommand::Daybreak => {
|
||||
if !self.config.features.enabled(Feature::CliDaybreak) {
|
||||
return;
|
||||
}
|
||||
if !self.daybreak_enabled {
|
||||
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());
|
||||
|
||||
@@ -19,7 +19,7 @@ async fn resumed_initial_messages_render_history() {
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
let mut configured = crate::session_state::ThreadSessionState {
|
||||
daybreak_enabled: true,
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
@@ -43,6 +43,18 @@ async fn resumed_initial_messages_render_history() {
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_thread_session(configured.clone());
|
||||
assert!(!chat.daybreak_enabled);
|
||||
insta::assert_snapshot!(
|
||||
drain_insert_history(&mut rx).into_iter().flatten()
|
||||
.map(|line| line.to_string())
|
||||
.filter(|line| line.contains("Daybreak"))
|
||||
.collect::<Vec<_>>().join("\n"),
|
||||
@""
|
||||
);
|
||||
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
configured.thread_id = ThreadId::new();
|
||||
chat.handle_thread_session(configured);
|
||||
assert!(chat.daybreak_enabled);
|
||||
replay_user_message_text(
|
||||
|
||||
@@ -2168,6 +2168,7 @@ async fn slash_copy_picker_remains_available_from_parent_owned_threads() {
|
||||
#[tokio::test]
|
||||
async fn slash_daybreak_offers_an_application_when_unavailable() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
chat.has_chatgpt_account = true;
|
||||
chat.config.model_provider_id = "openai".into();
|
||||
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
|
||||
@@ -2250,6 +2251,12 @@ async fn slash_daybreak_offers_an_application_when_unavailable() {
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ false);
|
||||
chat.set_daybreak_enabled(/*enabled*/ true);
|
||||
assert!(!chat.daybreak_enabled);
|
||||
assert!(chat.daybreak_command_description().is_none());
|
||||
chat.dispatch_command(SlashCommand::Daybreak);
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -148,6 +148,7 @@ async fn daybreak_refusal_offers_enable_for_the_next_turn() {
|
||||
FrameRequester::test_dummy(),
|
||||
)
|
||||
.await;
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
let thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(thread_id);
|
||||
let mut model = crate::test_support::TEST_MODEL_PRESETS[0].clone();
|
||||
|
||||
@@ -446,6 +446,7 @@ async fn status_line_setup_popup_rate_limits_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn status_line_setup_popup_mixed_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
chat.status_line_branch = Some("feature/mixed-preview".to_string());
|
||||
chat.thread_name = Some("Mixed preview thread".to_string());
|
||||
chat.set_daybreak_enabled(/*enabled*/ true);
|
||||
@@ -500,6 +501,7 @@ async fn terminal_title_setup_popup_hardcoded_only_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn terminal_title_setup_popup_mixed_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::CliDaybreak, /*enabled*/ true);
|
||||
chat.thread_name = Some("Mixed preview thread".to_string());
|
||||
chat.set_daybreak_enabled(/*enabled*/ true);
|
||||
chat.local_settings.tui.terminal_title = Some(vec![
|
||||
|
||||
@@ -54,6 +54,8 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnToolOutput;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use serde::Deserialize;
|
||||
@@ -290,6 +292,7 @@ pub(crate) async fn execute(
|
||||
request_handle: AppServerRequestHandle,
|
||||
params: DynamicToolCallParams,
|
||||
thread_start_params: ThreadStartParams,
|
||||
features: Features,
|
||||
status_updates: broadcast::Receiver<ThreadStatusChangedNotification>,
|
||||
app_event_tx: Option<&AppEventSender>,
|
||||
) -> DynamicToolCallResponse {
|
||||
@@ -297,6 +300,7 @@ pub(crate) async fn execute(
|
||||
request_handle,
|
||||
params,
|
||||
thread_start_params,
|
||||
features,
|
||||
status_updates,
|
||||
app_event_tx,
|
||||
)
|
||||
@@ -320,6 +324,7 @@ async fn execute_inner(
|
||||
handle: AppServerRequestHandle,
|
||||
params: DynamicToolCallParams,
|
||||
mut thread_start_params: ThreadStartParams,
|
||||
features: Features,
|
||||
mut status_updates: broadcast::Receiver<ThreadStatusChangedNotification>,
|
||||
app_event_tx: Option<&AppEventSender>,
|
||||
) -> Result<Value, String> {
|
||||
@@ -485,7 +490,9 @@ 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() {
|
||||
if features.enabled(Feature::CliDaybreak)
|
||||
&& 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(),
|
||||
@@ -554,6 +561,7 @@ async fn execute_inner(
|
||||
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(
|
||||
&features,
|
||||
&handle,
|
||||
&started.model,
|
||||
&started.model_provider,
|
||||
@@ -704,6 +712,7 @@ async fn execute_inner(
|
||||
)
|
||||
.await?;
|
||||
let cyber_access_program = background_turn_program(
|
||||
&features,
|
||||
&handle,
|
||||
arguments.model.as_deref().unwrap_or(&resumed.model),
|
||||
&resumed.model_provider,
|
||||
@@ -1247,11 +1256,15 @@ async fn start_turn(
|
||||
}
|
||||
|
||||
async fn background_turn_program(
|
||||
features: &Features,
|
||||
handle: &AppServerRequestHandle,
|
||||
model: &str,
|
||||
provider: &str,
|
||||
enabled: bool,
|
||||
) -> Result<Option<codex_app_server_protocol::CyberAccessProgram>, String> {
|
||||
if !features.enabled(Feature::CliDaybreak) {
|
||||
return Ok(None);
|
||||
}
|
||||
let (account, models) = tokio::join!(
|
||||
request::<GetAccountResponse>(handle, |request_id| ClientRequest::GetAccount {
|
||||
request_id,
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerRequirement;
|
||||
use codex_config::RawMcpServerConfig;
|
||||
use codex_features::Features;
|
||||
use rmcp::ErrorData as McpError;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
@@ -139,6 +140,7 @@ impl DynamicToolMcpServer {
|
||||
pub(crate) async fn start(
|
||||
request_handle: AppServerRequestHandle,
|
||||
mut thread_start_params: ThreadStartParams,
|
||||
features: Features,
|
||||
app_event_tx: AppEventSender,
|
||||
status_updates: broadcast::Sender<ThreadStatusChangedNotification>,
|
||||
managed_requirement: Option<&McpServerRequirement>,
|
||||
@@ -180,6 +182,7 @@ impl DynamicToolMcpServer {
|
||||
let handler = DynamicToolMcpHandler {
|
||||
connection: Arc::clone(&connection),
|
||||
thread_start_params,
|
||||
features,
|
||||
status_updates,
|
||||
server_config: server_config.clone(),
|
||||
services,
|
||||
@@ -236,6 +239,7 @@ async fn require_authorization(
|
||||
struct DynamicToolMcpHandler {
|
||||
connection: ToolConnection,
|
||||
thread_start_params: ThreadStartParams,
|
||||
features: Features,
|
||||
status_updates: broadcast::Sender<ThreadStatusChangedNotification>,
|
||||
server_config: Value,
|
||||
services: ToolServices,
|
||||
@@ -370,6 +374,7 @@ impl ServerHandler for DynamicToolMcpHandler {
|
||||
request_handle,
|
||||
params,
|
||||
thread_start_params,
|
||||
self.features.clone(),
|
||||
self.status_updates.subscribe(),
|
||||
Some(&app_event_tx),
|
||||
)
|
||||
|
||||
@@ -125,6 +125,7 @@ async fn call_tool(
|
||||
ephemeral: Some(true),
|
||||
..ThreadStartParams::default()
|
||||
},
|
||||
Features::with_defaults(),
|
||||
status_receiver,
|
||||
/*app_event_tx*/ None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user