From 273e5d754edaac62b4bc34032c99f7911bc1b97d Mon Sep 17 00:00:00 2001 From: Jiaming Zhang Date: Thu, 30 Apr 2026 07:01:13 -0700 Subject: [PATCH] feat(core): send attestation for ChatGPT compaction and realtime setup --- .github/workflows/ci.yml | 3 + codex-cli/scripts/test_build_npm_package.py | 61 +++++++++++++++++++++ codex-rs/core/src/client.rs | 36 ++++++++++-- codex-rs/core/src/client_tests.rs | 44 +++++++++++++++ codex-rs/model-provider-info/src/lib.rs | 3 +- 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 codex-cli/scripts/test_build_npm_package.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb5ed5b5bf..3e26bb1706 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/codex-cli/scripts/test_build_npm_package.py b/codex-cli/scripts/test_build_npm_package.py new file mode 100644 index 0000000000..282b87aad3 --- /dev/null +++ b/codex-cli/scripts/test_build_npm_package.py @@ -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() diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index a5aca79453..f1b4b6c79e 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -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); diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index e56500ba5f..a42c8f58f9 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -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, + )); +} diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index 0fb8be4746..6fca7e6a1f 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -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" };