diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index c744eb3f6e..447e386a83 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -124,7 +124,7 @@ use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput; use codex_login::default_client::DEFAULT_ORIGINATOR; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; @@ -965,7 +965,7 @@ fn app_mentioned_event_serializes_expected_shape() { "thread_id": "thread-1", "turn_id": "turn-1", "app_name": "Calendar", - "product_client_id": originator().value, + "product_client_id": Originator::process_default().value().to_string(), "invoke_type": "explicit", "model_slug": "gpt-5" } @@ -1003,7 +1003,7 @@ fn app_used_event_serializes_expected_shape() { "thread_id": "thread-2", "turn_id": "turn-2", "app_name": "Google Drive", - "product_client_id": originator().value, + "product_client_id": Originator::process_default().value().to_string(), "invoke_type": "implicit", "model_slug": "gpt-5" } @@ -2774,7 +2774,7 @@ fn plugin_used_event_serializes_expected_shape() { "has_skills": true, "mcp_server_count": 2, "connector_ids": ["calendar", "drive"], - "product_client_id": originator().value, + "product_client_id": Originator::process_default().value().to_string(), "thread_id": "thread-3", "turn_id": "turn-3", "model_slug": "gpt-5" @@ -2803,7 +2803,7 @@ fn plugin_management_event_serializes_expected_shape() { "has_skills": true, "mcp_server_count": 2, "connector_ids": ["calendar", "drive"], - "product_client_id": originator().value + "product_client_id": Originator::process_default().value().to_string() } }) ); @@ -3009,7 +3009,7 @@ async fn reducer_ingests_skill_invoked_fact() { "skill_id": expected_skill_id, "skill_name": "doc", "event_params": { - "product_client_id": originator().value, + "product_client_id": Originator::process_default().value().to_string(), "skill_scope": "user", "plugin_id": null, "repo_url": null, @@ -3170,7 +3170,7 @@ async fn reducer_ingests_plugin_state_changed_fact() { "has_skills": true, "mcp_server_count": 2, "connector_ids": ["calendar", "drive"], - "product_client_id": originator().value + "product_client_id": Originator::process_default().value().to_string() } }]) ); diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index fbcfa32dc5..b187d3e9ed 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -31,6 +31,7 @@ use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerResponse; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::create_client; use codex_plugin::PluginTelemetryMetadata; use codex_protocol::request_permissions::RequestPermissionsResponse; @@ -440,7 +441,8 @@ async fn send_track_events_request(auth: &CodexAuth, url: &str, events: Vec CodexPlu .map(|connector_id| connector_id.0) .collect() }), - product_client_id: Some(originator().value), + product_client_id: Some(Originator::process_default().value().to_string()), } } diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index d072720d10..4bd28fa8ba 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -111,7 +111,7 @@ use codex_app_server_protocol::UserInput; use codex_app_server_protocol::WebSearchAction; use codex_git_utils::collect_git_info; use codex_git_utils::get_git_repo_root; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; @@ -680,7 +680,7 @@ impl AnalyticsReducer { turn_id: Some(tracking.turn_id.clone()), invoke_type: Some(invocation.invocation_type), model_slug: Some(tracking.model_slug.clone()), - product_client_id: Some(originator().value), + product_client_id: Some(Originator::process_default().value().to_string()), repo_url, skill_scope: Some(skill_scope.to_string()), plugin_id: invocation.plugin_id, diff --git a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs index 9ec6e4d004..720ae9652d 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs @@ -3,6 +3,7 @@ use super::protocol::EnrollRemoteServerResponse; use super::protocol::RemoteControlTarget; use axum::http::HeaderMap; use codex_api::SharedAuthProvider; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_state::RemoteControlEnrollmentRecord; use codex_state::StateRuntime; @@ -204,7 +205,8 @@ pub(super) async fn enroll_remote_control_server( app_server_version: env!("CARGO_PKG_VERSION"), installation_id: installation_id.to_string(), }; - let client = build_reqwest_client(); + let originator = Originator::process_default(); + let client = build_reqwest_client(&originator); let mut auth_headers = HeaderMap::new(); auth.auth_provider.add_auth_headers(&mut auth_headers); let http_request = client diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index a40007db11..c3ab130c70 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -1,20 +1,15 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use axum::http::HeaderValue; use codex_analytics::AppServerRpcTransport; -use codex_login::default_client::SetOriginatorError; -use codex_login::default_client::USER_AGENT_SUFFIX; +use codex_login::default_client::Originator; use codex_login::default_client::get_codex_user_agent; use codex_login::default_client::set_default_client_residency_requirement; -use codex_login::default_client::set_default_originator; use super::*; use crate::message_processor::ConnectionSessionState; use crate::message_processor::InitializedConnectionSessionState; -const NON_ORIGINATING_CLIENT_NAMES: &[&str] = &["codex_app_server_daemon", "codex-backend"]; - #[derive(Clone)] pub(crate) struct InitializeRequestProcessor { outgoing: Arc, @@ -83,23 +78,19 @@ impl InitializeRequestProcessor { title: _title, version, } = params.client_info; - // Validate before committing; set_default_originator validates while - // mutating process-global metadata. - if HeaderValue::from_str(&name).is_err() { - return Err(invalid_request(format!( - "Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value." - ))); - } - let originator = name.clone(); - let user_agent_suffix = format!("{name}; {version}"); - let mutates_global_identity = !NON_ORIGINATING_CLIENT_NAMES.contains(&name.as_str()); + let originator = Originator::from_app_server_client(name.clone(), version.clone()) + .map_err(|_| { + invalid_request(format!( + "Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value." + )) + })?; let codex_home = self.config.codex_home.clone(); if session .initialize(InitializedConnectionSessionState { experimental_api_enabled, opted_out_notification_methods: opt_out_notification_methods.into_iter().collect(), app_server_client_name: name.clone(), - client_version: version, + client_version: version.clone(), request_attestation, }) .is_err() @@ -107,37 +98,15 @@ impl InitializeRequestProcessor { return Err(invalid_request("Already initialized")); } - if mutates_global_identity { - // Only real client initialization may mutate process-global client metadata. - if let Err(error) = set_default_originator(originator.clone()) { - match error { - SetOriginatorError::InvalidHeaderValue => { - tracing::warn!( - client_info_name = %name, - "validated clientInfo.name was rejected while setting originator" - ); - } - SetOriginatorError::AlreadyInitialized => { - // No-op. This is expected to happen if the originator is already set via env var. - // TODO(owen): Once we remove support for CODEX_INTERNAL_ORIGINATOR_OVERRIDE, - // this will be an unexpected state and we can return a JSON-RPC error indicating - // internal server error. - } - } - } - } self.analytics_events_client.track_initialize( connection_id.0, analytics_initialize_params, - originator, + name, self.rpc_transport, ); set_default_client_residency_requirement(self.config.enforce_residency.value()); - if mutates_global_identity && let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() { - *suffix = Some(user_agent_suffix); - } - let user_agent = get_codex_user_agent(); + let user_agent = get_codex_user_agent(&originator); let response = InitializeResponse { user_agent, codex_home, diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 47cacdb20e..620b3f36d5 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -63,7 +63,7 @@ async fn initialize_uses_client_info_name_as_originator() -> Result<()> { } #[tokio::test] -async fn initialize_probe_does_not_override_originator() -> Result<()> { +async fn initialize_probe_uses_connection_scoped_originator() -> Result<()> { let responses = Vec::new(); let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; @@ -85,12 +85,12 @@ async fn initialize_probe_does_not_override_originator() -> Result<()> { }; let InitializeResponse { user_agent, .. } = to_response::(response)?; - assert!(user_agent.starts_with("codex_cli_rs/")); + assert!(user_agent.starts_with("codex_app_server_daemon/")); Ok(()) } #[tokio::test] -async fn initialize_codex_backend_does_not_override_originator() -> Result<()> { +async fn initialize_codex_backend_uses_connection_scoped_originator() -> Result<()> { let responses = Vec::new(); let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; @@ -112,12 +112,12 @@ async fn initialize_codex_backend_does_not_override_originator() -> Result<()> { }; let InitializeResponse { user_agent, .. } = to_response::(response)?; - assert!(user_agent.starts_with("codex_cli_rs/")); + assert!(user_agent.starts_with("codex-backend/")); Ok(()) } #[tokio::test] -async fn initialize_respects_originator_override_env_var() -> Result<()> { +async fn initialize_ignores_process_originator_override_env_var() -> Result<()> { let responses = Vec::new(); let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; @@ -152,7 +152,7 @@ async fn initialize_respects_originator_override_env_var() -> Result<()> { platform_os, } = to_response::(response)?; - assert!(user_agent.starts_with("codex_originator_via_env_var/")); + assert!(user_agent.starts_with("codex_vscode/")); assert_eq!(response_codex_home, expected_codex_home); assert_eq!(platform_family, std::env::consts::FAMILY); assert_eq!(platform_os, std::env::consts::OS); diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 6365d527ed..ff0a379f1f 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -9,6 +9,7 @@ use codex_api::SharedAuthProvider; use codex_client::build_reqwest_client_with_custom_ca; use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::get_codex_user_agent; use codex_protocol::account::PlanType as AccountPlanType; use codex_protocol::protocol::CreditsSnapshot; @@ -170,8 +171,9 @@ impl Client { } pub fn from_auth(base_url: impl Into, auth: &CodexAuth) -> Result { + let originator = Originator::process_default(); Ok(Self::new(base_url)? - .with_user_agent(get_codex_user_agent()) + .with_user_agent(get_codex_user_agent(&originator)) .with_auth_provider(codex_model_provider::auth_provider_from_auth(auth))) } diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index 372f62e696..099f33e183 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -1,5 +1,6 @@ use codex_core::config::Config; use codex_login::AuthManager; +use codex_login::default_client::Originator; use codex_login::default_client::create_client; use anyhow::Context; @@ -39,7 +40,8 @@ pub(crate) async fn chatgpt_get_request_with_timeout( ); // Make direct HTTP request to ChatGPT backend API with the token - let client = create_client(); + let originator = Originator::process_default(); + let client = create_client(&originator); let url = format!( "{}/{}", chatgpt_base_url.trim_end_matches('/'), diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 2e54192e85..26102bc968 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -20,7 +20,7 @@ pub use codex_core::connectors::with_app_enabled_state; use codex_core_plugins::PluginsManager; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_plugin::AppConnectorId; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -85,10 +85,8 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option> .into_iter() .map(|connector_id| connector_id.0), ); - Some(filter_disallowed_connectors( - connectors, - originator().value.as_str(), - )) + let originator = Originator::process_default(); + Some(filter_disallowed_connectors(connectors, originator.value())) } pub async fn list_all_connectors_with_options( @@ -121,10 +119,8 @@ pub async fn list_all_connectors_with_options( .into_iter() .map(|connector_id| connector_id.0), ); - Ok(filter_disallowed_connectors( - connectors, - originator().value.as_str(), - )) + let originator = Originator::process_default(); + Ok(filter_disallowed_connectors(connectors, originator.value())) } fn connector_directory_cache_context( @@ -165,7 +161,8 @@ pub fn connectors_for_plugin_apps( .iter() .map(|connector_id| connector_id.0.clone()), ); - filter_disallowed_connectors(connectors, originator().value.as_str()) + let originator = Originator::process_default(); + filter_disallowed_connectors(connectors, originator.value()) .into_iter() .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) .collect() @@ -189,7 +186,8 @@ pub fn merge_connectors_with_accessible( accessible_connectors }; let merged = merge_connectors(connectors, accessible_connectors); - filter_disallowed_connectors(merged, originator().value.as_str()) + let originator = Originator::process_default(); + filter_disallowed_connectors(merged, originator.value()) } #[cfg(test)] diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index aec57c1e82..9b676ec655 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -47,6 +47,7 @@ use codex_login::CODEX_ACCESS_TOKEN_ENV_VAR; use codex_login::CODEX_API_KEY_ENV_VAR; use codex_login::CodexAuth; use codex_login::OPENAI_API_KEY_ENV_VAR; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_login::default_client::default_headers; use codex_login::load_auth_dot_json; @@ -2164,12 +2165,13 @@ async fn websocket_reachability_check( OPENAI_BETA_HEADER, HeaderValue::from_static(RESPONSES_WEBSOCKETS_V2_BETA_HEADER_VALUE), ); + let originator = Originator::process_default(); let client = ResponsesWebsocketClient::new(api_provider, api_auth); match tokio::time::timeout( provider.websocket_connect_timeout(), client.probe_handshake( extra_headers, - default_headers(), + default_headers(&originator), WEBSOCKET_IMMEDIATE_CLOSE_GRACE, ), ) @@ -2657,7 +2659,8 @@ async fn mcp_http_probe_url_with_timeout(url: &str, timeout: Duration) -> Result } async fn http_probe_url_with_timeout(url: &str, timeout: Duration) -> Result { - let response = build_reqwest_client() + let originator = Originator::process_default(); + let response = build_reqwest_client(&originator) .head(url) .timeout(timeout) .send() @@ -2683,7 +2686,8 @@ async fn http_get_probe_url_with_timeout(url: &str, timeout: Duration) -> Result } async fn http_get_probe_status_with_timeout(url: &str, timeout: Duration) -> Result { - let response = build_reqwest_client() + let originator = Originator::process_default(); + let response = build_reqwest_client(&originator) .get(url) .timeout(timeout) .send() diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index e8d6b545b5..18ab662705 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -12,6 +12,7 @@ use chrono::Utc; use codex_cloud_tasks_client::TaskStatus; use codex_git_utils::current_branch_name; use codex_git_utils::default_branch_name; +use codex_login::default_client::Originator; use codex_login::default_client::get_codex_user_agent; use owo_colors::OwoColorize; use owo_colors::Stream; @@ -28,7 +29,6 @@ use tracing::info; use tracing_subscriber::EnvFilter; use util::append_error_log; use util::format_relative_time; -use util::set_user_agent_suffix; struct ApplyJob { task_id: codex_cloud_tasks_client::TaskId, @@ -49,8 +49,6 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result let base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL") .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()); - set_user_agent_suffix(user_agent_suffix); - #[cfg(debug_assertions)] if use_mock { return Ok(BackendContext { @@ -59,7 +57,9 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result }); } - let ua = get_codex_user_agent(); + let originator = Originator::for_process(user_agent_suffix.to_string()) + .expect("cloud task originator should be a valid header value"); + let ua = get_codex_user_agent(&originator); let mut http = codex_cloud_tasks_client::HttpClient::new(base_url.clone())?.with_user_agent(ua); let style = if base_url.contains("/backend-api") { "wham" @@ -799,7 +799,10 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an append_error_log(format!( "startup: wham_force_internal={} ua={}", force_internal, - get_codex_user_agent() + get_codex_user_agent( + &Originator::for_process("codex_cloud_tasks_tui".to_string()) + .expect("codex_cloud_tasks_tui should be a valid originator header value") + ) )); // Non-blocking initial load so the in-box spinner can animate app.status = "Loading tasks…".to_string(); diff --git a/codex-rs/cloud-tasks/src/util.rs b/codex-rs/cloud-tasks/src/util.rs index 9a5056aa66..fedb12e31b 100644 --- a/codex-rs/cloud-tasks/src/util.rs +++ b/codex-rs/cloud-tasks/src/util.rs @@ -5,12 +5,7 @@ use reqwest::header::HeaderMap; use codex_core::config::Config; use codex_login::AuthManager; - -pub fn set_user_agent_suffix(suffix: &str) { - if let Ok(mut guard) = codex_login::default_client::USER_AGENT_SUFFIX.lock() { - guard.replace(suffix.to_string()); - } -} +use codex_login::default_client::Originator; pub fn append_error_log(message: impl AsRef) { let ts = Utc::now().to_rfc3339(); @@ -61,8 +56,9 @@ pub async fn build_chatgpt_headers() -> HeaderMap { use reqwest::header::HeaderValue; use reqwest::header::USER_AGENT; - set_user_agent_suffix("codex_cloud_tasks_tui"); - let ua = codex_login::default_client::get_codex_user_agent(); + let originator = Originator::for_process("codex_cloud_tasks_tui".to_string()) + .expect("codex_cloud_tasks_tui should be a valid originator header value"); + let ua = codex_login::default_client::get_codex_user_agent(&originator); let mut headers = HeaderMap::new(); headers.insert( USER_AGENT, diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index e87ee82f30..38c112bb05 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -50,7 +50,7 @@ pub use codex_extension_api::empty_extension_registry; pub use codex_features::Feature; pub use codex_features::Features; pub use codex_login::AuthManager; -pub use codex_login::default_client::set_default_originator; +pub use codex_login::default_client::Originator; pub use codex_model_provider_info::OPENAI_PROVIDER_ID; pub use codex_model_provider_info::built_in_model_providers; pub use codex_models_manager::manager::RefreshStrategy; diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 56427473a1..17943e4e8c 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -7,6 +7,7 @@ use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginInterface; use codex_app_server_protocol::SkillInterface; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_plugin::PluginId; use codex_utils_absolute_path::AbsolutePathBuf; @@ -23,6 +24,11 @@ use url::Url; mod remote_installed_plugin_sync; mod share; +fn build_process_reqwest_client() -> reqwest::Client { + let originator = Originator::process_default(); + build_reqwest_client(&originator) +} + pub use remote_installed_plugin_sync::RemoteInstalledPluginBundleSyncError; pub use remote_installed_plugin_sync::RemoteInstalledPluginBundleSyncOutcome; pub use remote_installed_plugin_sync::RemotePluginCacheMutationGuard; @@ -782,7 +788,7 @@ pub async fn fetch_remote_plugin_skill_detail( } let url = remote_plugin_skill_detail_url(config, plugin_id, skill_name)?; - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.get(&url), auth)?; let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?; if response.plugin_id != plugin_id { @@ -883,7 +889,7 @@ pub async fn install_remote_plugin( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/{plugin_id}/install"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?; let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { @@ -919,7 +925,7 @@ pub async fn uninstall_remote_plugin( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/{plugin_id}/uninstall"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?; let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { @@ -1252,7 +1258,7 @@ async fn get_remote_plugin_list_page( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/list"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = authenticated_request(client.get(&url), auth)?; request = request.query(&[("scope", scope.api_value())]); request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); @@ -1269,7 +1275,7 @@ async fn get_remote_shared_workspace_plugins_page( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/workspace/shared"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = authenticated_request(client.get(&url), auth)?; request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); if let Some(page_token) = page_token { @@ -1287,7 +1293,7 @@ async fn get_remote_plugin_installed_page( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/installed"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = authenticated_request(client.get(&url), auth)?; request = request.query(&[("scope", scope.api_value())]); if include_download_urls { @@ -1307,7 +1313,7 @@ async fn fetch_plugin_detail( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/{plugin_id}"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = authenticated_request(client.get(&url), auth)?; if include_download_urls { request = request.query(&[("includeDownloadUrls", true)]); diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index 9b5a2572a4..0b454ed68b 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -1,6 +1,5 @@ use super::*; use codex_login::CodexAuth; -use codex_login::default_client::build_reqwest_client; use codex_utils_absolute_path::AbsolutePathBuf; use flate2::Compression; use flate2::write::GzEncoder; @@ -282,7 +281,7 @@ pub async fn delete_remote_plugin_share( let auth = ensure_chatgpt_auth(auth)?; let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.delete(&url), auth)?; send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?; if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) { @@ -315,7 +314,7 @@ pub async fn update_remote_plugin_share_targets( .unwrap_or_default(); let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.put(&url), auth)?.json( &RemotePluginShareUpdateTargetsRequest { discoverability, @@ -381,7 +380,7 @@ async fn get_created_workspace_plugins_page( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/workspace/created"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = authenticated_request(client.get(&url), auth)?; request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); if let Some(page_token) = page_token { @@ -399,7 +398,7 @@ async fn create_workspace_plugin_upload( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/public/plugins/workspace/upload-url"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?.json( &RemoteWorkspacePluginUploadUrlRequest { filename, @@ -415,7 +414,7 @@ async fn put_workspace_plugin_upload( upload_url: &str, archive_bytes: Vec, ) -> Result<(), RemotePluginCatalogError> { - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = client .put(upload_url) .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) @@ -453,7 +452,7 @@ async fn finalize_workspace_plugin_upload( } else { format!("{base_url}/public/plugins/workspace") }; - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?.json(&body); send_and_decode(request, &url).await } diff --git a/codex-rs/core-plugins/src/remote_bundle.rs b/codex-rs/core-plugins/src/remote_bundle.rs index ed7cd56a86..d530d02ad9 100644 --- a/codex-rs/core-plugins/src/remote_bundle.rs +++ b/codex-rs/core-plugins/src/remote_bundle.rs @@ -2,6 +2,7 @@ use crate::store::PluginInstallResult; use crate::store::PluginStore; use crate::store::PluginStoreError; use crate::store::validate_plugin_version_segment; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_plugin::PluginId; use codex_plugin::PluginIdError; @@ -29,6 +30,11 @@ const REMOTE_PLUGIN_INSTALL_STAGING_DIR: &str = "plugins/.remote-plugin-install- const TEST_ALLOW_LOOPBACK_HTTP_REMOTE_PLUGIN_BUNDLES_ENV: &str = "CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS"; +fn build_process_reqwest_client() -> reqwest::Client { + let originator = Originator::process_default(); + build_reqwest_client(&originator) +} + #[derive(Debug, Clone)] pub struct ValidatedRemotePluginBundle { pub plugin_id: PluginId, @@ -263,7 +269,7 @@ async fn download_remote_plugin_bundle_with_limit( bundle_download_url: &str, max_bytes: u64, ) -> Result, RemotePluginBundleInstallError> { - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let response = client .get(bundle_download_url) .timeout(REMOTE_PLUGIN_BUNDLE_DOWNLOAD_TIMEOUT) diff --git a/codex-rs/core-plugins/src/remote_legacy.rs b/codex-rs/core-plugins/src/remote_legacy.rs index dcf9f79eb8..f7c3c17fe9 100644 --- a/codex-rs/core-plugins/src/remote_legacy.rs +++ b/codex-rs/core-plugins/src/remote_legacy.rs @@ -1,5 +1,6 @@ use crate::remote::RemotePluginServiceConfig; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_protocol::protocol::Product; use serde::Deserialize; @@ -11,6 +12,11 @@ const REMOTE_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(30); const REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(10); const REMOTE_PLUGIN_MUTATION_TIMEOUT: Duration = Duration::from_secs(30); +fn build_process_reqwest_client() -> reqwest::Client { + let originator = Originator::process_default(); + build_reqwest_client(&originator) +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct RemotePluginStatusSummary { pub name: String, @@ -129,7 +135,7 @@ pub async fn fetch_remote_plugin_status( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/list"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = client .get(&url) .timeout(REMOTE_PLUGIN_FETCH_TIMEOUT) @@ -161,7 +167,7 @@ pub async fn fetch_remote_featured_plugin_ids( ) -> Result, RemotePluginFetchError> { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/featured"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let mut request = client .get(&url) .query(&[( @@ -236,7 +242,7 @@ async fn post_remote_plugin_mutation( ) -> Result { let auth = ensure_codex_backend_auth(auth)?; let url = remote_plugin_mutation_url(config, plugin_id, action)?; - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let request = client .post(url.clone()) .timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT) diff --git a/codex-rs/core-plugins/src/startup_sync.rs b/codex-rs/core-plugins/src/startup_sync.rs index f03aff93f6..2087a97e3a 100644 --- a/codex-rs/core-plugins/src/startup_sync.rs +++ b/codex-rs/core-plugins/src/startup_sync.rs @@ -13,6 +13,7 @@ use tempfile::TempDir; use tracing::warn; use zip::ZipArchive; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; const GITHUB_API_BASE_URL: &str = "https://api.github.com"; @@ -31,6 +32,11 @@ const CURATED_PLUGINS_BACKUP_ARCHIVE_TIMEOUT: Duration = Duration::from_secs(30) // Keep this comfortably above a normal sync attempt so we do not race another Codex process. const CURATED_PLUGINS_STALE_TEMP_DIR_MAX_AGE: Duration = Duration::from_secs(10 * 60); +fn build_process_reqwest_client() -> Client { + let originator = Originator::process_default(); + build_reqwest_client(&originator) +} + #[derive(Debug, Deserialize)] struct GitHubRepositorySummary { default_branch: String, @@ -590,7 +596,7 @@ fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> { async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result { let api_base_url = api_base_url.trim_end_matches('/'); let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let repo_body = fetch_github_text(&client, &repo_url, "get curated plugins repository").await?; let repo_summary: GitHubRepositorySummary = serde_json::from_str(&repo_body).map_err(|err| { @@ -624,14 +630,14 @@ async fn fetch_curated_repo_zipball( let api_base_url = api_base_url.trim_end_matches('/'); let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}"); let zipball_url = format!("{repo_url}/zipball/{remote_sha}"); - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); fetch_github_bytes(&client, &zipball_url, "download curated plugins archive").await } async fn fetch_curated_repo_backup_archive_zip( backup_archive_api_url: &str, ) -> Result, String> { - let client = build_reqwest_client(); + let client = build_process_reqwest_client(); let export_body = fetch_public_text( &client, backup_archive_api_url, diff --git a/codex-rs/core-skills/src/remote.rs b/codex-rs/core-skills/src/remote.rs index 1ca7cd0cb7..a126c4a1ff 100644 --- a/codex-rs/core-skills/src/remote.rs +++ b/codex-rs/core-skills/src/remote.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use std::time::Duration; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); @@ -107,7 +108,8 @@ pub async fn list_remote_skills( query_params.push(("enabled", enabled)); } - let client = build_reqwest_client(); + let originator = Originator::process_default(); + let client = build_reqwest_client(&originator); let request = client .get(&url) .timeout(REMOTE_SKILLS_API_TIMEOUT) @@ -146,7 +148,8 @@ pub async fn export_remote_skill( ) -> Result { let auth = ensure_codex_backend_auth(auth)?; - let client = build_reqwest_client(); + let originator = Originator::process_default(); + let client = build_reqwest_client(&originator); let base_url = chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/hazelnuts/{skill_id}/export"); let request = client diff --git a/codex-rs/core/src/arc_monitor.rs b/codex-rs/core/src/arc_monitor.rs index a4f7e038e0..084f5d029a 100644 --- a/codex-rs/core/src/arc_monitor.rs +++ b/codex-rs/core/src/arc_monitor.rs @@ -9,6 +9,7 @@ use crate::compact::content_items_to_text; use crate::event_mapping::is_contextual_user_message_content; use crate::session::session::Session; use crate::session::turn_context::TurnContext; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseItem; @@ -128,7 +129,8 @@ pub(crate) async fn monitor_action( }; let body = build_arc_monitor_request(sess, turn_context, action, protection_client_callsite).await; - let client = build_reqwest_client(); + let originator = Originator::process_default(); + let client = build_reqwest_client(&originator); let mut request = client.post(&url).timeout(ARC_MONITOR_TIMEOUT).json(&body); if let Some(token) = env_token { request = request.bearer_auth(token); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index f604a63458..8fa95c0752 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -66,7 +66,9 @@ use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::RefreshTokenError; use codex_login::UnauthorizedRecovery; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; +use codex_login::default_client::default_headers; use codex_otel::SessionTelemetry; use codex_otel::current_span_w3c_trace_context; @@ -151,6 +153,16 @@ const MEMORIES_SUMMARIZE_ENDPOINT: &str = "/memories/trace_summarize"; pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = Duration::from_millis(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS); +fn process_reqwest_client() -> reqwest::Client { + let originator = Originator::process_default(); + build_reqwest_client(&originator) +} + +fn process_default_headers() -> ApiHeaderMap { + let originator = Originator::process_default(); + default_headers(&originator) +} + pub(crate) struct CompactConversationRequestSettings { pub(crate) effort: Option, pub(crate) summary: ReasoningSummaryConfig, @@ -442,7 +454,7 @@ impl ModelClient { return Ok(Vec::new()); } let client_setup = self.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = ReqwestTransport::new(process_reqwest_client()); let request_telemetry = Self::build_request_telemetry( session_telemetry, AuthRequestTelemetryContext::new( @@ -530,7 +542,7 @@ impl ModelClient { sideband_headers.extend(sideband_websocket_auth_headers( client_setup.api_auth.as_ref(), )); - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = ReqwestTransport::new(process_reqwest_client()); let response = ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth) .create_with_session_and_headers(sdp, session_config, extra_headers) @@ -561,7 +573,7 @@ impl ModelClient { } let client_setup = self.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = ReqwestTransport::new(process_reqwest_client()); let request_telemetry = Self::build_request_telemetry( session_telemetry, AuthRequestTelemetryContext::new( @@ -823,7 +835,7 @@ impl ModelClient { websocket_connect_timeout, ApiWebSocketResponsesClient::new(api_provider, api_auth).connect( headers, - codex_login::default_client::default_headers(), + process_default_headers(), turn_state, Some(websocket_telemetry), ), @@ -1225,7 +1237,7 @@ impl ModelClientSession { let mut pending_retry = PendingUnauthorizedRetry::default(); loop { let client_setup = self.client.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = ReqwestTransport::new(process_reqwest_client()); let request_auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), client_setup.api_auth.as_ref(), diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index f4ee42c821..3a132d0e97 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -32,7 +32,7 @@ use codex_core_plugins::PluginsManager; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpConnectionManager; use codex_mcp::McpRuntimeEnvironment; @@ -124,7 +124,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( directory_connectors, accessible_connectors, &connector_ids, - originator().value.as_str(), + Originator::process_default().value(), ) .into_iter() .map(DiscoverableTool::from); @@ -153,7 +153,7 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools( read_cached_accessible_connectors(&cache_key).map(|connectors| { codex_connectors::filter::filter_disallowed_connectors( connectors, - originator().value.as_str(), + Originator::process_default().value(), ) }) } @@ -170,7 +170,7 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools( let cache_key = accessible_connectors_cache_key(config, auth); let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors( accessible_connectors_from_mcp_tools(mcp_tools), - originator().value.as_str(), + Originator::process_default().value(), ); write_cached_accessible_connectors(cache_key, &accessible_connectors); } @@ -232,7 +232,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( { let cached_connectors = codex_connectors::filter::filter_disallowed_connectors( cached_connectors, - originator().value.as_str(), + Originator::process_default().value(), ); let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance); return Ok(AccessibleConnectorsStatus { @@ -341,7 +341,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors( accessible_connectors_from_mcp_tools(&tools), - originator().value.as_str(), + Originator::process_default().value(), ); if codex_apps_ready || !accessible_connectors.is_empty() { write_cached_accessible_connectors(cache_key, &accessible_connectors); diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index b8552596b3..5883da92b9 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -5,8 +5,8 @@ use codex_config::ConfigEditsBuilder; use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; use codex_config::load_global_mcp_servers; +use codex_login::default_client::Originator; use codex_login::default_client::is_first_party_originator; -use codex_login::default_client::originator; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; @@ -38,8 +38,8 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies( mentioned_skills: &[SkillMetadata], elicitation_reviewer: Option, ) { - let originator_value = originator().value; - if !is_first_party_originator(originator_value.as_str()) { + let originator = Originator::process_default(); + if !is_first_party_originator(originator.value()) { // Only support first-party clients for now. return; } diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index 8b5aac5f78..7292711051 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -2,7 +2,7 @@ use crate::config::Config; use codex_config::types::OtelExporterKind as Kind; use codex_config::types::OtelHttpProtocol as Protocol; use codex_features::Feature; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_otel::OtelExporter; use codex_otel::OtelHttpProtocol; use codex_otel::OtelProvider; @@ -76,8 +76,8 @@ pub fn build_provider( OtelExporter::None }; - let originator = originator(); - let service_name = service_name_override.unwrap_or(originator.value.as_str()); + let originator = Originator::process_default(); + let service_name = service_name_override.unwrap_or(originator.value()); let runtime_metrics = config.features.enabled(Feature::RuntimeMetrics); OtelProvider::from(&OtelSettings { diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 249b3ae15f..e0e33f110e 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -24,6 +24,7 @@ use codex_app_server_protocol::AuthMode; use codex_config::config_toml::RealtimeWsMode; use codex_config::config_toml::RealtimeWsVersion; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::default_headers; use codex_login::read_openai_api_key_from_env; use codex_model_provider_info::ModelProviderInfo; @@ -74,6 +75,11 @@ const REALTIME_V2_STEER_ACKNOWLEDGEMENT: &str = const REALTIME_ACTIVE_RESPONSE_ERROR_PREFIX: &str = "Conversation already has an active response in progress:"; +fn process_default_headers() -> HeaderMap { + let originator = Originator::process_default(); + default_headers(&originator) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum RealtimeConversationEnd { Requested, @@ -333,7 +339,7 @@ impl RealtimeConversationManager { .connect( session_config, extra_headers.unwrap_or_default(), - default_headers(), + process_default_headers(), ) .await .map_err(map_api_error)?; @@ -1054,7 +1060,7 @@ fn spawn_webrtc_sideband_input_task(input: RealtimeWebrtcSidebandInputTask) -> J session_config, &call_id, sideband_headers, - default_headers(), + process_default_headers(), ) .await { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8f0d2f1c01..ebe84f084f 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -59,7 +59,6 @@ use codex_hooks::HooksConfig; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::auth_env_telemetry::collect_auth_env_telemetry; -use codex_login::default_client::originator; use codex_mcp::McpConnectionManager; use codex_mcp::McpRuntimeEnvironment; use codex_mcp::codex_apps_tools_cache_key; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 2d96cf9165..560f9486b3 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -2,6 +2,7 @@ use super::input_queue::InputQueue; use super::*; use crate::goals::GoalRuntimeState; use crate::state::ActiveTurn; +use codex_login::default_client::Originator; use codex_protocol::SessionId; use codex_protocol::config_types::ServiceTier; use codex_protocol::permissions::FileSystemPath; @@ -670,7 +671,7 @@ impl Session { let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from); let account_id = auth.and_then(CodexAuth::get_account_id); let account_email = auth.and_then(CodexAuth::get_account_email); - let originator = originator().value; + let originator = Originator::process_default().value().to_string(); let terminal_type = user_agent(); let session_model = session_configuration.collaboration_mode.model().to_string(); let auth_env_telemetry = collect_auth_env_telemetry( diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index a094545bab..dcaa590768 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -6,7 +6,7 @@ use codex_config::types::WindowsSandboxModeToml; use codex_features::Feature; use codex_features::Features; use codex_features::FeaturesToml; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_otel::sanitize_metric_tag_value; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::protocol::SandboxPolicy; @@ -283,7 +283,8 @@ pub struct WindowsSandboxSetupRequest { pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> anyhow::Result<()> { let start = Instant::now(); let mode = request.mode; - let originator_tag = sanitize_metric_tag_value(originator().value.as_str()); + let originator = Originator::process_default(); + let originator_tag = sanitize_metric_tag_value(originator.value()); let result = run_windows_sandbox_setup_and_persist(request).await; match result { diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f6731a0ea4..3c78ccab8e 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -11,7 +11,7 @@ use codex_extension_api::empty_extension_registry; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_model_provider_info::built_in_model_providers; @@ -781,7 +781,7 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() { assert_eq!(request_session_id, expected_session_id.to_string()); assert_eq!(request_thread_id, thread_id_string.as_str()); - assert_eq!(request_originator, originator().value); + assert_eq!(request_originator, Originator::process_default().value()); assert_eq!(request_authorization, "Bearer Test API Key"); assert_eq!( request_body["prompt_cache_key"].as_str(), @@ -1052,7 +1052,7 @@ async fn chatgpt_auth_sends_correct_request() { assert_eq!(request_session_id, expected_session_id.to_string()); assert_eq!(request_thread_id, expected_thread_id.to_string()); - assert_eq!(request_originator, originator().value); + assert_eq!(request_originator, Originator::process_default().value()); assert_eq!(request_authorization, "Bearer Access Token"); assert_eq!(request_chatgpt_account_id, "account_id"); assert_eq!( diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index feee9d4227..306a21fabc 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -8,6 +8,7 @@ use codex_core::ResponseEvent; use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; use codex_features::Feature; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_otel::MetricsClient; @@ -61,6 +62,11 @@ const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; const X_CODEX_WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY: &str = "x-codex-ws-stream-request-start-ms"; +fn default_user_agent() -> String { + let originator = Originator::process_default(); + codex_login::default_client::get_codex_user_agent(&originator) +} + fn assert_request_trace_matches(body: &serde_json::Value, expected_trace: &W3cTraceContext) { let client_metadata = body["client_metadata"] .as_object() @@ -141,7 +147,7 @@ async fn responses_websocket_streams_request() { ); assert_eq!( handshake.header(USER_AGENT_HEADER), - Some(codex_login::default_client::get_codex_user_agent()) + Some(default_user_agent()) ); assert_eq!( body["client_metadata"]["x-codex-installation-id"].as_str(), @@ -385,7 +391,7 @@ async fn responses_websocket_reuses_connection_with_per_turn_trace_payloads() { assert_eq!(server.handshakes().len(), 1); assert_eq!( server.single_handshake().header(USER_AGENT_HEADER), - Some(codex_login::default_client::get_codex_user_agent()) + Some(default_user_agent()) ); let connection = server.single_connection(); assert_eq!(connection.len(), 2); @@ -474,7 +480,7 @@ async fn responses_websocket_preconnect_reuses_connection() { assert_eq!(server.handshakes().len(), 1); assert_eq!( server.single_handshake().header(USER_AGENT_HEADER), - Some(codex_login::default_client::get_codex_user_agent()) + Some(default_user_agent()) ); let connection = server.single_connection(); assert_eq!(connection.len(), 1); @@ -512,7 +518,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() { assert_eq!(server.handshakes().len(), 1); assert_eq!( server.single_handshake().header(USER_AGENT_HEADER), - Some(codex_login::default_client::get_codex_user_agent()) + Some(default_user_agent()) ); let connection = server.single_connection(); assert_eq!(connection.len(), 2); @@ -1359,10 +1365,7 @@ async fn responses_websocket_connection_limit_error_reconnects_and_completes() { .collect(); assert_eq!( handshake_user_agents, - vec![ - Some(codex_login::default_client::get_codex_user_agent()), - Some(codex_login::default_client::get_codex_user_agent()), - ] + vec![Some(default_user_agent()), Some(default_user_agent()),] ); server.shutdown().await; diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 45fdf3df07..4beef3fc4a 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -429,7 +429,11 @@ async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> { ); assert_eq!( event["event_params"]["product_client_id"], - serde_json::json!(codex_login::default_client::originator().value) + serde_json::json!( + codex_login::default_client::Originator::process_default() + .value() + .to_string() + ) ); assert_eq!(event["event_params"]["model_slug"], "gpt-5.2"); assert!(event["event_params"]["thread_id"].as_str().is_some()); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 9e54a72abd..0cf67aefd6 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -72,7 +72,6 @@ use codex_feedback::CodexFeedback; use codex_git_utils::get_git_repo_root; use codex_login::AuthConfig; use codex_login::default_client::set_default_client_residency_requirement; -use codex_login::default_client::set_default_originator; use codex_login::enforce_login_restrictions; use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; @@ -236,10 +235,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result eprintln!("{message}"); } - if let Err(err) = set_default_originator("codex_exec".to_string()) { - tracing::warn!(?err, "Failed to set codex exec originator override {err:?}"); - } - let Cli { command, strict_config, diff --git a/codex-rs/login/src/auth/agent_identity.rs b/codex-rs/login/src/auth/agent_identity.rs index 3644713328..ba693df37c 100644 --- a/codex-rs/login/src/auth/agent_identity.rs +++ b/codex-rs/login/src/auth/agent_identity.rs @@ -3,6 +3,7 @@ use codex_agent_identity::register_agent_task; use codex_protocol::account::PlanType as AccountPlanType; use std::env; +use crate::default_client::Originator; use crate::default_client::build_reqwest_client; use super::storage::AgentIdentityAuthRecord; @@ -19,8 +20,9 @@ pub struct AgentIdentityAuth { impl AgentIdentityAuth { pub async fn load(record: AgentIdentityAuthRecord) -> std::io::Result { let agent_identity_authapi_base_url = agent_identity_authapi_base_url(); + let originator = Originator::process_default(); let process_task_id = register_agent_task( - &build_reqwest_client(), + &build_reqwest_client(&originator), &agent_identity_authapi_base_url, key(&record), ) diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index ee51edf9b5..0efdc3bbb4 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -14,79 +14,111 @@ use reqwest::header::HeaderMap; use reqwest::header::HeaderValue; use reqwest::header::USER_AGENT; use std::sync::LazyLock; -use std::sync::Mutex; use std::sync::RwLock; - -/// Set this to add a suffix to the User-Agent string. -/// -/// It is not ideal that we're using a global singleton for this. -/// This is primarily designed to differentiate MCP clients from each other. -/// Because there can only be one MCP server per process, it should be safe for this to be a global static. -/// However, future users of this should use this with caution as a result. -/// In addition, we want to be confident that this value is used for ALL clients and doing that requires a -/// lot of wiring and it's easy to miss code paths by doing so. -/// See https://github.com/openai/codex/pull/3388/files for an example of what that would look like. -/// Finally, we want to make sure this is set for ALL mcp clients without needing to know a special env var -/// or having to set data that they already specified in the mcp initialize request somewhere else. -/// -/// A space is automatically added between the suffix and the rest of the User-Agent string. -/// The full user agent string is returned from the mcp initialize response. -/// Parenthesis will be added by Codex. This should only specify what goes inside of the parenthesis. -pub static USER_AGENT_SUFFIX: LazyLock>> = LazyLock::new(|| Mutex::new(None)); pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs"; pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency"; pub use codex_config::ResidencyRequirement; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Originator { - pub value: String, - pub header_value: HeaderValue, -} -static ORIGINATOR: LazyLock>> = LazyLock::new(|| RwLock::new(None)); -static REQUIREMENTS_RESIDENCY: LazyLock>> = - LazyLock::new(|| RwLock::new(None)); - -#[derive(Debug)] -pub enum SetOriginatorError { - InvalidHeaderValue, - AlreadyInitialized, + kind: OriginatorKind, } -fn get_originator_value(provided: Option) -> Originator { - let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR) - .ok() - .or(provided) - .unwrap_or(DEFAULT_ORIGINATOR.to_string()); +#[derive(Debug, Clone, PartialEq, Eq)] +enum OriginatorKind { + Process { value: String }, + AppServerClient { client: AppServerClient }, +} - match HeaderValue::from_str(&value) { - Ok(header_value) => Originator { - value, - header_value, - }, - Err(e) => { - tracing::error!("Unable to turn originator override {value} into header value: {e}"); - Originator { - value: DEFAULT_ORIGINATOR.to_string(), - header_value: HeaderValue::from_static(DEFAULT_ORIGINATOR), +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AppServerClient { + name: String, + version: String, +} + +impl Originator { + pub fn process_default() -> Self { + let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR) + .unwrap_or_else(|_| DEFAULT_ORIGINATOR.to_string()); + + match Self::for_process(value.clone()) { + Ok(originator) => originator, + Err(e) => { + tracing::error!( + "Unable to turn originator override {value} into header value: {e}" + ); + Self::for_process(DEFAULT_ORIGINATOR.to_string()) + .expect("default originator should be a valid HTTP header value") } } } + + pub fn for_process(value: String) -> Result { + validate_originator_value(&value)?; + Ok(Self { + kind: OriginatorKind::Process { value }, + }) + } + + pub fn from_app_server_client( + name: String, + version: String, + ) -> Result { + validate_originator_value(&name)?; + Ok(Self { + kind: OriginatorKind::AppServerClient { + client: AppServerClient { name, version }, + }, + }) + } + + pub fn value(&self) -> &str { + match &self.kind { + OriginatorKind::Process { value } => value, + OriginatorKind::AppServerClient { client } => client.name(), + } + } + + pub fn app_server_client(&self) -> Option<&AppServerClient> { + match &self.kind { + OriginatorKind::Process { .. } => None, + OriginatorKind::AppServerClient { client } => Some(client), + } + } } -pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> { - if HeaderValue::from_str(&value).is_err() { - return Err(SetOriginatorError::InvalidHeaderValue); +impl AppServerClient { + pub fn name(&self) -> &str { + &self.name } - let originator = get_originator_value(Some(value)); - let Ok(mut guard) = ORIGINATOR.write() else { - return Err(SetOriginatorError::AlreadyInitialized); - }; - if guard.is_some() { - return Err(SetOriginatorError::AlreadyInitialized); + + pub fn version(&self) -> &str { + &self.version } - *guard = Some(originator); +} + +static REQUIREMENTS_RESIDENCY: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InvalidOriginator { + InvalidHeaderValue, +} + +impl std::fmt::Display for InvalidOriginator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidHeaderValue => f.write_str("invalid HTTP header value"), + } + } +} + +impl std::error::Error for InvalidOriginator {} + +fn validate_originator_value(value: &str) -> Result<(), InvalidOriginator> { + HeaderValue::from_str(value).map_err(|_| InvalidOriginator::InvalidHeaderValue)?; Ok(()) } @@ -98,27 +130,6 @@ pub fn set_default_client_residency_requirement(enforce_residency: Option Originator { - if let Ok(guard) = ORIGINATOR.read() - && let Some(originator) = guard.as_ref() - { - return originator.clone(); - } - - if std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() { - let originator = get_originator_value(/*provided*/ None); - if let Ok(mut guard) = ORIGINATOR.write() { - match guard.as_ref() { - Some(originator) => return originator.clone(), - None => *guard = Some(originator.clone()), - } - } - return originator; - } - - get_originator_value(/*provided*/ None) -} - pub fn is_first_party_originator(originator_value: &str) -> bool { originator_value == DEFAULT_ORIGINATOR || originator_value == "codex-tui" @@ -130,23 +141,18 @@ pub fn is_first_party_chat_originator(originator_value: &str) -> bool { originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop" } -pub fn get_codex_user_agent() -> String { +pub fn get_codex_user_agent(originator: &Originator) -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); - let originator = originator(); let prefix = format!( "{}/{build_version} ({} {}; {}) {}", - originator.value.as_str(), + originator.value(), os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), user_agent() ); - let suffix = USER_AGENT_SUFFIX - .lock() - .ok() - .and_then(|guard| guard.clone()); - let suffix = suffix + let suffix = user_agent_suffix(originator) .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) @@ -184,13 +190,19 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String { tracing::warn!( "Falling back to default Codex originator because base user agent string is invalid" ); - originator().value + DEFAULT_ORIGINATOR.to_string() } } +fn user_agent_suffix(originator: &Originator) -> Option { + originator + .app_server_client() + .map(|client| format!("{}; {}", client.name(), client.version())) +} + /// Create an HTTP client with default `originator` and `User-Agent` headers set. -pub fn create_client() -> CodexHttpClient { - let inner = build_reqwest_client(); +pub fn create_client(originator: &Originator) -> CodexHttpClient { + let inner = build_reqwest_client(originator); CodexHttpClient::new(inner) } @@ -200,8 +212,12 @@ pub fn create_client() -> CodexHttpClient { /// policy, then layers in shared custom CA handling from `CODEX_CA_CERTIFICATE` / /// `SSL_CERT_FILE`. The function remains infallible for compatibility with existing call sites, so /// a custom-CA or builder failure is logged and falls back to `reqwest::Client::new()`. -pub fn build_reqwest_client() -> reqwest::Client { - try_build_reqwest_client().unwrap_or_else(|error| { +pub fn build_reqwest_client(originator: &Originator) -> reqwest::Client { + build_reqwest_client_with_headers(default_headers(originator)) +} + +fn build_reqwest_client_with_headers(headers: HeaderMap) -> reqwest::Client { + try_build_reqwest_client_with_headers(headers).unwrap_or_else(|error| { tracing::warn!(error = %error, "failed to build default reqwest client"); with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()) .build() @@ -219,8 +235,16 @@ pub fn build_reqwest_client() -> reqwest::Client { /// /// Callers that need a structured CA-loading failure instead of the legacy logged fallback can use /// this method directly. -pub fn try_build_reqwest_client() -> Result { - let mut builder = reqwest::Client::builder().default_headers(default_headers()); +pub fn try_build_reqwest_client( + originator: &Originator, +) -> Result { + try_build_reqwest_client_with_headers(default_headers(originator)) +} + +fn try_build_reqwest_client_with_headers( + headers: HeaderMap, +) -> Result { + let mut builder = reqwest::Client::builder().default_headers(headers); if is_sandboxed() { builder = builder.no_proxy(); } @@ -229,10 +253,13 @@ pub fn try_build_reqwest_client() -> Result HeaderMap { +pub fn default_headers(originator: &Originator) -> HeaderMap { let mut headers = HeaderMap::new(); - headers.insert("originator", originator().header_value); - if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent()) { + let originator_header = HeaderValue::from_str(originator.value()) + .expect("originator should have been validated as a header value"); + headers.insert("originator", originator_header); + let user_agent = get_codex_user_agent(originator); + if let Ok(user_agent) = HeaderValue::from_str(&user_agent) { headers.insert(USER_AGENT, user_agent); } if let Ok(guard) = REQUIREMENTS_RESIDENCY.read() diff --git a/codex-rs/login/src/auth/default_client_tests.rs b/codex-rs/login/src/auth/default_client_tests.rs index be8e6bc392..c9833a7664 100644 --- a/codex-rs/login/src/auth/default_client_tests.rs +++ b/codex-rs/login/src/auth/default_client_tests.rs @@ -2,12 +2,13 @@ use super::sanitize_user_agent; use super::*; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use serial_test::serial; #[test] fn test_get_codex_user_agent() { - let user_agent = get_codex_user_agent(); - let originator = originator().value; - let prefix = format!("{originator}/"); + let originator = Originator::process_default(); + let user_agent = get_codex_user_agent(&originator); + let prefix = format!("{}/", originator.value()); assert!(user_agent.starts_with(&prefix)); } @@ -44,7 +45,8 @@ async fn test_create_client_sets_default_headers() { use wiremock::matchers::method; use wiremock::matchers::path; - let client = create_client(); + let originator = Originator::process_default(); + let client = create_client(&originator); // Spin up a local mock server and capture a request. let server = MockServer::start().await; @@ -72,10 +74,10 @@ async fn test_create_client_sets_default_headers() { let originator_header = headers .get("originator") .expect("originator header missing"); - assert_eq!(originator_header.to_str().unwrap(), originator().value); + assert_eq!(originator_header.to_str().unwrap(), originator.value()); // User-Agent matches the computed Codex UA for that originator - let expected_ua = get_codex_user_agent(); + let expected_ua = get_codex_user_agent(&originator); let ua_header = headers .get("user-agent") .expect("user-agent header missing"); @@ -89,6 +91,94 @@ async fn test_create_client_sets_default_headers() { set_default_client_residency_requirement(/*enforce_residency*/ None); } +#[test] +fn app_server_originator_builds_explicit_headers() { + let originator = + Originator::from_app_server_client("codex_ios".to_string(), "1.2.3".to_string()) + .expect("originator should be valid"); + + let headers = default_headers(&originator); + assert_eq!( + headers + .get("originator") + .expect("originator header missing") + .to_str() + .expect("originator should be valid"), + "codex_ios" + ); + assert_eq!( + originator.app_server_client().map(AppServerClient::name), + Some("codex_ios") + ); + assert_eq!( + originator.app_server_client().map(AppServerClient::version), + Some("1.2.3") + ); + assert!( + headers + .get("user-agent") + .expect("user-agent header missing") + .to_str() + .expect("user-agent should be valid") + .starts_with("codex_ios/") + ); + assert!(get_codex_user_agent(&originator).contains("(codex_ios; 1.2.3)")); +} + +#[test] +fn process_originator_does_not_add_user_agent_suffix() { + let originator = + Originator::for_process("codex_exec".to_string()).expect("originator should be valid"); + + assert_eq!(originator.value(), "codex_exec"); + assert_eq!(originator.app_server_client(), None); + assert!(!get_codex_user_agent(&originator).contains("(codex_exec")); +} + +#[test] +#[serial(originator_env)] +fn process_default_reads_originator_override() { + let _guard = EnvVarGuard::set(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR, "codex_override"); + + let originator = Originator::process_default(); + + assert_eq!(originator.value(), "codex_override"); +} + +#[test] +#[serial(originator_env)] +fn app_server_originator_ignores_originator_override() { + let _guard = EnvVarGuard::set(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR, "codex_override"); + + let originator = + Originator::from_app_server_client("codex_ios".to_string(), "1.2.3".to_string()) + .expect("originator should be valid"); + + assert_eq!(originator.value(), "codex_ios"); +} + +#[test] +#[serial(originator_env)] +fn invalid_process_default_override_falls_back() { + let _guard = EnvVarGuard::set(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR, "bad\rvalue"); + + let originator = Originator::process_default(); + + assert_eq!(originator.value(), DEFAULT_ORIGINATOR); +} + +#[test] +fn invalid_originator_values_are_rejected() { + assert_eq!( + Originator::for_process("bad\rvalue".to_string()), + Err(InvalidOriginator::InvalidHeaderValue) + ); + assert_eq!( + Originator::from_app_server_client("bad\rvalue".to_string(), "1.2.3".to_string()), + Err(InvalidOriginator::InvalidHeaderValue) + ); +} + #[test] fn test_invalid_suffix_is_sanitized() { let prefix = "codex_cli_rs/0.0.0"; @@ -115,11 +205,38 @@ fn test_invalid_suffix_is_sanitized2() { #[cfg(target_os = "macos")] fn test_macos() { use regex_lite::Regex; - let user_agent = get_codex_user_agent(); - let originator = regex_lite::escape(originator().value.as_str()); + let originator = Originator::process_default(); + let user_agent = get_codex_user_agent(&originator); + let originator = regex_lite::escape(originator.value()); let re = Regex::new(&format!( r"^{originator}/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$" )) .unwrap(); assert!(re.is_match(&user_agent)); } + +struct EnvVarGuard { + key: &'static str, + original: Option, +} + +impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + Self { key, original } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + unsafe { + match &self.original { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } + } +} diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index a2e4e8e0d8..a54af62335 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -31,6 +31,7 @@ pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; use crate::auth::util::try_parse_error_message; +use crate::default_client::Originator; use crate::default_client::build_reqwest_client; use crate::default_client::create_client; use crate::token_data::TokenData; @@ -203,7 +204,8 @@ impl CodexAuth { chatgpt_base_url: Option<&str>, ) -> std::io::Result { let auth_mode = auth_dot_json.resolved_mode(); - let client = create_client(); + let originator = Originator::process_default(); + let client = create_client(&originator); if auth_mode == ApiAuthMode::ApiKey { let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else { return Err(std::io::Error::other("API key auth is missing a key.")); @@ -423,7 +425,8 @@ impl CodexAuth { agent_identity: None, }; - let client = create_client(); + let originator = Originator::process_default(); + let client = create_client(&originator); let state = ChatgptAuthState { auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))), client, @@ -493,7 +496,8 @@ async fn verified_agent_identity_record( chatgpt_base_url: &str, ) -> std::io::Result { AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?; - let jwks = fetch_agent_identity_jwks(&build_reqwest_client(), chatgpt_base_url) + let originator = Originator::process_default(); + let jwks = fetch_agent_identity_jwks(&build_reqwest_client(&originator), chatgpt_base_url) .await .map_err(std::io::Error::other)?; let claims = decode_agent_identity_jwt(jwt, Some(&jwks)).map_err(std::io::Error::other)?; diff --git a/codex-rs/login/src/auth/revoke.rs b/codex-rs/login/src/auth/revoke.rs index b6a9fef4bb..9c384c5635 100644 --- a/codex-rs/login/src/auth/revoke.rs +++ b/codex-rs/login/src/auth/revoke.rs @@ -17,6 +17,7 @@ use super::manager::REVOKE_TOKEN_URL; use super::manager::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR; use super::storage::AuthDotJson; use super::util::try_parse_error_message; +use crate::default_client::Originator; use crate::default_client::create_client; use crate::token_data::TokenData; @@ -59,7 +60,8 @@ pub(crate) async fn revoke_auth_tokens( return Ok(()); }; - let client = create_client(); + let originator = Originator::process_default(); + let client = create_client(&originator); let endpoint = revoke_token_endpoint(); revoke_oauth_token(&client, endpoint.as_str(), token, kind, REVOKE_HTTP_TIMEOUT).await } diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index b72bc946f2..0859bfbb0e 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -29,7 +29,7 @@ use crate::auth::load_auth_dot_json; use crate::auth::revoke_auth_tokens; use crate::auth::save_auth; use crate::auth::should_revoke_auth_tokens; -use crate::default_client::originator; +use crate::default_client::Originator; use crate::pkce::PkceCodes; use crate::pkce::generate_pkce; use crate::token_data::TokenData; @@ -505,7 +505,10 @@ fn build_authorize_url( ("id_token_add_organizations".to_string(), "true".to_string()), ("codex_cli_simplified_flow".to_string(), "true".to_string()), ("state".to_string(), state.to_string()), - ("originator".to_string(), originator().value), + ( + "originator".to_string(), + Originator::process_default().value().to_string(), + ), ]; if let Some(workspace_ids) = forced_chatgpt_workspace_ids { query.push(("allowed_workspace_id".to_string(), workspace_ids.join(","))); diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 9e536d930c..3b808ef2c1 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -8,7 +8,7 @@ use codex_core::config::Config; use codex_exec_server::EnvironmentManager; use codex_extension_api::empty_extension_registry; use codex_login::AuthManager; -use codex_login::default_client::USER_AGENT_SUFFIX; +use codex_login::default_client::Originator; use codex_login::default_client::get_codex_user_agent; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; @@ -210,14 +210,6 @@ impl MessageProcessor { return; } - let client_info = params.client_info; - let name = client_info.name; - let version = client_info.version; - let user_agent_suffix = format!("{name}; {version}"); - if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() { - *suffix = Some(user_agent_suffix); - } - let server_info = Implementation { name: "codex-mcp-server".to_string(), title: Some("Codex".to_string()), @@ -244,7 +236,11 @@ impl MessageProcessor { } }; if let serde_json::Value::Object(ref mut obj) = server_info_value { - obj.insert("user_agent".to_string(), json!(get_codex_user_agent())); + let originator = Originator::process_default(); + obj.insert( + "user_agent".to_string(), + json!(get_codex_user_agent(&originator)), + ); } let mut result_value = match serde_json::to_value(InitializeResult { diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index f50f25f49f..27803bc22a 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -11,7 +11,6 @@ use tokio::process::ChildStdout; use anyhow::Context; use codex_mcp_server::CodexToolCallParam; -use codex_terminal_detection::user_agent; use pretty_assertions::assert_eq; use rmcp::model::CallToolRequestParams; @@ -149,16 +148,8 @@ impl McpProcess { .await?; let initialized = self.read_jsonrpc_message().await?; - let os_info = os_info::get(); - let build_version = env!("CARGO_PKG_VERSION"); - let originator = codex_login::default_client::originator().value; - let user_agent = format!( - "{originator}/{build_version} ({} {}; {}) {} (elicitation test; 0.0.0)", - os_info.os_type(), - os_info.version(), - os_info.architecture().unwrap_or("unknown"), - user_agent() - ); + let originator = codex_login::default_client::Originator::process_default(); + let user_agent = codex_login::default_client::get_codex_user_agent(&originator); let JsonRpcMessage::Response(JsonRpcResponse { jsonrpc, id, diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index b1ffb2d215..ef4d477c25 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -12,7 +12,7 @@ use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::auth_env_telemetry::collect_auth_env_telemetry; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_protocol::SessionId; @@ -86,6 +86,7 @@ impl MemoryStartupContext { let account_id = auth.and_then(CodexAuth::get_account_id); let account_email = auth.and_then(CodexAuth::get_account_email); let model = config.model.as_deref().unwrap_or("unknown"); + let originator = Originator::process_default(); let auth_env_telemetry = collect_auth_env_telemetry( &config.model_provider, auth_manager.codex_api_key_env_enabled(), @@ -97,7 +98,7 @@ impl MemoryStartupContext { account_id, account_email, auth_mode, - originator().value, + originator.value().to_string(), config.otel.log_user_prompt, user_agent(), source, diff --git a/codex-rs/model-provider/src/models_endpoint.rs b/codex-rs/model-provider/src/models_endpoint.rs index 8a72beea70..44cd4eeac3 100644 --- a/codex-rs/model-provider/src/models_endpoint.rs +++ b/codex-rs/model-provider/src/models_endpoint.rs @@ -14,6 +14,7 @@ use codex_login::AuthEnvTelemetry; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::collect_auth_env_telemetry; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_model_provider_info::ModelProviderInfo; use codex_models_manager::manager::ModelsEndpointClient; @@ -88,7 +89,8 @@ impl ModelsEndpointClient for OpenAiModelsEndpoint { let auth_mode = auth.as_ref().map(CodexAuth::auth_mode); let api_provider = self.provider_info.to_api_provider(auth_mode)?; let api_auth = resolve_provider_auth(auth.as_ref(), &self.provider_info)?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let originator = Originator::process_default(); + let transport = ReqwestTransport::new(build_reqwest_client(&originator)); let auth_telemetry = auth_header_telemetry(api_auth.as_ref()); let request_telemetry: Arc = Arc::new(ModelsRequestTelemetry { auth_mode: auth_mode.map(|mode| TelemetryAuthMode::from(mode).to_string()), diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index 23b05b9def..cde6d14dc6 100644 --- a/codex-rs/rollout/src/recorder.rs +++ b/codex-rs/rollout/src/recorder.rs @@ -44,7 +44,7 @@ use super::list::parse_timestamp_uuid_from_filename; use super::metadata; use super::session_index::find_thread_names_by_ids; use crate::config::RolloutConfigView; -use crate::default_client::originator; +use crate::default_client::Originator; use crate::state_db; use crate::state_db::StateDbHandle; use codex_git_utils::collect_git_info; @@ -675,7 +675,7 @@ impl RolloutRecorder { forked_from_id, timestamp, cwd: config.cwd().to_path_buf(), - originator: originator().value, + originator: Originator::process_default().value().to_string(), cli_version: env!("CARGO_PKG_VERSION").to_string(), agent_nickname: source.get_nickname(), agent_role: source.get_agent_role(), diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index cf499c7703..e2c87512ca 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -59,7 +59,6 @@ use codex_core_api::find_codex_home; use codex_core_api::init_state_db; use codex_core_api::item_event_to_server_notification; use codex_core_api::resolve_installation_id; -use codex_core_api::set_default_originator; use codex_core_api::thread_store_from_config; #[derive(Debug, Parser)] @@ -82,10 +81,6 @@ fn main() -> anyhow::Result<()> { } async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { - if let Err(err) = set_default_originator("codex_thread_manager_sample".to_string()) { - tracing::warn!("failed to set originator: {err:?}"); - } - let args = Args::parse(); let prompt = if args.prompt.is_empty() { if std::io::stdin().is_terminal() { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5cfb520dbe..e3ce85540d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -744,7 +744,9 @@ impl App { /*account_id*/ None, bootstrap.account_email.clone(), auth_mode, - codex_login::default_client::originator().value, + codex_login::default_client::Originator::process_default() + .value() + .to_string(), config.otel.log_user_prompt, user_agent(), serde_json::from_value(serde_json::json!("cli")) diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9ece5e3776..38cdf66ee9 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -44,7 +44,7 @@ use codex_config::format_config_error_with_source; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecServerRuntimePaths; use codex_login::AuthConfig; -use codex_login::default_client::originator; +use codex_login::default_client::Originator; use codex_login::default_client::set_default_client_residency_requirement; use codex_login::enforce_login_restrictions; use codex_protocol::ThreadId; @@ -977,7 +977,7 @@ pub async fn run_main( ) .await; - let otel_originator = originator().value; + let otel_originator = Originator::process_default().value().to_string(); let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { crate::legacy_core::otel_init::build_provider( &config, diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index e99852ead9..74ee34b220 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -11,6 +11,7 @@ use crate::update_versions::is_source_build_version; use chrono::DateTime; use chrono::Duration; use chrono::Utc; +use codex_login::default_client::Originator; use codex_login::default_client::create_client; use serde::Deserialize; use serde::Serialize; @@ -85,9 +86,10 @@ fn read_version_info(version_file: &Path) -> anyhow::Result { } async fn check_for_update(version_file: &Path, action: Option) -> anyhow::Result<()> { + let originator = Originator::process_default(); let latest_version = match action { Some(UpdateAction::BrewUpgrade) => { - let HomebrewCaskInfo { version } = create_client() + let HomebrewCaskInfo { version } = create_client(&originator) .get(HOMEBREW_CASK_API_URL) .send() .await? @@ -98,7 +100,7 @@ async fn check_for_update(version_file: &Path, action: Option) -> } Some(UpdateAction::NpmGlobalLatest) | Some(UpdateAction::BunGlobalLatest) => { let latest_version = fetch_latest_github_release_version().await?; - let package_info = create_client() + let package_info = create_client(&originator) .get(npm_registry::PACKAGE_URL) .send() .await? @@ -130,9 +132,10 @@ async fn check_for_update(version_file: &Path, action: Option) -> } async fn fetch_latest_github_release_version() -> anyhow::Result { + let originator = Originator::process_default(); let ReleaseInfo { tag_name: latest_tag_name, - } = create_client() + } = create_client(&originator) .get(LATEST_RELEASE_URL) .send() .await? diff --git a/codex-rs/utils/plugins/src/mcp_connector.rs b/codex-rs/utils/plugins/src/mcp_connector.rs index 0c293936c9..ded5e26539 100644 --- a/codex-rs/utils/plugins/src/mcp_connector.rs +++ b/codex-rs/utils/plugins/src/mcp_connector.rs @@ -1,5 +1,5 @@ +use codex_login::default_client::Originator; use codex_login::default_client::is_first_party_chat_originator; -use codex_login::default_client::originator; const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ "asdk_app_6938a94a61d881918ef32cb999ff937c", @@ -13,7 +13,8 @@ const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] = &["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"]; pub fn is_connector_id_allowed(connector_id: &str) -> bool { - is_connector_id_allowed_for_originator(connector_id, originator().value.as_str()) + let originator = Originator::process_default(); + is_connector_id_allowed_for_originator(connector_id, originator.value()) } fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: &str) -> bool {