feat(core): send attestation for ChatGPT compaction and realtime setup

This commit is contained in:
Jiaming Zhang
2026-04-30 07:01:13 -07:00
parent 2fa3e50ef8
commit 273e5d754e
5 changed files with 142 additions and 5 deletions

View File

@@ -60,6 +60,9 @@ jobs:
PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz"
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
- name: Test Darwin arm64 npm staging
run: python3 codex-cli/scripts/test_build_npm_package.py
- name: Upload staged npm package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Focused tests for Codex CLI npm package staging."""
from __future__ import annotations
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
BUILD_SCRIPT = SCRIPT_DIR / "build_npm_package.py"
APPLE_SILICON_TARGET = "aarch64-apple-darwin"
class BuildNpmPackageTests(unittest.TestCase):
def test_darwin_arm64_package_includes_devicecheck_probe(self) -> None:
with tempfile.TemporaryDirectory(prefix="codex-npm-test-") as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
vendor_src = tmp_dir / "vendor"
target_dir = vendor_src / APPLE_SILICON_TARGET
(target_dir / "codex").mkdir(parents=True)
(target_dir / "codex" / "codex").touch()
(target_dir / "devicecheck-probe" / "DeviceCheckProbe.app").mkdir(parents=True)
(target_dir / "path").mkdir(parents=True)
(target_dir / "path" / "rg").touch()
staging_dir = tmp_dir / "stage"
subprocess.run(
[
sys.executable,
str(BUILD_SCRIPT),
"--package",
"codex-darwin-arm64",
"--version",
"0.0.0-test",
"--staging-dir",
str(staging_dir),
"--vendor-src",
str(vendor_src),
],
check=True,
)
staged_target_dir = staging_dir / "vendor" / APPLE_SILICON_TARGET
self.assertTrue((staged_target_dir / "codex" / "codex").exists())
self.assertTrue(
(
staged_target_dir
/ "devicecheck-probe"
/ "DeviceCheckProbe.app"
).is_dir()
)
self.assertTrue((staged_target_dir / "path" / "rg").exists())
if __name__ == "__main__":
unittest.main()

View File

@@ -118,6 +118,7 @@ use codex_login::auth_env_telemetry::AuthEnvTelemetry;
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::CHATGPT_CODEX_BASE_URL;
#[cfg(test)]
use codex_model_provider_info::DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS;
use codex_model_provider_info::ModelProviderInfo;
@@ -424,6 +425,8 @@ impl ModelClient {
return Ok(Vec::new());
}
let client_setup = self.current_client_setup().await?;
let should_send_attestation =
should_send_attestation(&client_setup.api_provider, AttestationPurpose::Compaction);
let transport = ReqwestTransport::new(build_reqwest_client());
let request_telemetry = Self::build_request_telemetry(
session_telemetry,
@@ -477,6 +480,9 @@ impl ModelClient {
extra_headers.extend(build_conversation_headers(Some(
self.state.conversation_id.to_string(),
)));
if should_send_attestation && let Some(header_value) = macos_devicecheck_header() {
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
let trace_attempt = compaction_trace.start_attempt(&payload);
let result = client
.compact_input(&payload, extra_headers)
@@ -495,10 +501,18 @@ impl ModelClient {
// Create the media call over HTTP first, then retain matching auth so realtime can attach
// the server-side control WebSocket to the call id from that HTTP response.
let client_setup = self.current_client_setup().await?;
let should_send_attestation = should_send_attestation(
&client_setup.api_provider,
AttestationPurpose::RealtimeWebrtcCallSetup,
);
let mut sideband_headers = extra_headers.clone();
sideband_headers.extend(sideband_websocket_auth_headers(
client_setup.api_auth.as_ref(),
));
let mut extra_headers = extra_headers;
if should_send_attestation && let Some(header_value) = macos_devicecheck_header() {
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
let transport = ReqwestTransport::new(build_reqwest_client());
let response =
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
@@ -1627,18 +1641,32 @@ fn build_responses_headers(
headers
}
fn provider_base_url_is_chatgpt_codex(provider: &codex_api::Provider) -> bool {
provider
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AttestationPurpose {
Response,
Compaction,
RealtimeWebrtcCallSetup,
}
fn should_send_attestation(provider: &codex_api::Provider, purpose: AttestationPurpose) -> bool {
let provider_is_chatgpt_codex = provider
.base_url
.trim_end_matches('/')
.eq_ignore_ascii_case("https://chatgpt.com/backend-api/codex")
.eq_ignore_ascii_case(CHATGPT_CODEX_BASE_URL);
provider_is_chatgpt_codex
&& matches!(
purpose,
AttestationPurpose::Response
| AttestationPurpose::Compaction
| AttestationPurpose::RealtimeWebrtcCallSetup
)
}
fn extend_devicecheck_header_for_responses_endpoint(
headers: &mut ApiHeaderMap,
provider: &codex_api::Provider,
) {
if provider_base_url_is_chatgpt_codex(provider)
if should_send_attestation(provider, AttestationPurpose::Response)
&& let Some(header_value) = macos_devicecheck_header()
{
headers.insert(X_OAI_ATTESTATION_HEADER, header_value);

View File

@@ -1,3 +1,5 @@
use super::AttestationPurpose;
use super::AttestationPurpose;
use super::AuthRequestTelemetryContext;
use super::ModelClient;
use super::PendingUnauthorizedRetry;
@@ -8,7 +10,9 @@ use super::X_CODEX_TURN_METADATA_HEADER;
use super::X_CODEX_WINDOW_ID_HEADER;
use super::X_OPENAI_SUBAGENT_HEADER;
use codex_api::ApiError;
use codex_api::Provider as ApiProvider;
use codex_api::ResponseEvent;
use codex_api::RetryConfig as ApiRetryConfig;
use codex_app_server_protocol::AuthMode;
use codex_model_provider::BearerAuthProvider;
use codex_model_provider_info::WireApi;
@@ -383,3 +387,43 @@ fn auth_request_telemetry_context_tracks_attached_auth_and_retry_phase() {
assert_eq!(auth_context.recovery_mode, Some("managed"));
assert_eq!(auth_context.recovery_phase, Some("refresh_token"));
}
fn api_provider(base_url: &str) -> ApiProvider {
ApiProvider {
name: "test".to_string(),
base_url: base_url.to_string(),
query_params: None,
headers: http::HeaderMap::new(),
retry: ApiRetryConfig {
max_attempts: 1,
base_delay: Duration::from_millis(1),
retry_429: false,
retry_5xx: true,
retry_transport: true,
},
stream_idle_timeout: Duration::from_secs(1),
}
}
#[test]
fn should_send_attestation_for_allowed_chatgpt_codex_purposes() {
let provider = api_provider("https://chatgpt.com/backend-api/codex/");
for purpose in [
AttestationPurpose::Response,
AttestationPurpose::Compaction,
AttestationPurpose::RealtimeWebrtcCallSetup,
] {
assert!(super::should_send_attestation(&provider, purpose));
}
}
#[test]
fn should_not_send_attestation_for_non_chatgpt_codex_provider() {
let provider = api_provider("https://api.openai.com/v1");
assert!(!super::should_send_attestation(
&provider,
AttestationPurpose::Response,
));
}

View File

@@ -34,6 +34,7 @@ const MAX_REQUEST_MAX_RETRIES: u64 = 100;
const OPENAI_PROVIDER_NAME: &str = "OpenAI";
pub const OPENAI_PROVIDER_ID: &str = "openai";
pub const CHATGPT_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex";
const AMAZON_BEDROCK_PROVIDER_NAME: &str = "Amazon Bedrock";
pub const AMAZON_BEDROCK_PROVIDER_ID: &str = "amazon-bedrock";
pub const AMAZON_BEDROCK_DEFAULT_BASE_URL: &str =
@@ -234,7 +235,7 @@ impl ModelProviderInfo {
auth_mode,
Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::AgentIdentity)
) {
"https://chatgpt.com/backend-api/codex"
CHATGPT_CODEX_BASE_URL
} else {
"https://api.openai.com/v1"
};