Enable Cyber access programs for API-key sessions by default (#53717)

## What changed

- Default `features.api_key_cyber_access_programs` to `true` and allow `codex exec` to select Daybreak with an OpenAI API key. Select the advertised standard program when `daybreak=false`, while respecting explicit feature opt-outs.
- Resolve API-key eligibility for TUI background turns from the target thread's feature settings, including project opt-outs. Omit access-program overrides when the server cannot report those settings.
- Preserve automatic server treatment for unlisted models and missing or empty Cyber program metadata in exec and TUI. Continue rejecting Daybreak when a nonempty program list does not offer it.

## Testing

Update regression coverage for API-key Daybreak selection, the off override, explicit opt-outs, resumed sessions, target-thread settings, and missing model metadata.

GitOrigin-RevId: f3cf7bd95a2754d4b4281bc70b18c7da4b177552
This commit is contained in:
Brian Lee
2026-10-13 16:57:54 +00:00
committed by copyberry
parent 7d0b91d5d6
commit 6cc7d9cc5c
11 changed files with 140 additions and 54 deletions
+27 -17
View File
@@ -9,9 +9,12 @@ use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::openai_models::ModelAccessPrograms;
pub(crate) async fn program_for_turn(
features: &Features,
client: &InProcessAppServerClient,
request_ids: &mut RequestIdSequencer,
model: &str,
@@ -25,8 +28,11 @@ pub(crate) async fn program_for_turn(
if enabled {
anyhow::ensure!(
has_chatgpt_account(client, request_ids).await?,
"Daybreak requires a signed-in ChatGPT account"
matches!(
read_account(client, request_ids).await?,
Some(Account::Chatgpt { .. } | Account::ApiKey { .. })
),
"Daybreak requires a signed-in ChatGPT account or an OpenAI API key"
);
}
@@ -43,44 +49,48 @@ pub(crate) async fn program_for_turn(
})
.await;
let Ok(result) = result else {
anyhow::ensure!(!enabled, "Daybreak availability could not be determined");
return Ok(None);
};
if let Some(selected) = result.data.into_iter().find(|entry| entry.model == model) {
let programs = selected
.available_access_programs
.map(ModelAccessPrograms::from);
.map(ModelAccessPrograms::from)
.filter(|programs| !programs.cyber.is_empty());
let Some(programs) = programs else {
return Ok(None);
};
if enabled {
return programs
.and_then(|programs| programs.daybreak())
.daybreak()
.map(|program| Some(program.into()))
.ok_or_else(|| anyhow::anyhow!("Daybreak is unavailable for model {model}; turn it off or choose a compatible model"));
}
let Some(standard) = programs.and_then(|programs| programs.standard()) else {
let Some(standard) = programs.standard() else {
return Ok(None);
};
return Ok(has_chatgpt_account(client, request_ids)
.await
.unwrap_or(false)
.then_some(standard.into()));
let eligible = match read_account(client, request_ids).await.unwrap_or(None) {
Some(Account::Chatgpt { .. }) => true,
Some(Account::ApiKey { .. }) => {
features.enabled(Feature::ApiKeyCyberAccessPrograms)
}
Some(Account::AmazonBedrock { .. }) | None => false,
};
return Ok(eligible.then_some(standard.into()));
}
match result.next_cursor {
Some(next) => cursor = Some(next),
None => {
anyhow::ensure!(
!enabled,
"Daybreak availability could not be determined for model {model}"
);
// Preserve automatic treatment for explicit model IDs absent from the catalog.
return Ok(None);
}
}
}
}
async fn has_chatgpt_account(
async fn read_account(
client: &InProcessAppServerClient,
request_ids: &mut RequestIdSequencer,
) -> anyhow::Result<bool> {
) -> anyhow::Result<Option<Account>> {
let account = client
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
request_id: request_ids.next(),
@@ -92,5 +102,5 @@ async fn has_chatgpt_account(
.map_err(|error| {
anyhow::anyhow!("Daybreak account availability could not be determined: {error}")
})?;
Ok(matches!(account.account, Some(Account::Chatgpt { .. })))
Ok(account.account)
}
+1
View File
@@ -1215,6 +1215,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
Some(program) => Some(program),
None if config.features.enabled(Feature::CliDaybreak) => {
daybreak::program_for_turn(
&config.features,
&client,
&mut request_ids,
&session_configured.model,
+37 -19
View File
@@ -21,34 +21,52 @@ fn started_thread(output: std::process::Output) -> anyhow::Result<String> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_rejects_daybreak_for_api_keys_and_allows_an_override() -> anyhow::Result<()> {
async fn exec_resolves_daybreak_for_api_keys_and_allows_an_off_override() -> anyhow::Result<()> {
let (test, server) = configured_daybreak_exec().await?;
std::fs::remove_file(test.home_path().join("auth.json"))?;
let response = responses::mount_sse_once(&server, responses::sse_completed("response")).await;
for (args, expected) in [
(vec![], Some("daybreak_blue")),
(vec!["-c", "daybreak=false"], Some("standard")),
(vec!["-m", "unlisted-model"], None),
(
vec![
"-c",
"daybreak=false",
"-c",
"features.api_key_cyber_access_programs=false",
],
None,
),
] {
let response =
responses::mount_sse_once(&server, responses::sse_completed("response")).await;
test.cmd_with_server(&server)
.env_remove("CODEX_ACCESS_TOKEN")
.arg("--skip-git-repo-check")
.args(args)
.arg("hello")
.assert()
.success();
assert_eq!(
response.single_request().body_json().get("access_programs"),
expected.map(|program| json!({"cyber": program})).as_ref()
);
}
let response = responses::mount_sse_once(&server, responses::sse_completed("unexpected")).await;
test.cmd_with_server(&server)
.env_remove("CODEX_ACCESS_TOKEN")
.arg("--skip-git-repo-check")
.arg("hello")
.args([
"--skip-git-repo-check",
"-c",
"features.api_key_cyber_access_programs=false",
"hello",
])
.assert()
.failure()
.stderr(predicate::str::contains(
"Daybreak requires a signed-in ChatGPT account",
"Cyber access programs are disabled for this API-key session.",
));
assert!(response.requests().is_empty());
test.cmd_with_server(&server)
.env_remove("CODEX_ACCESS_TOKEN")
.arg("--skip-git-repo-check")
.arg("-c")
.arg("daybreak=false")
.arg("hello")
.assert()
.success();
assert_eq!(
response.single_request().body_json().get("access_programs"),
None
);
Ok(())
}