mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
Support explicit cyber access programs with OpenAI API keys (#49406)
## What changed Forward explicit `cyberAccessProgram` selections for the built-in OpenAI provider when both `api_key_cyber_access_programs` and `api_key_model_discovery` are enabled. The new feature defaults to disabled and can be toggled through the app server's experimental feature enablement API. Reject explicit selections in OpenAI API-key sessions when either feature is disabled, instead of silently omitting the program. Leave entitlement and model restrictions to the server, and preserve the requested program when a server rejection triggers transport retries. ## Testing Add app-server coverage for both feature gates, forwarding all three program values, and server rejection without fallback. Update core coverage for API-key resumes with disabled features and custom-provider omission. GitOrigin-RevId: aa639afb841aa651b28f6f3f61198299372baa2d
This commit is contained in:
+1
-1
@@ -762,7 +762,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"CyberAccessProgram": {
|
||||
"description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"enum": [
|
||||
"standard",
|
||||
"daybreakBlue",
|
||||
|
||||
+1
-1
@@ -10712,7 +10712,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"CyberAccessProgram": {
|
||||
"description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"enum": [
|
||||
"standard",
|
||||
"daybreakBlue",
|
||||
|
||||
+1
-1
@@ -6568,7 +6568,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"CyberAccessProgram": {
|
||||
"description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"enum": [
|
||||
"standard",
|
||||
"daybreakBlue",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"CyberAccessProgram": {
|
||||
"description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"enum": [
|
||||
"standard",
|
||||
"daybreakBlue",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"CyberAccessProgram": {
|
||||
"description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.",
|
||||
"enum": [
|
||||
"standard",
|
||||
"daybreakBlue",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Requested cyber treatment for a ChatGPT-authenticated Codex turn.
|
||||
* Requested cyber treatment for an OpenAI model turn.
|
||||
* Authorization and model-tier restrictions remain server-owned.
|
||||
*/
|
||||
export type CyberAccessProgram = "standard" | "daybreakBlue" | "daybreakRed";
|
||||
|
||||
@@ -118,7 +118,7 @@ pub struct AdditionalContextEntry {
|
||||
pub kind: AdditionalContextKind,
|
||||
}
|
||||
|
||||
/// Requested cyber treatment for a ChatGPT-authenticated Codex turn.
|
||||
/// Requested cyber treatment for an OpenAI model turn.
|
||||
/// Authorization and model-tier restrictions remain server-owned.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -271,7 +271,7 @@ pub struct TurnStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
|
||||
/// EXPERIMENTAL - Request a workspace-authorized cyber program for this
|
||||
/// EXPERIMENTAL - Request an authorized cyber program for this
|
||||
/// turn. Omission preserves automatic behavior. This does not grant access.
|
||||
#[experimental("turn/start.cyberAccessProgram")]
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -64,6 +64,7 @@ const BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE: &str =
|
||||
"background_paginated_rollout_migration";
|
||||
|
||||
const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[
|
||||
"api_key_cyber_access_programs",
|
||||
"api_key_model_discovery",
|
||||
"auth_elicitation",
|
||||
BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE,
|
||||
|
||||
@@ -3,7 +3,10 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CyberAccessProgram;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
@@ -16,13 +19,18 @@ use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::login_with_api_key;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
@@ -192,6 +200,167 @@ async fn turn_start_forwards_explicit_cyber_access_program() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_key_cyber_access_program_requires_both_features() -> Result<()> {
|
||||
core_test_support::skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let expected_programs = [
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(json!({"cyber": "daybreak_blue"})),
|
||||
Some(json!({"cyber": "daybreak_red"})),
|
||||
Some(json!({"cyber": "standard"})),
|
||||
];
|
||||
let requests = responses::mount_sse_sequence(
|
||||
&server,
|
||||
expected_programs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _)| responses::sse_completed(&format!("resp-{index}")))
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
let home = TempDir::new()?;
|
||||
let mut app = start_api_key_app(home.path(), &server).await?;
|
||||
set_api_key_cyber_access_programs(&mut app, /*enabled*/ true).await?;
|
||||
for disabled_feature in [
|
||||
Some("api_key_cyber_access_programs"),
|
||||
Some("api_key_model_discovery"),
|
||||
None,
|
||||
] {
|
||||
let thread = app
|
||||
.start_thread(ThreadStartParams {
|
||||
config: disabled_feature
|
||||
.map(|feature| HashMap::from([(format!("features.{feature}"), json!(false))])),
|
||||
..Default::default()
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
for program in [
|
||||
None,
|
||||
Some(CyberAccessProgram::DaybreakBlue),
|
||||
Some(CyberAccessProgram::DaybreakRed),
|
||||
Some(CyberAccessProgram::Standard),
|
||||
] {
|
||||
let completed = app
|
||||
.start_turn_and_wait_for_completion(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
cyber_access_program: program,
|
||||
input: vec![UserInput::Text {
|
||||
text: "hello".to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
if program.is_some() && disabled_feature.is_some() {
|
||||
assert_eq!(completed.turn.status, TurnStatus::Failed);
|
||||
assert_eq!(
|
||||
completed.turn.error,
|
||||
Some(TurnError {
|
||||
message: "Cyber access programs are disabled for this API-key session."
|
||||
.to_owned(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
misalignment: None,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
requests
|
||||
.requests()
|
||||
.iter()
|
||||
.map(|request| (
|
||||
request.header("authorization"),
|
||||
request.body_json()["model"].clone(),
|
||||
request.body_json().get("access_programs").cloned(),
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_programs
|
||||
.into_iter()
|
||||
.map(|program| (
|
||||
Some("Bearer test-key".to_owned()),
|
||||
json!("gpt-6-sol"),
|
||||
program
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
app.shutdown_gracefully().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_key_cyber_access_program_server_rejection_does_not_fall_back() -> Result<()> {
|
||||
core_test_support::skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 403).set_body_json(json!({
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"code": "access_program_not_enabled",
|
||||
"message": "Daybreak Blue is not enabled for this project."
|
||||
}
|
||||
})))
|
||||
.expect(/*r*/ 1..)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let home = TempDir::new()?;
|
||||
let mut app = start_api_key_app(home.path(), &server).await?;
|
||||
set_api_key_cyber_access_programs(&mut app, /*enabled*/ true).await?;
|
||||
let thread = app.start_thread(ThreadStartParams::default()).await?.thread;
|
||||
let completed = app
|
||||
.start_turn_and_wait_for_completion(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
cyber_access_program: Some(CyberAccessProgram::DaybreakBlue),
|
||||
input: vec![UserInput::Text {
|
||||
text: "hello".to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(completed.turn.status, TurnStatus::Failed);
|
||||
let error = completed.turn.error.expect("server rejection");
|
||||
assert_eq!(
|
||||
error.codex_error_info,
|
||||
Some(CodexErrorInfo::HttpConnectionFailed {
|
||||
http_status_code: Some(403),
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
error
|
||||
.message
|
||||
.contains("Daybreak Blue is not enabled for this project.")
|
||||
);
|
||||
let requests = responses::received_responses_requests(&server).await;
|
||||
assert!(!requests.is_empty());
|
||||
// Existing transport retries may repeat the request; none may drop the program.
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| (
|
||||
request.header("authorization"),
|
||||
request.body_json()["access_programs"].clone()
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(
|
||||
Some("Bearer test-key".to_owned()),
|
||||
json!({"cyber": "daybreak_blue"})
|
||||
);
|
||||
requests.len()
|
||||
]
|
||||
);
|
||||
app.shutdown_gracefully().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn daybreak_thread_metadata_persists_independently_across_restart_and_fork() -> Result<()> {
|
||||
core_test_support::skip_if_no_network!(Ok(()));
|
||||
@@ -461,3 +630,55 @@ async fn start_chatgpt_app(home: &Path, server: &MockServer) -> Result<TestAppSe
|
||||
.build_initialized()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_api_key_app(home: &Path, server: &MockServer) -> Result<TestAppServer> {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 426))
|
||||
.mount(server)
|
||||
.await;
|
||||
// The existing inference override disables remote catalog discovery. Forwarding
|
||||
// must therefore work without granting programs through a test catalog.
|
||||
std::fs::write(
|
||||
home.join("config.toml"),
|
||||
format!(
|
||||
"model = \"gpt-6-sol\"\nopenai_base_url = \"{}/v1\"\n[features]\napi_key_model_discovery = true\n",
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
login_with_api_key(
|
||||
home,
|
||||
"test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
TestAppServer::builder()
|
||||
.with_codex_home(home)
|
||||
.without_managed_config()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
("CODEX_API_KEY", None),
|
||||
("OPENAI_BASE_URL", None),
|
||||
])
|
||||
.build_initialized()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_api_key_cyber_access_programs(app: &mut TestAppServer, enabled: bool) -> Result<()> {
|
||||
let enablement = BTreeMap::from([("api_key_cyber_access_programs".to_owned(), enabled)]);
|
||||
let response: ExperimentalFeatureEnablementSetResponse = app
|
||||
.request(
|
||||
|request_id| ClientRequest::ExperimentalFeatureEnablementSet {
|
||||
request_id,
|
||||
params: ExperimentalFeatureEnablementSetParams {
|
||||
enablement: enablement.clone(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
response,
|
||||
ExperimentalFeatureEnablementSetResponse { enablement }
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -713,6 +713,9 @@
|
||||
"analytics_plan_history": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"api_key_cyber_access_programs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"api_key_model_discovery": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6740,6 +6743,9 @@
|
||||
"analytics_plan_history": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"api_key_cyber_access_programs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"api_key_model_discovery": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -272,6 +272,8 @@ pub struct ModelClient {
|
||||
restored_history: bool,
|
||||
request_contributors: Vec<Arc<dyn codex_extension_api::ModelRequestContributor>>,
|
||||
executed_tool_calls: Option<ExecutedToolCalls>,
|
||||
// Resolved once when the session is created, like other session feature flags.
|
||||
api_key_cyber_access_programs: cyber_access_program::ApiKeyCyberAccessPrograms,
|
||||
}
|
||||
|
||||
/// A turn-scoped streaming session created from a [`ModelClient`].
|
||||
@@ -541,6 +543,8 @@ impl ModelClient {
|
||||
restored_history: false,
|
||||
request_contributors,
|
||||
executed_tool_calls: None,
|
||||
api_key_cyber_access_programs:
|
||||
cyber_access_program::ApiKeyCyberAccessPrograms::UnsupportedProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,10 +569,12 @@ impl ModelClient {
|
||||
prompt_cache_key_override: Option<String>,
|
||||
event_sender: Sender<ProtocolEvent>,
|
||||
codex_responses_headers: Option<Arc<CodexResponsesHeaders>>,
|
||||
api_key_cyber_access_programs: cyber_access_program::ApiKeyCyberAccessPrograms,
|
||||
) -> Self {
|
||||
self.prompt_cache_key_override = prompt_cache_key_override;
|
||||
self.event_sender = Some(event_sender);
|
||||
self.codex_responses_headers = codex_responses_headers;
|
||||
self.api_key_cyber_access_programs = api_key_cyber_access_programs;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1730,7 +1736,8 @@ impl ModelClientSession {
|
||||
request.access_programs = cyber_access_program::for_auth(
|
||||
client_setup.auth.as_ref(),
|
||||
prompt.cyber_access_program,
|
||||
);
|
||||
self.client.api_key_cyber_access_programs,
|
||||
)?;
|
||||
self.client
|
||||
.prepare_response_items_for_request(&mut request.input);
|
||||
if crate::guardian::is_basic_session_source(&self.client.state.session_source) {
|
||||
@@ -1889,7 +1896,8 @@ impl ModelClientSession {
|
||||
request.access_programs = cyber_access_program::for_auth(
|
||||
client_setup.auth.as_ref(),
|
||||
prompt.cyber_access_program,
|
||||
);
|
||||
self.client.api_key_cyber_access_programs,
|
||||
)?;
|
||||
let mut websocket_metadata = responses_metadata.clone();
|
||||
websocket_metadata.routing_hint = self.client.build_routing_hint_header(
|
||||
client_setup.auth.as_ref(),
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
//! Forward explicit programs while leaving entitlement and model policy to the server.
|
||||
|
||||
use codex_api::AccessPrograms;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::turn_input::CyberAccessProgram;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum ApiKeyCyberAccessPrograms {
|
||||
UnsupportedProvider,
|
||||
Disabled,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl ApiKeyCyberAccessPrograms {
|
||||
pub(crate) fn from_config(config: &crate::config::Config) -> Self {
|
||||
if config.model_provider_id != codex_model_provider_info::OPENAI_PROVIDER_ID {
|
||||
Self::UnsupportedProvider
|
||||
} else if config.features.enabled(Feature::ApiKeyCyberAccessPrograms)
|
||||
&& config.features.enabled(Feature::ApiKeyModelDiscovery)
|
||||
{
|
||||
Self::Enabled
|
||||
} else {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
program: Option<CyberAccessProgram>,
|
||||
) -> Option<AccessPrograms> {
|
||||
program
|
||||
.filter(|_| auth.is_some_and(CodexAuth::is_chatgpt_auth))
|
||||
.map(AccessPrograms::from)
|
||||
policy: ApiKeyCyberAccessPrograms,
|
||||
) -> Result<Option<AccessPrograms>> {
|
||||
let Some(program) = program else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(auth) = auth else {
|
||||
return Ok(None);
|
||||
};
|
||||
if auth.is_chatgpt_auth() {
|
||||
return Ok(Some(program.into()));
|
||||
}
|
||||
if !auth.is_api_key_auth() {
|
||||
return Ok(None);
|
||||
}
|
||||
match policy {
|
||||
ApiKeyCyberAccessPrograms::UnsupportedProvider => Ok(None),
|
||||
ApiKeyCyberAccessPrograms::Disabled => Err(CodexErr::InvalidRequest(
|
||||
"Cyber access programs are disabled for this API-key session.".to_owned(),
|
||||
)),
|
||||
ApiKeyCyberAccessPrograms::Enabled => Ok(Some(program.into())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1763,6 +1763,7 @@ impl Session {
|
||||
.or(fork_cache_key),
|
||||
tx_event.clone(),
|
||||
codex_responses_headers,
|
||||
crate::cyber_access_program::ApiKeyCyberAccessPrograms::from_config(&config),
|
||||
),
|
||||
executed_tool_calls: executed_tool_calls.clone(),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
|
||||
@@ -534,6 +534,8 @@ async fn recorder_refreshes_without_changing_execution_features_or_claiming_miss
|
||||
crate::ConfigRefreshOutcome::Published
|
||||
);
|
||||
let mut replay = output(&call.call_id);
|
||||
// Replay the same history item across the configuration refresh.
|
||||
replay.set_id(direct_output.id().cloned());
|
||||
calls.attach_direct_call_to_output(&mut replay, prepared);
|
||||
assert_eq!(replay, direct_output);
|
||||
}
|
||||
|
||||
@@ -254,6 +254,30 @@ async fn model_switch_program_pair(
|
||||
Arc::clone(&resumed.codex)
|
||||
}
|
||||
};
|
||||
if matches!(history, History::ApiKeyResume) {
|
||||
// Resuming a ChatGPT turn does not enable the API-key rollout features.
|
||||
let error = submit_pair(&thread, next_model, Some(CyberAccessProgram::DaybreakRed))
|
||||
.await
|
||||
.expect_err("API-key selections require the rollout features");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("Cyber access programs are disabled for this API-key session.")
|
||||
);
|
||||
thread.shutdown_and_wait().await?;
|
||||
assert_eq!(
|
||||
server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("recorded fixture requests")
|
||||
.iter()
|
||||
.filter(|request| request.method == "POST" && request.url.path() == "/v1/responses")
|
||||
.count(),
|
||||
1,
|
||||
"only the original ChatGPT turn should reach inference"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let summary = if local {
|
||||
ev_assistant_message("summary", "summary of surviving history")
|
||||
} else {
|
||||
@@ -277,17 +301,7 @@ async fn model_switch_program_pair(
|
||||
submit_pair(&thread, next_model, Some(CyberAccessProgram::DaybreakRed)).await?;
|
||||
thread.shutdown_and_wait().await?;
|
||||
let requests = requests.requests();
|
||||
let authorized = !matches!(history, History::ApiKeyResume);
|
||||
let old_program = if authorized {
|
||||
expected_program
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
let new_program = if authorized {
|
||||
json!({"cyber": "daybreak_red"})
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
let new_program = json!({"cyber": "daybreak_red"});
|
||||
let actual = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
@@ -302,7 +316,7 @@ async fn model_switch_program_pair(
|
||||
}
|
||||
following.push(json!([next_model, new_program]));
|
||||
assert_eq!(&actual[1..], following.as_slice());
|
||||
assert_eq!(actual[0], json!([previous_model, old_program]));
|
||||
assert_eq!(actual[0], json!([previous_model, expected_program]));
|
||||
if !local {
|
||||
assert_eq!(requests[0].inputs_of_type("compaction_trigger").len(), 1);
|
||||
}
|
||||
|
||||
@@ -159,10 +159,10 @@ async fn recover_turn_restores_cyber_access_program_without_making_it_sticky() -
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn cyber_access_program_omits_api_key_and_spoofed_custom_provider() -> Result<()> {
|
||||
async fn cyber_access_program_omits_spoofed_custom_providers() -> Result<()> {
|
||||
core_test_support::skip_if_no_network!(Ok(()));
|
||||
for (auth, provider_id) in [
|
||||
(CodexAuth::from_api_key("test-key"), "openai"),
|
||||
(CodexAuth::from_api_key("test-key"), "custom"),
|
||||
(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "custom"),
|
||||
] {
|
||||
let server = responses::start_mock_server().await;
|
||||
|
||||
@@ -96,6 +96,8 @@ pub enum Feature {
|
||||
AnalyticsPlanHistory,
|
||||
/// Discover model catalogs for OpenAI API-key authentication.
|
||||
ApiKeyModelDiscovery,
|
||||
/// Forward explicit programs with builtin OpenAI API keys.
|
||||
ApiKeyCyberAccessPrograms,
|
||||
/// Deprecated no-op; use `tui.fullscreen_transcript` instead.
|
||||
TranscriptV2,
|
||||
// Stable.
|
||||
@@ -1300,6 +1302,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ApiKeyCyberAccessPrograms,
|
||||
key: "api_key_cyber_access_programs",
|
||||
stage: Stage::Stable,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::EnableRequestCompression,
|
||||
key: "enable_request_compression",
|
||||
|
||||
@@ -142,7 +142,7 @@ pub enum TurnInputMode {
|
||||
Steer { expected_turn_id: String },
|
||||
}
|
||||
|
||||
/// Requested cyber treatment for a ChatGPT-authenticated Codex turn.
|
||||
/// Requested cyber treatment for an OpenAI model turn.
|
||||
/// Authorization and model-tier restrictions remain server-owned.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
Reference in New Issue
Block a user