diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index c744eb3f6ec4..447e386a8363 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 fbcfa32dc5e1..b187d3e9ed44 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 d072720d10d3..4bd28fa8ba73 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 9ec6e4d00470..720ae9652d76 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/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 4e2c6f38cc31..bcc12a583980 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -74,6 +74,7 @@ use codex_login::auth::ExternalAuth; use codex_login::auth::ExternalAuthRefreshContext; use codex_login::auth::ExternalAuthRefreshReason; use codex_login::auth::ExternalAuthTokens; +use codex_login::default_client::Originator; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::W3cTraceContext; @@ -193,8 +194,7 @@ pub(crate) struct ConnectionSessionState { pub(crate) struct InitializedConnectionSessionState { pub(crate) experimental_api_enabled: bool, pub(crate) opted_out_notification_methods: HashSet, - pub(crate) app_server_client_name: String, - pub(crate) client_version: String, + pub(crate) originator: Originator, pub(crate) request_attestation: bool, } @@ -232,13 +232,21 @@ impl ConnectionSessionState { pub(crate) fn app_server_client_name(&self) -> Option<&str> { self.initialized .get() - .map(|session| session.app_server_client_name.as_str()) + .and_then(|session| session.originator.app_server_client()) + .map(codex_login::default_client::AppServerClient::name) } pub(crate) fn client_version(&self) -> Option<&str> { self.initialized .get() - .map(|session| session.client_version.as_str()) + .and_then(|session| session.originator.app_server_client()) + .map(codex_login::default_client::AppServerClient::version) + } + + pub(crate) fn originator(&self) -> Option { + self.initialized + .get() + .map(|session| session.originator.clone()) } pub(crate) fn request_attestation(&self) -> bool { @@ -802,8 +810,12 @@ impl MessageProcessor { ); let serialization_scope = codex_request.serialization_scope(); - let app_server_client_name = session.app_server_client_name().map(str::to_string); - let client_version = session.client_version().map(str::to_string); + let Some(originator) = session.originator() else { + return Err(crate::error_code::internal_error( + "initialized session is missing originator", + )); + }; + let request_context = request_context.with_originator(originator); let error_request_id = connection_request_id.clone(); let rpc_gate = Arc::clone(&session.rpc_gate); let processor = Arc::clone(self); @@ -817,8 +829,6 @@ impl MessageProcessor { connection_request_id, codex_request, request_context, - app_server_client_name, - client_version, ) .await; if let Err(error) = result { @@ -846,8 +856,6 @@ impl MessageProcessor { connection_request_id: ConnectionRequestId, codex_request: ClientRequest, request_context: RequestContext, - app_server_client_name: Option, - client_version: Option, ) -> Result<(), JSONRPCErrorError> { let connection_id = connection_request_id.connection_id; let request_id = ConnectionRequestId { @@ -876,7 +884,7 @@ impl MessageProcessor { .map(|response| Some(response.into())), ClientRequest::ExternalAgentConfigImport { params, .. } => self .external_agent_config_processor - .import(request_id.clone(), params) + .import(request_id.clone(), params, &request_context) .await .map(|()| None), ClientRequest::ConfigValueWrite { params, .. } => { @@ -887,7 +895,7 @@ impl MessageProcessor { } ClientRequest::ExperimentalFeatureEnablementSet { params, .. } => { self.config_processor - .experimental_feature_enablement_set(request_id.clone(), params) + .experimental_feature_enablement_set(&request_context, params) .await } ClientRequest::RemoteControlEnable { .. } => self @@ -962,13 +970,7 @@ impl MessageProcessor { .map(|response| Some(response.into())), ClientRequest::ThreadStart { params, .. } => { self.thread_processor - .thread_start( - request_id.clone(), - params, - app_server_client_name.clone(), - client_version.clone(), - request_context, - ) + .thread_start(request_id.clone(), params, request_context) .await } ClientRequest::ThreadUnsubscribe { params, .. } => { @@ -978,22 +980,12 @@ impl MessageProcessor { } ClientRequest::ThreadResume { params, .. } => { self.thread_processor - .thread_resume( - request_id.clone(), - params, - app_server_client_name.clone(), - client_version.clone(), - ) + .thread_resume(request_id.clone(), params, &request_context) .await } ClientRequest::ThreadFork { params, .. } => { self.thread_processor - .thread_fork( - request_id.clone(), - params, - app_server_client_name.clone(), - client_version.clone(), - ) + .thread_fork(request_id.clone(), params, &request_context) .await } ClientRequest::ThreadArchive { params, .. } => { @@ -1100,48 +1092,72 @@ impl MessageProcessor { self.marketplace_processor.marketplace_upgrade(params).await } ClientRequest::PluginList { params, .. } => { - self.plugin_processor.plugin_list(params).await + self.plugin_processor + .plugin_list(params, &request_context) + .await } ClientRequest::PluginInstalled { params, .. } => { - self.plugin_processor.plugin_installed(params).await + self.plugin_processor + .plugin_installed(params, &request_context) + .await } ClientRequest::PluginRead { params, .. } => { - self.plugin_processor.plugin_read(params).await + self.plugin_processor + .plugin_read(params, &request_context) + .await } ClientRequest::PluginSkillRead { params, .. } => { - self.plugin_processor.plugin_skill_read(params).await + self.plugin_processor + .plugin_skill_read(params, &request_context) + .await } ClientRequest::PluginShareSave { params, .. } => { - self.plugin_processor.plugin_share_save(params).await + self.plugin_processor + .plugin_share_save(params, &request_context) + .await } ClientRequest::PluginShareUpdateTargets { params, .. } => { self.plugin_processor - .plugin_share_update_targets(params) + .plugin_share_update_targets(params, &request_context) .await } ClientRequest::PluginShareList { params, .. } => { - self.plugin_processor.plugin_share_list(params).await + self.plugin_processor + .plugin_share_list(params, &request_context) + .await } ClientRequest::PluginShareCheckout { params, .. } => { - self.plugin_processor.plugin_share_checkout(params).await + self.plugin_processor + .plugin_share_checkout(params, &request_context) + .await } ClientRequest::PluginShareDelete { params, .. } => { - self.plugin_processor.plugin_share_delete(params).await + self.plugin_processor + .plugin_share_delete(params, &request_context) + .await } ClientRequest::AppsList { params, .. } => { - self.apps_processor.apps_list(&request_id, params).await + self.apps_processor + .apps_list(&request_context, params) + .await } ClientRequest::SkillsConfigWrite { params, .. } => { self.catalog_processor.skills_config_write(params).await } ClientRequest::PluginInstall { params, .. } => { - self.plugin_processor.plugin_install(params).await + self.plugin_processor + .plugin_install(params, &request_context) + .await } ClientRequest::PluginUninstall { params, .. } => { - self.plugin_processor.plugin_uninstall(params).await + self.plugin_processor + .plugin_uninstall(params, &request_context) + .await } ClientRequest::ModelList { params, .. } => { - self.catalog_processor.model_list(params).await + self.catalog_processor + .model_list(params, &request_context) + .await } ClientRequest::ExperimentalFeatureList { params, .. } => { self.catalog_processor @@ -1158,12 +1174,7 @@ impl MessageProcessor { } ClientRequest::TurnStart { params, .. } => { self.turn_processor - .turn_start( - request_id.clone(), - params, - app_server_client_name.clone(), - client_version.clone(), - ) + .turn_start(request_id.clone(), params, &request_context) .await } ClientRequest::ThreadInjectItems { params, .. } => { @@ -1201,7 +1212,9 @@ impl MessageProcessor { self.turn_processor.thread_realtime_list_voices().await } ClientRequest::ReviewStart { params, .. } => { - self.turn_processor.review_start(&request_id, params).await + self.turn_processor + .review_start(&request_id, params, &request_context) + .await } ClientRequest::McpServerOauthLogin { params, .. } => { self.mcp_processor.mcp_server_oauth_login(params).await diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 4d75a2058063..c29cfedf2940 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -5,6 +5,7 @@ use codex_app_server_protocol::ModelServiceTier; use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; +use codex_login::default_client::Originator; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; @@ -12,9 +13,10 @@ use codex_protocol::openai_models::ReasoningEffortPreset; pub async fn supported_models( thread_manager: Arc, include_hidden: bool, + originator: Option, ) -> Vec { thread_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, originator) .await .into_iter() .filter(|preset| include_hidden || preset.show_in_picker) diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 97ef37f74234..c5d782daca16 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -14,6 +14,7 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::ServerResponse; +use codex_login::default_client::Originator; use codex_otel::span_w3c_trace_context; use codex_protocol::ThreadId; use codex_protocol::protocol::W3cTraceContext; @@ -52,6 +53,7 @@ pub(crate) struct RequestContext { request_id: ConnectionRequestId, span: Span, parent_trace: Option, + originator: Originator, } impl RequestContext { @@ -64,9 +66,23 @@ impl RequestContext { request_id, span, parent_trace, + originator: Originator::process_default(), } } + pub(crate) fn with_originator(mut self, originator: Originator) -> Self { + self.originator = originator; + self + } + + pub(crate) fn request_id(&self) -> &ConnectionRequestId { + &self.request_id + } + + pub(crate) fn originator(&self) -> &Originator { + &self.originator + } + pub(crate) fn request_trace(&self) -> Option { span_w3c_trace_context(&self.span).or_else(|| self.parent_trace.clone()) } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 3e84c397f833..8b072703878c 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -331,6 +331,8 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::auth::login_with_chatgpt_auth_tokens; use codex_login::complete_device_code_login; +use codex_login::default_client::AppServerClient; +use codex_login::default_client::Originator; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; diff --git a/codex-rs/app-server/src/request_processors/apps_processor.rs b/codex-rs/app-server/src/request_processors/apps_processor.rs index caf60a91a014..72f9aeeabc65 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor.rs @@ -28,17 +28,17 @@ impl AppsRequestProcessor { pub(crate) async fn apps_list( &self, - request_id: &ConnectionRequestId, + request_context: &RequestContext, params: AppsListParams, ) -> Result, JSONRPCErrorError> { - self.apps_list_inner(request_id, params) + self.apps_list_inner(request_context, params) .await .map(|response| response.map(Into::into)) } async fn apps_list_inner( &self, - request_id: &ConnectionRequestId, + request_context: &RequestContext, params: AppsListParams, ) -> Result, JSONRPCErrorError> { let thread = if let Some(thread_id) = params.thread_id.as_deref() { @@ -80,11 +80,20 @@ impl AppsRequestProcessor { })); } - let request = request_id.clone(); + let request = request_context.request_id().clone(); let outgoing = Arc::clone(&self.outgoing); let environment_manager = self.thread_manager.environment_manager(); + let originator = request_context.originator().clone(); tokio::spawn(async move { - Self::apps_list_task(outgoing, request, params, config, environment_manager).await; + Self::apps_list_task( + outgoing, + request, + params, + config, + environment_manager, + originator, + ) + .await; }); Ok(None) } @@ -95,11 +104,15 @@ impl AppsRequestProcessor { params: AppsListParams, config: Config, environment_manager: Arc, + originator: Originator, ) { let retry_params = params.clone(); let retry_config = config.clone(); let retry_environment_manager = Arc::clone(&environment_manager); - let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await; + let retry_originator = originator.clone(); + let result = + Self::apps_list_response(&outgoing, params, config, environment_manager, originator) + .await; let should_retry = result .as_ref() .is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready); @@ -115,6 +128,7 @@ impl AppsRequestProcessor { retry_params, retry_config, retry_environment_manager, + retry_originator, ) .await { @@ -128,6 +142,7 @@ impl AppsRequestProcessor { params: AppsListParams, config: Config, environment_manager: Arc, + originator: Originator, ) -> Result<(AppsListResponse, bool), JSONRPCErrorError> { let AppsListParams { cursor, @@ -143,9 +158,13 @@ impl AppsRequestProcessor { None => 0, }; + let originator_value = originator.value().to_string(); let (mut accessible_connectors, mut all_connectors) = tokio::join!( - connectors::list_cached_accessible_connectors_from_mcp_tools(&config), - connectors::list_cached_all_connectors(&config) + connectors::list_cached_accessible_connectors_from_mcp_tools( + &config, + &originator_value + ), + connectors::list_cached_all_connectors_with_originator(&config, &originator) ); let cached_all_connectors = all_connectors.clone(); @@ -153,12 +172,14 @@ impl AppsRequestProcessor { let accessible_config = config.clone(); let accessible_tx = tx.clone(); + let accessible_originator_value = originator_value.clone(); tokio::spawn(async move { let result = connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( &accessible_config, force_refetch, &environment_manager, + &accessible_originator_value, ) .await .map_err(|err| format!("failed to load accessible apps: {err}")); @@ -166,10 +187,15 @@ impl AppsRequestProcessor { }); let all_config = config.clone(); + let all_originator = originator.clone(); tokio::spawn(async move { - let result = connectors::list_all_connectors_with_options(&all_config, force_refetch) - .await - .map_err(|err| format!("failed to list apps: {err}")); + let result = connectors::list_all_connectors_with_options_and_originator( + &all_config, + force_refetch, + &all_originator, + ) + .await + .map_err(|err| format!("failed to list apps: {err}")); let _ = tx.send(AppListLoadResult::Directory(result)); }); @@ -181,7 +207,11 @@ impl AppsRequestProcessor { if accessible_connectors.is_some() || all_connectors.is_some() { let merged = connectors::with_app_enabled_state( - merge_loaded_apps(all_connectors.as_deref(), accessible_connectors.as_deref()), + merge_loaded_apps( + all_connectors.as_deref(), + accessible_connectors.as_deref(), + &originator_value, + ), &config, ); if should_send_app_list_updated_notification( @@ -240,7 +270,11 @@ impl AppsRequestProcessor { accessible_connectors.as_deref() }; let merged = connectors::with_app_enabled_state( - merge_loaded_apps(all_connectors_for_update, accessible_connectors_for_update), + merge_loaded_apps( + all_connectors_for_update, + accessible_connectors_for_update, + &originator_value, + ), &config, ); if should_send_app_list_updated_notification( @@ -319,11 +353,17 @@ enum AppListLoadResult { fn merge_loaded_apps( all_connectors: Option<&[AppInfo]>, accessible_connectors: Option<&[AppInfo]>, + originator_value: &str, ) -> Vec { let all_connectors_loaded = all_connectors.is_some(); let all = all_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); let accessible = accessible_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); - connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded) + connectors::merge_connectors_with_accessible_for_originator( + all, + accessible, + all_connectors_loaded, + originator_value, + ) } fn should_send_app_list_updated_notification( diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 834f70654dc8..8f893ec5b236 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -140,10 +140,15 @@ impl CatalogRequestProcessor { pub(crate) async fn model_list( &self, params: ModelListParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - Self::list_models(self.thread_manager.clone(), params) - .await - .map(|response| Some(response.into())) + Self::list_models( + self.thread_manager.clone(), + params, + Some(request_context.originator().clone()), + ) + .await + .map(|response| Some(response.into())) } pub(crate) async fn experimental_feature_list( @@ -223,13 +228,15 @@ impl CatalogRequestProcessor { async fn list_models( thread_manager: Arc, params: ModelListParams, + originator: Option, ) -> Result { let ModelListParams { limit, cursor, include_hidden, } = params; - let models = supported_models(thread_manager, include_hidden.unwrap_or(false)).await; + let models = + supported_models(thread_manager, include_hidden.unwrap_or(false), originator).await; let total = models.len(); if total == 0 { diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 5054ea248d67..baed9ce2fbc4 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -4,8 +4,8 @@ use crate::config_manager::ConfigManager; use crate::config_manager_service::ConfigManagerError; use crate::error_code::internal_error; use crate::error_code::invalid_request; -use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; +use crate::outgoing_message::RequestContext; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::AppListUpdatedNotification; use codex_app_server_protocol::ClientResponsePayload; @@ -41,6 +41,7 @@ use codex_core::ThreadManager; use codex_features::canonical_feature_for_key; use codex_features::feature_for_key; use codex_login::AuthManager; +use codex_login::default_client::Originator; use codex_model_provider::create_model_provider; use codex_plugin::PluginId; use codex_protocol::config_types::WebSearchMode; @@ -145,21 +146,22 @@ impl ConfigRequestProcessor { pub(crate) async fn experimental_feature_enablement_set( &self, - request_id: ConnectionRequestId, + request_context: &RequestContext, params: ExperimentalFeatureEnablementSetParams, ) -> Result, JSONRPCErrorError> { + let originator = request_context.originator().clone(); let should_refresh_apps_list = params.enablement.get("apps").copied() == Some(true); let response = self .handle_config_mutation_result(self.set_experimental_feature_enablement(params).await) .await?; self.outgoing .send_response_as( - request_id, + request_context.request_id().clone(), ClientResponsePayload::ExperimentalFeatureEnablementSet(response), ) .await; if should_refresh_apps_list { - self.refresh_apps_list_after_experimental_feature_enablement_set() + self.refresh_apps_list_after_experimental_feature_enablement_set(originator) .await; } Ok(None) @@ -192,7 +194,10 @@ impl ConfigRequestProcessor { Ok(response) } - async fn refresh_apps_list_after_experimental_feature_enablement_set(&self) { + async fn refresh_apps_list_after_experimental_feature_enablement_set( + &self, + originator: Originator, + ) { let config = match self.load_latest_config(/*fallback_cwd*/ None).await { Ok(config) => config, Err(error) => { @@ -214,12 +219,18 @@ impl ConfigRequestProcessor { let outgoing = Arc::clone(&self.outgoing); let environment_manager = self.thread_manager.environment_manager(); tokio::spawn(async move { + let originator_value = originator.value().to_string(); let (all_connectors_result, accessible_connectors_result) = tokio::join!( - connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true), + connectors::list_all_connectors_with_options_and_originator( + &config, + /*force_refetch*/ true, + &originator, + ), connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( &config, /*force_refetch*/ true, &environment_manager, + &originator_value, ), ); let all_connectors = match all_connectors_result { @@ -242,10 +253,11 @@ impl ConfigRequestProcessor { }; let data = connectors::with_app_enabled_state( - connectors::merge_connectors_with_accessible( + connectors::merge_connectors_with_accessible_for_originator( all_connectors, accessible_connectors, /*all_connectors_loaded*/ true, + &originator_value, ), &config, ); diff --git a/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs index 21b7da679b51..1d8e34c592e6 100644 --- a/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs +++ b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs @@ -11,6 +11,7 @@ use crate::error_code::internal_error; use crate::error_code::invalid_params; use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; +use crate::outgoing_message::RequestContext; use codex_app_server_protocol::CommandMigration; use codex_app_server_protocol::ExternalAgentConfigDetectParams; use codex_app_server_protocol::ExternalAgentConfigDetectResponse; @@ -34,6 +35,7 @@ use codex_external_agent_sessions::ImportedExternalAgentSession; use codex_external_agent_sessions::PendingSessionImport; use codex_external_agent_sessions::prepare_validated_session_imports; use codex_external_agent_sessions::record_imported_session; +use codex_login::default_client::Originator; use codex_protocol::ThreadId; use codex_protocol::protocol::InitialHistory; use codex_thread_store::ThreadMetadataPatch; @@ -174,6 +176,7 @@ impl ExternalAgentConfigRequestProcessor { &self, request_id: ConnectionRequestId, params: ExternalAgentConfigImportParams, + request_context: &RequestContext, ) -> Result<(), JSONRPCErrorError> { let needs_runtime_refresh = migration_items_need_runtime_refresh(¶ms.migration_items); let has_migration_items = !params.migration_items.is_empty(); @@ -212,6 +215,7 @@ impl ExternalAgentConfigRequestProcessor { let plugin_processor = self.clone(); let outgoing = Arc::clone(&self.outgoing); let thread_manager = Arc::clone(&self.thread_manager); + let originator = request_context.originator().clone(); tokio::spawn(async move { let session_imports = async move { if !pending_session_imports.is_empty() { @@ -223,7 +227,10 @@ impl ExternalAgentConfigRequestProcessor { .prepare_validated_session_imports(pending_session_imports); for pending_session_import in pending_session_imports { match session_processor - .import_external_agent_session(pending_session_import.session) + .import_external_agent_session( + pending_session_import.session, + originator.clone(), + ) .await { Ok(imported_thread_id) => { @@ -277,6 +284,7 @@ impl ExternalAgentConfigRequestProcessor { async fn import_external_agent_session( &self, session: ImportedExternalAgentSession, + originator: Originator, ) -> Result { let ImportedExternalAgentSession { cwd, @@ -313,6 +321,7 @@ impl ExternalAgentConfigRequestProcessor { metrics_service_name: None, parent_trace: None, environments, + originator, }) .await .map_err(|err| internal_error(format!("failed to import session: {err}")))?; 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 a40007db115a..d9f6e285f59c 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,14 @@ 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 +77,18 @@ 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, + originator: originator.clone(), request_attestation, }) .is_err() @@ -107,37 +96,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.clone(), 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/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 199ddf1191a9..af92c5ab39b5 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -296,8 +296,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_list( &self, params: PluginListParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_list_response(params) + self.plugin_list_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -305,8 +306,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_installed( &self, params: PluginInstalledParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_installed_response(params) + self.plugin_installed_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -314,8 +316,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_read( &self, params: PluginReadParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_read_response(params) + self.plugin_read_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -323,8 +326,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_skill_read( &self, params: PluginSkillReadParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_skill_read_response(params) + self.plugin_skill_read_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -332,8 +336,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_share_save( &self, params: PluginShareSaveParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_share_save_response(params) + self.plugin_share_save_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -341,8 +346,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_share_update_targets( &self, params: PluginShareUpdateTargetsParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_share_update_targets_response(params) + self.plugin_share_update_targets_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -350,8 +356,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_share_list( &self, params: PluginShareListParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_share_list_response(params) + self.plugin_share_list_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -359,8 +366,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_share_checkout( &self, params: PluginShareCheckoutParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_share_checkout_response(params) + self.plugin_share_checkout_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -368,8 +376,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_share_delete( &self, params: PluginShareDeleteParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_share_delete_response(params) + self.plugin_share_delete_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -377,8 +386,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_install( &self, params: PluginInstallParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_install_response(params) + self.plugin_install_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -386,8 +396,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_uninstall( &self, params: PluginUninstallParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.plugin_uninstall_response(params) + self.plugin_uninstall_response(params, request_context.originator()) .await .map(|response| Some(response.into())) } @@ -464,6 +475,7 @@ impl PluginRequestProcessor { async fn plugin_list_response( &self, params: PluginListParams, + originator: &Originator, ) -> Result { let plugins_manager = self.thread_manager.plugins_manager(); let PluginListParams { @@ -583,6 +595,7 @@ impl PluginRequestProcessor { match codex_core_plugins::remote::fetch_remote_marketplaces( &remote_plugin_service_config, auth.as_ref(), + originator, &remote_sources, ) .await @@ -661,6 +674,7 @@ impl PluginRequestProcessor { async fn plugin_installed_response( &self, params: PluginInstalledParams, + originator: &Originator, ) -> Result { let plugins_manager = self.thread_manager.plugins_manager(); let PluginInstalledParams { @@ -714,6 +728,7 @@ impl PluginRequestProcessor { &plugins_input, &remote_installed_plugin_visible_scopes, auth.as_ref(), + originator, ) .await, ); @@ -810,6 +825,7 @@ impl PluginRequestProcessor { plugins_input: &codex_core_plugins::PluginsConfigInput, visible_scopes: &[RemotePluginScope], auth: Option<&CodexAuth>, + originator: &Originator, ) -> Vec { let remote_marketplaces = if let Some(remote_marketplaces) = plugins_manager.build_remote_installed_plugin_marketplaces_from_cache(visible_scopes) @@ -820,6 +836,7 @@ impl PluginRequestProcessor { .build_and_cache_remote_installed_plugin_marketplaces( plugins_input, auth, + originator, visible_scopes, Some(self.effective_plugins_changed_callback()), ) @@ -848,6 +865,7 @@ impl PluginRequestProcessor { async fn plugin_read_response( &self, params: PluginReadParams, + originator: &Originator, ) -> Result { let plugins_manager = self.thread_manager.plugins_manager(); let PluginReadParams { @@ -896,6 +914,7 @@ impl PluginRequestProcessor { match codex_core_plugins::remote::fetch_remote_plugin_share_context( &remote_plugin_service_config, auth.as_ref(), + originator, &context.remote_plugin_id, ) .await @@ -936,9 +955,13 @@ impl PluginRequestProcessor { None => None, }; let environment_manager = self.thread_manager.environment_manager(); - let app_summaries = - load_plugin_app_summaries(&config, &outcome.plugin.apps, &environment_manager) - .await; + let app_summaries = load_plugin_app_summaries( + &config, + &outcome.plugin.apps, + &environment_manager, + originator, + ) + .await; let visible_skills = outcome .plugin .skills @@ -1000,6 +1023,7 @@ impl PluginRequestProcessor { let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail( &remote_plugin_service_config, auth.as_ref(), + originator, &remote_marketplace_name, &plugin_name, ) @@ -1014,8 +1038,13 @@ impl PluginRequestProcessor { .map(codex_plugin::AppConnectorId) .collect::>(); let environment_manager = self.thread_manager.environment_manager(); - let app_summaries = - load_plugin_app_summaries(&config, &plugin_apps, &environment_manager).await; + let app_summaries = load_plugin_app_summaries( + &config, + &plugin_apps, + &environment_manager, + originator, + ) + .await; remote_plugin_detail_to_info(remote_detail, app_summaries) } }; @@ -1026,6 +1055,7 @@ impl PluginRequestProcessor { async fn plugin_skill_read_response( &self, params: PluginSkillReadParams, + originator: &Originator, ) -> Result { let PluginSkillReadParams { remote_marketplace_name, @@ -1053,6 +1083,7 @@ impl PluginRequestProcessor { let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail( &remote_plugin_service_config, auth.as_ref(), + originator, &remote_marketplace_name, &remote_plugin_id, &skill_name, @@ -1070,6 +1101,7 @@ impl PluginRequestProcessor { async fn plugin_share_save_response( &self, params: PluginShareSaveParams, + originator: &Originator, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; if !config.features.enabled(Feature::PluginSharing) { @@ -1110,6 +1142,7 @@ impl PluginRequestProcessor { let result = codex_core_plugins::remote::save_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), + originator, config.codex_home.as_path(), &plugin_path, remote_plugin_id.as_deref(), @@ -1128,6 +1161,7 @@ impl PluginRequestProcessor { async fn plugin_share_update_targets_response( &self, params: PluginShareUpdateTargetsParams, + originator: &Originator, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; if !config.features.enabled(Feature::PluginSharing) { @@ -1149,6 +1183,7 @@ impl PluginRequestProcessor { let result = codex_core_plugins::remote::update_remote_plugin_share_targets( &remote_plugin_service_config, auth.as_ref(), + originator, &remote_plugin_id, remote_plugin_share_targets(share_targets), remote_plugin_share_update_discoverability(discoverability), @@ -1171,6 +1206,7 @@ impl PluginRequestProcessor { async fn plugin_share_list_response( &self, _params: PluginShareListParams, + originator: &Originator, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; let remote_plugin_service_config = RemotePluginServiceConfig { @@ -1179,6 +1215,7 @@ impl PluginRequestProcessor { let data = codex_core_plugins::remote::list_remote_plugin_shares( &remote_plugin_service_config, auth.as_ref(), + originator, config.codex_home.as_path(), ) .await @@ -1202,6 +1239,7 @@ impl PluginRequestProcessor { async fn plugin_share_checkout_response( &self, params: PluginShareCheckoutParams, + originator: &Originator, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; if !config.features.enabled(Feature::PluginSharing) { @@ -1218,6 +1256,7 @@ impl PluginRequestProcessor { let result = codex_core_plugins::remote::checkout_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), + originator, config.codex_home.as_path(), &remote_plugin_id, ) @@ -1238,6 +1277,7 @@ impl PluginRequestProcessor { async fn plugin_share_delete_response( &self, params: PluginShareDeleteParams, + originator: &Originator, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; let PluginShareDeleteParams { remote_plugin_id } = params; @@ -1251,6 +1291,7 @@ impl PluginRequestProcessor { codex_core_plugins::remote::delete_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), + originator, config.codex_home.as_path(), &remote_plugin_id, ) @@ -1274,6 +1315,7 @@ impl PluginRequestProcessor { async fn plugin_install_response( &self, params: PluginInstallParams, + originator: &Originator, ) -> Result { let PluginInstallParams { marketplace_path, @@ -1284,7 +1326,11 @@ impl PluginRequestProcessor { (Some(marketplace_path), None) => marketplace_path, (None, Some(remote_marketplace_name)) => { return self - .remote_plugin_install_response(remote_marketplace_name, plugin_name) + .remote_plugin_install_response( + remote_marketplace_name, + plugin_name, + originator, + ) .await; } (Some(_), Some(_)) | (None, None) => { @@ -1342,6 +1388,7 @@ impl PluginRequestProcessor { auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth), &result.plugin_id.as_key(), &plugin_apps, + originator, ) .await; @@ -1355,6 +1402,7 @@ impl PluginRequestProcessor { &self, remote_marketplace_name: String, remote_plugin_id: String, + originator: &Originator, ) -> Result { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if !config.features.enabled(Feature::Plugins) { @@ -1372,6 +1420,7 @@ impl PluginRequestProcessor { codex_core_plugins::remote::fetch_remote_plugin_detail_with_download_urls( &remote_plugin_service_config, auth.as_ref(), + originator, &remote_marketplace_name, &remote_plugin_id, ) @@ -1423,6 +1472,7 @@ impl PluginRequestProcessor { codex_core_plugins::remote::install_remote_plugin( &remote_plugin_service_config, auth.as_ref(), + originator, &actual_remote_marketplace_name, &remote_plugin_id, ) @@ -1456,6 +1506,7 @@ impl PluginRequestProcessor { auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth), &result.plugin_id.as_key(), &plugin_apps, + originator, ) .await; @@ -1471,18 +1522,23 @@ impl PluginRequestProcessor { is_chatgpt_auth: bool, plugin_id: &str, plugin_apps: &[codex_plugin::AppConnectorId], + originator: &Originator, ) -> Vec { if plugin_apps.is_empty() || !config.features.apps_enabled_for_auth(is_chatgpt_auth) { return Vec::new(); } let environment_manager = self.thread_manager.environment_manager(); + let originator_value = originator.value().to_string(); let (all_connectors_result, accessible_connectors_result) = tokio::join!( - connectors::list_all_connectors_with_options(config, /*force_refetch*/ true), + connectors::list_all_connectors_with_options_and_originator( + config, /*force_refetch*/ true, originator, + ), connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( config, /*force_refetch*/ true, - &environment_manager + &environment_manager, + &originator_value ), ); @@ -1493,12 +1549,16 @@ impl PluginRequestProcessor { plugin = plugin_id, "failed to load app metadata after plugin install: {err:#}" ); - connectors::list_cached_all_connectors(config) + connectors::list_cached_all_connectors_with_originator(config, originator) .await .unwrap_or_default() } }; - let all_connectors = connectors::connectors_for_plugin_apps(all_connectors, plugin_apps); + let all_connectors = connectors::connectors_for_plugin_apps_for_originator( + all_connectors, + plugin_apps, + &originator_value, + ); let (accessible_connectors, codex_apps_ready) = match accessible_connectors_result { Ok(status) => (status.connectors, status.codex_apps_ready), Err(err) => { @@ -1507,9 +1567,12 @@ impl PluginRequestProcessor { "failed to load accessible apps after plugin install: {err:#}" ); ( - connectors::list_cached_accessible_connectors_from_mcp_tools(config) - .await - .unwrap_or_default(), + connectors::list_cached_accessible_connectors_from_mcp_tools( + config, + &originator_value, + ) + .await + .unwrap_or_default(), false, ) } @@ -1613,6 +1676,7 @@ impl PluginRequestProcessor { async fn plugin_uninstall_response( &self, params: PluginUninstallParams, + originator: &Originator, ) -> Result { let PluginUninstallParams { plugin_id } = params; if codex_plugin::PluginId::parse(&plugin_id).is_err() @@ -1621,7 +1685,9 @@ impl PluginRequestProcessor { return Err(invalid_request("invalid remote plugin id")); } if is_valid_remote_plugin_id(&plugin_id) { - return self.remote_plugin_uninstall_response(plugin_id).await; + return self + .remote_plugin_uninstall_response(plugin_id, originator) + .await; } let plugins_manager = self.thread_manager.plugins_manager(); @@ -1704,6 +1770,7 @@ impl PluginRequestProcessor { async fn remote_plugin_uninstall_response( &self, plugin_id: String, + originator: &Originator, ) -> Result { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if !config.features.enabled(Feature::Plugins) { @@ -1718,6 +1785,7 @@ impl PluginRequestProcessor { let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin( &remote_plugin_service_config, auth.as_ref(), + originator, config.codex_home.to_path_buf(), &plugin_id, ) @@ -1749,29 +1817,38 @@ async fn load_plugin_app_summaries( config: &Config, plugin_apps: &[codex_plugin::AppConnectorId], environment_manager: &EnvironmentManager, + originator: &Originator, ) -> Vec { if plugin_apps.is_empty() { return Vec::new(); } - let connectors = - match connectors::list_all_connectors_with_options(config, /*force_refetch*/ false).await { - Ok(connectors) => connectors, - Err(err) => { - warn!("failed to load app metadata for plugin/read: {err:#}"); - connectors::list_cached_all_connectors(config) - .await - .unwrap_or_default() - } - }; - - let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps); + let originator_value = originator.value().to_string(); + let connectors = match connectors::list_all_connectors_with_options_and_originator( + config, /*force_refetch*/ false, originator, + ) + .await + { + Ok(connectors) => connectors, + Err(err) => { + warn!("failed to load app metadata for plugin/read: {err:#}"); + connectors::list_cached_all_connectors_with_originator(config, originator) + .await + .unwrap_or_default() + } + }; + let plugin_connectors = connectors::connectors_for_plugin_apps_for_originator( + connectors, + plugin_apps, + &originator_value, + ); let accessible_connectors = match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( config, /*force_refetch*/ false, environment_manager, + &originator_value, ) .await { diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index dd2dd6a57e35..c9d36a8c2e78 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -382,19 +382,11 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadStartParams, - app_server_client_name: Option, - app_server_client_version: Option, request_context: RequestContext, ) -> Result, JSONRPCErrorError> { - self.thread_start_inner( - request_id, - params, - app_server_client_name, - app_server_client_version, - request_context, - ) - .await - .map(|()| None) + self.thread_start_inner(request_id, params, request_context) + .await + .map(|()| None) } pub(crate) async fn thread_unsubscribe( @@ -411,34 +403,22 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadResumeParams, - app_server_client_name: Option, - app_server_client_version: Option, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.thread_resume_inner( - request_id, - params, - app_server_client_name, - app_server_client_version, - ) - .await - .map(|()| None) + self.thread_resume_inner(request_id, params, request_context.originator().clone()) + .await + .map(|()| None) } pub(crate) async fn thread_fork( &self, request_id: ConnectionRequestId, params: ThreadForkParams, - app_server_client_name: Option, - app_server_client_version: Option, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.thread_fork_inner( - request_id, - params, - app_server_client_name, - app_server_client_version, - ) - .await - .map(|()| None) + self.thread_fork_inner(request_id, params, request_context.originator().clone()) + .await + .map(|()| None) } pub(crate) async fn thread_archive( @@ -689,19 +669,15 @@ impl ThreadRequestProcessor { async fn set_app_server_client_info( thread: &CodexThread, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result<(), JSONRPCErrorError> { + let app_server_client = originator.app_server_client(); let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny( - app_server_client_name.as_deref(), - app_server_client_version.as_deref(), + app_server_client.map(AppServerClient::name), + app_server_client.map(AppServerClient::version), ); thread - .set_app_server_client_info( - app_server_client_name, - app_server_client_version, - mcp_elicitations_auto_deny, - ) + .set_app_server_client_info(originator, mcp_elicitations_auto_deny) .await .map_err(|err| internal_error(format!("failed to set app server client info: {err}"))) } @@ -814,8 +790,6 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadStartParams, - app_server_client_name: Option, - app_server_client_version: Option, request_context: RequestContext, ) -> Result<(), JSONRPCErrorError> { let ThreadStartParams { @@ -879,16 +853,16 @@ impl ThreadRequestProcessor { skills_watcher: Arc::clone(&self.skills_watcher), }; let request_trace = request_context.request_trace(); + let originator = request_context.originator().clone(); let config_manager = self.config_manager.clone(); let outgoing = Arc::clone(&listener_task_context.outgoing); let error_request_id = request_id.clone(); - let thread_start_task = async move { + let thread_start_task = Box::pin(async move { if let Err(error) = Self::thread_start_task( listener_task_context, config_manager, request_id, - app_server_client_name, - app_server_client_version, + originator, config, typesafe_overrides, dynamic_tools, @@ -903,7 +877,7 @@ impl ThreadRequestProcessor { { outgoing.send_error(error_request_id, error).await; } - }; + }); self.background_tasks .spawn(thread_start_task.instrument(request_context.span())); Ok(()) @@ -971,8 +945,7 @@ impl ThreadRequestProcessor { listener_task_context: ListenerTaskContext, config_manager: ConfigManager, request_id: ConnectionRequestId, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, config_overrides: Option>, typesafe_overrides: ConfigOverrides, dynamic_tools: Option>, @@ -1101,6 +1074,7 @@ impl ThreadRequestProcessor { metrics_service_name: service_name, parent_trace: request_trace, environments, + originator: originator.clone(), }) .instrument(tracing::info_span!( "app_server.thread_start.create_thread", @@ -1120,12 +1094,7 @@ impl ThreadRequestProcessor { Some("ready"), ); - Self::set_app_server_client_info( - thread.as_ref(), - app_server_client_name, - app_server_client_version, - ) - .await?; + Self::set_app_server_client_info(thread.as_ref(), originator).await?; let config_snapshot = thread .config_snapshot() @@ -2304,8 +2273,7 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadResumeParams, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result<(), JSONRPCErrorError> { if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id) && self @@ -2338,6 +2306,9 @@ impl ThreadRequestProcessor { self.send_persist_extended_history_deprecation_notice(request_id.connection_id) .await; } + let app_server_client_name = originator + .app_server_client() + .map(|client| client.name().to_string()); let redact_resume_payloads = should_redact_thread_resume_payloads(app_server_client_name.as_deref()); @@ -2349,12 +2320,7 @@ impl ThreadRequestProcessor { } }; match self - .resume_running_thread( - &request_id, - ¶ms, - app_server_client_name.clone(), - app_server_client_version.clone(), - ) + .resume_running_thread(&request_id, ¶ms, originator.clone()) .await { Ok(true) => return Ok(()), @@ -2450,6 +2416,7 @@ impl ThreadRequestProcessor { self.auth_manager.clone(), /*persist_extended_history*/ false, self.request_trace_context(&request_id).await, + originator.clone(), ) .await { @@ -2459,12 +2426,8 @@ impl ThreadRequestProcessor { session_configured, .. }) => { - if let Err(err) = Self::set_app_server_client_info( - codex_thread.as_ref(), - app_server_client_name, - app_server_client_version, - ) - .await + if let Err(err) = + Self::set_app_server_client_info(codex_thread.as_ref(), originator).await { self.outgoing.send_error(request_id, err).await; return Ok(()); @@ -2613,8 +2576,7 @@ impl ThreadRequestProcessor { &self, request_id: &ConnectionRequestId, params: &ThreadResumeParams, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result { let running_thread = if params.history.is_some() { if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id) @@ -2676,6 +2638,9 @@ impl ThreadRequestProcessor { }; if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread { + let app_server_client_name = originator + .app_server_client() + .map(|client| client.name().to_string()); let redact_resume_payloads = should_redact_thread_resume_payloads(app_server_client_name.as_deref()); let history_items = source_thread @@ -2698,12 +2663,7 @@ impl ThreadRequestProcessor { thread_state.clone(), ) .await?; - Self::set_app_server_client_info( - existing_thread.as_ref(), - app_server_client_name, - app_server_client_version, - ) - .await?; + Self::set_app_server_client_info(existing_thread.as_ref(), originator).await?; let config_snapshot = existing_thread.config_snapshot().await; let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot); @@ -3001,8 +2961,7 @@ impl ThreadRequestProcessor { &self, request_id: ConnectionRequestId, params: ThreadForkParams, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result<(), JSONRPCErrorError> { let ThreadForkParams { thread_id, @@ -3115,6 +3074,7 @@ impl ThreadRequestProcessor { thread_source.map(Into::into), /*persist_extended_history*/ false, self.request_trace_context(&request_id).await, + originator.clone(), ) .await .map_err(|err| match err { @@ -3125,12 +3085,7 @@ impl ThreadRequestProcessor { err => internal_error(format!("error forking thread: {err}")), })?; - Self::set_app_server_client_info( - forked_thread.as_ref(), - app_server_client_name, - app_server_client_version, - ) - .await?; + Self::set_app_server_client_info(forked_thread.as_ref(), originator).await?; // Auto-attach a conversation listener when forking a thread. log_listener_attach_result( diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 71715e5079b5..e0005631f6fc 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -66,17 +66,11 @@ impl TurnRequestProcessor { &self, request_id: ConnectionRequestId, params: TurnStartParams, - app_server_client_name: Option, - app_server_client_version: Option, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.turn_start_inner( - request_id, - params, - app_server_client_name, - app_server_client_version, - ) - .await - .map(|response| Some(response.into())) + self.turn_start_inner(request_id, params, request_context.originator().clone()) + .await + .map(|response| Some(response.into())) } pub(crate) async fn thread_inject_items( @@ -163,8 +157,10 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ReviewStartParams, + request_context: &RequestContext, ) -> Result, JSONRPCErrorError> { - self.review_start_inner(request_id, params) + let originator = request_context.originator().clone(); + self.review_start_inner(request_id, params, originator) .await .map(|()| None) } @@ -330,8 +326,7 @@ impl TurnRequestProcessor { &self, request_id: ConnectionRequestId, params: TurnStartParams, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result { if let Err(error) = Self::validate_v2_input_limit(¶ms.input) { self.track_error_response( @@ -347,15 +342,11 @@ impl TurnRequestProcessor { .inspect_err(|error| { self.track_error_response(&request_id, error, /*error_type*/ None); })?; - Self::set_app_server_client_info( - thread.as_ref(), - app_server_client_name, - app_server_client_version, - ) - .await - .inspect_err(|error| { - self.track_error_response(&request_id, error, /*error_type*/ None); - })?; + Self::set_app_server_client_info(thread.as_ref(), originator) + .await + .inspect_err(|error| { + self.track_error_response(&request_id, error, /*error_type*/ None); + })?; let collaboration_mode = params .collaboration_mode @@ -595,19 +586,15 @@ impl TurnRequestProcessor { async fn set_app_server_client_info( thread: &CodexThread, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, ) -> Result<(), JSONRPCErrorError> { + let app_server_client = originator.app_server_client(); let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny( - app_server_client_name.as_deref(), - app_server_client_version.as_deref(), + app_server_client.map(AppServerClient::name), + app_server_client.map(AppServerClient::version), ); thread - .set_app_server_client_info( - app_server_client_name, - app_server_client_version, - mcp_elicitations_auto_deny, - ) + .set_app_server_client_info(originator, mcp_elicitations_auto_deny) .await .map_err(|err| internal_error(format!("failed to set app server client info: {err}"))) } @@ -922,6 +909,7 @@ impl TurnRequestProcessor { parent_thread: Arc, review_request: ReviewRequest, display_text: &str, + originator: Originator, ) -> std::result::Result<(), JSONRPCErrorError> { parent_thread.ensure_rollout_materialized().await; parent_thread.flush_rollout().await.map_err(|err| { @@ -960,6 +948,7 @@ impl TurnRequestProcessor { /*thread_source*/ None, /*persist_extended_history*/ false, self.request_trace_context(request_id).await, + originator, ) .await .map_err(|err| { @@ -1031,6 +1020,7 @@ impl TurnRequestProcessor { &self, request_id: &ConnectionRequestId, params: ReviewStartParams, + originator: Originator, ) -> Result<(), JSONRPCErrorError> { let ReviewStartParams { thread_id, @@ -1058,6 +1048,7 @@ impl TurnRequestProcessor { parent_thread, review_request, &display_text, + originator, ) .await?; } diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs index cf76b9d57365..4191cac7f1c7 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs @@ -2,12 +2,14 @@ use anyhow::Context; use anyhow::Result; use anyhow::bail; use app_test_support::DISABLE_PLUGIN_STARTUP_TASKS_ARG; +use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::InitializeParams; +use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCNotification; @@ -18,6 +20,9 @@ use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; use futures::SinkExt; use futures::StreamExt; use hmac::Hmac; @@ -104,6 +109,58 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Ok(()) } +#[tokio::test] +async fn websocket_transport_originator_is_connection_scoped_across_clients() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), "never")?; + + let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?; + + let mut backend_ws = connect_websocket(bind_addr).await?; + send_initialize_request(&mut backend_ws, /*id*/ 1, "codex-backend").await?; + let backend_init = read_response_for_id(&mut backend_ws, /*id*/ 1).await?; + let backend_init = to_response::(backend_init)?; + assert!(backend_init.user_agent.starts_with("codex-backend/")); + + let mut client_ws = connect_websocket(bind_addr).await?; + send_initialize_request(&mut client_ws, /*id*/ 2, "codex_ios").await?; + let client_init = read_response_for_id(&mut client_ws, /*id*/ 2).await?; + let client_init = to_response::(client_init)?; + assert!(client_init.user_agent.starts_with("codex_ios/")); + + let thread_id = start_thread(&mut client_ws, /*id*/ 3).await?; + send_turn_start_request(&mut client_ws, /*id*/ 4, thread_id).await?; + let turn_response = read_response_for_id(&mut client_ws, /*id*/ 4).await?; + let _: TurnStartResponse = to_response(turn_response)?; + read_notification_for_method(&mut client_ws, "turn/completed").await?; + + let requests = server + .received_requests() + .await + .context("failed to fetch received requests")?; + assert!(!requests.is_empty()); + for request in requests { + let originator = request + .headers + .get("originator") + .context("originator header missing")?; + assert_eq!(originator.to_str()?, "codex_ios"); + let user_agent = request + .headers + .get("user-agent") + .context("user-agent header missing")?; + assert!(user_agent.to_str()?.starts_with("codex_ios/")); + } + + process + .kill() + .await + .context("failed to stop websocket app-server process")?; + Ok(()) +} + #[tokio::test] async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; @@ -625,6 +682,23 @@ async fn start_thread(stream: &mut WsClient, id: i64) -> Result { Ok(thread.id) } +async fn send_turn_start_request(stream: &mut WsClient, id: i64, thread_id: String) -> Result<()> { + send_request( + stream, + "turn/start", + id, + Some(serde_json::to_value(TurnStartParams { + thread_id, + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + })?), + ) + .await +} + async fn assert_loaded_threads(stream: &mut WsClient, id: i64, expected: &[&str]) -> Result<()> { let response = request_loaded_threads(stream, id).await?; let mut actual = response.data; diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 47cacdb20e22..0f76e8ecda70 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,39 +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<()> { - let responses = Vec::new(); - let server = create_mock_responses_server_sequence_unchecked(responses).await; - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; - - let message = timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: "codex-backend".to_string(), - title: Some("Codex Backend".to_string()), - version: "0.1.0".to_string(), - }), - ) - .await??; - - let JSONRPCMessage::Response(response) = message else { - anyhow::bail!("expected initialize response, got {message:?}"); - }; - let InitializeResponse { user_agent, .. } = to_response::(response)?; - - assert!(user_agent.starts_with("codex_cli_rs/")); - 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 +125,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 6365d527ed5b..ff0a379f1fed 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 372f62e6966d..15178fd31bfb 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; @@ -21,6 +22,16 @@ pub(crate) async fn chatgpt_get_request_with_timeout( config: &Config, path: String, timeout: Option, +) -> anyhow::Result { + let originator = Originator::process_default(); + chatgpt_get_request_with_timeout_and_originator(config, path, timeout, &originator).await +} + +pub(crate) async fn chatgpt_get_request_with_timeout_and_originator( + config: &Config, + path: String, + timeout: Option, + originator: &Originator, ) -> anyhow::Result { let chatgpt_base_url = &config.chatgpt_base_url; let auth_manager = @@ -39,7 +50,7 @@ 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 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 2e54192e8558..449735da7830 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use crate::chatgpt_client::chatgpt_get_request_with_timeout; +use crate::chatgpt_client::chatgpt_get_request_with_timeout_and_originator; use codex_app_server_protocol::AppInfo; use codex_connectors::ConnectorDirectoryCacheContext; @@ -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); @@ -52,9 +52,11 @@ pub async fn list_connectors(config: &Config) -> anyhow::Result> { if !apps_enabled(config).await { return Ok(Vec::new()); } + let originator = Originator::process_default(); + let originator_value = originator.value().to_string(); let (connectors_result, accessible_result) = tokio::join!( - list_all_connectors(config), - list_accessible_connectors_from_mcp_tools(config), + list_all_connectors_with_originator(config, &originator), + list_accessible_connectors_from_mcp_tools(config, &originator_value), ); let connectors = connectors_result?; let accessible = accessible_result?; @@ -67,10 +69,24 @@ pub async fn list_connectors(config: &Config) -> anyhow::Result> { } pub async fn list_all_connectors(config: &Config) -> anyhow::Result> { - list_all_connectors_with_options(config, /*force_refetch*/ false).await + let originator = Originator::process_default(); + list_all_connectors_with_options_and_originator( + config, + /*force_refetch*/ false, + &originator, + ) + .await } pub async fn list_cached_all_connectors(config: &Config) -> Option> { + let originator = Originator::process_default(); + list_cached_all_connectors_with_originator(config, &originator).await +} + +pub async fn list_cached_all_connectors_with_originator( + config: &Config, + originator: &Originator, +) -> Option> { if !apps_enabled(config).await { return Some(Vec::new()); } @@ -85,15 +101,31 @@ 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(), - )) + Some(filter_disallowed_connectors(connectors, originator.value())) } pub async fn list_all_connectors_with_options( config: &Config, force_refetch: bool, +) -> anyhow::Result> { + let originator = Originator::process_default(); + list_all_connectors_with_options_and_originator(config, force_refetch, &originator).await +} + +pub async fn list_all_connectors_with_originator( + config: &Config, + originator: &Originator, +) -> anyhow::Result> { + list_all_connectors_with_options_and_originator( + config, /*force_refetch*/ false, originator, + ) + .await +} + +pub async fn list_all_connectors_with_options_and_originator( + config: &Config, + force_refetch: bool, + originator: &Originator, ) -> anyhow::Result> { if !apps_enabled(config).await { return Ok(Vec::new()); @@ -105,10 +137,11 @@ pub async fn list_all_connectors_with_options( auth.is_workspace_account(), force_refetch, |path| async move { - chatgpt_get_request_with_timeout::( + chatgpt_get_request_with_timeout_and_originator::( config, path, Some(DIRECTORY_CONNECTORS_TIMEOUT), + originator, ) .await }, @@ -121,10 +154,7 @@ 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(), - )) + Ok(filter_disallowed_connectors(connectors, originator.value())) } fn connector_directory_cache_context( @@ -153,6 +183,15 @@ async fn plugin_apps_for_config(config: &Config) -> Vec { pub fn connectors_for_plugin_apps( connectors: Vec, plugin_apps: &[AppConnectorId], +) -> Vec { + let originator = Originator::process_default(); + connectors_for_plugin_apps_for_originator(connectors, plugin_apps, originator.value()) +} + +pub fn connectors_for_plugin_apps_for_originator( + connectors: Vec, + plugin_apps: &[AppConnectorId], + originator_value: &str, ) -> Vec { let plugin_app_ids = plugin_apps .iter() @@ -165,7 +204,7 @@ pub fn connectors_for_plugin_apps( .iter() .map(|connector_id| connector_id.0.clone()), ); - filter_disallowed_connectors(connectors, originator().value.as_str()) + filter_disallowed_connectors(connectors, originator_value) .into_iter() .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) .collect() @@ -175,6 +214,21 @@ pub fn merge_connectors_with_accessible( connectors: Vec, accessible_connectors: Vec, all_connectors_loaded: bool, +) -> Vec { + let originator = Originator::process_default(); + merge_connectors_with_accessible_for_originator( + connectors, + accessible_connectors, + all_connectors_loaded, + originator.value(), + ) +} + +pub fn merge_connectors_with_accessible_for_originator( + connectors: Vec, + accessible_connectors: Vec, + all_connectors_loaded: bool, + originator_value: &str, ) -> Vec { let accessible_connectors = if all_connectors_loaded { let connector_ids: HashSet<&str> = connectors @@ -189,7 +243,7 @@ pub fn merge_connectors_with_accessible( accessible_connectors }; let merged = merge_connectors(connectors, accessible_connectors); - filter_disallowed_connectors(merged, originator().value.as_str()) + 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 aec57c1e82f8..9b676ec655bd 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/cli/src/main.rs b/codex-rs/cli/src/main.rs index 1b71ab15ae2e..73e2688aa4e1 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1750,7 +1750,7 @@ async fn run_debug_models_command( AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await; let models_manager = build_models_manager(&config, auth_manager); models_manager - .raw_model_catalog(RefreshStrategy::OnlineIfUncached) + .raw_model_catalog(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await }; diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index e8d6b545b50d..18ab6627051b 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 9a5056aa668b..fedb12e31b7e 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 e87ee82f3090..38c112bb05c0 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/manager.rs b/codex-rs/core-plugins/src/manager.rs index 56b8c18bdafc..344e2382fd77 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -59,6 +59,7 @@ use codex_core_skills::SkillMetadata; use codex_hooks::plugin_hook_declarations; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; @@ -614,12 +615,14 @@ impl PluginsManager { &self, config: &PluginsConfigInput, auth: Option<&CodexAuth>, + originator: &Originator, visible_scopes: &[RemotePluginScope], on_effective_plugins_changed: Option>, ) -> Result, RemotePluginCatalogError> { let plugins = crate::remote::fetch_remote_installed_plugins( &remote_plugin_service_config(config), auth, + originator, ) .await?; let marketplaces = @@ -1786,9 +1789,11 @@ impl PluginsManager { } }; + let originator = Originator::process_default(); let installed_plugins = crate::remote::fetch_remote_installed_plugins( &request.service_config, request.auth.as_ref(), + &originator, ) .await; match installed_plugins { diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 56427473a1f2..2d80e3abe5ff 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; @@ -483,6 +484,7 @@ struct RemotePluginMutationResponse { pub async fn fetch_remote_marketplaces( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, sources: &[RemoteMarketplaceSource], ) -> Result, RemotePluginCatalogError> { let auth = ensure_chatgpt_auth(auth)?; @@ -494,7 +496,15 @@ pub async fn fetch_remote_marketplaces( ) }); let workspace_installed_plugins = if needs_workspace_installed { - Some(fetch_installed_plugins_for_scope(config, auth, RemotePluginScope::Workspace).await?) + Some( + fetch_installed_plugins_for_scope( + config, + auth, + originator, + RemotePluginScope::Workspace, + ) + .await?, + ) } else { None }; @@ -504,8 +514,8 @@ pub async fn fetch_remote_marketplaces( RemoteMarketplaceSource::Global => { let scope = RemotePluginScope::Global; let (directory_plugins, installed_plugins) = tokio::try_join!( - fetch_directory_plugins_for_scope(config, auth, scope), - fetch_installed_plugins_for_scope(config, auth, scope), + fetch_directory_plugins_for_scope(config, auth, originator, scope), + fetch_installed_plugins_for_scope(config, auth, originator, scope), )?; if let Some(marketplace) = build_remote_marketplace( scope.marketplace_name(), @@ -520,7 +530,7 @@ pub async fn fetch_remote_marketplaces( RemoteMarketplaceSource::WorkspaceDirectory => { let scope = RemotePluginScope::Workspace; let directory_plugins = - fetch_directory_plugins_for_scope(config, auth, scope).await?; + fetch_directory_plugins_for_scope(config, auth, originator, scope).await?; if let Some(marketplace) = build_remote_marketplace( scope.marketplace_name(), scope.marketplace_display_name(), @@ -535,7 +545,8 @@ pub async fn fetch_remote_marketplaces( // The shared endpoint is the source of truth for plugins explicitly shared // with the user. Installed unlisted plugins that are not returned there are // link-installed and stay in the separate unlisted bucket. - let shared_plugins = fetch_shared_workspace_plugins(config, auth).await?; + let shared_plugins = + fetch_shared_workspace_plugins(config, auth, originator).await?; let shared_plugin_ids = shared_plugins .iter() .map(|plugin| plugin.id.clone()) @@ -646,16 +657,19 @@ fn build_remote_marketplace( pub(crate) async fn fetch_remote_installed_plugins( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, ) -> Result, RemotePluginCatalogError> { let auth = ensure_chatgpt_auth(auth)?; let global = async { let scope = RemotePluginScope::Global; - let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?; + let installed_plugins = + fetch_installed_plugins_for_scope(config, auth, originator, scope).await?; Ok::<_, RemotePluginCatalogError>((scope, installed_plugins)) }; let workspace = async { let scope = RemotePluginScope::Workspace; - let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?; + let installed_plugins = + fetch_installed_plugins_for_scope(config, auth, originator, scope).await?; Ok::<_, RemotePluginCatalogError>((scope, installed_plugins)) }; @@ -725,12 +739,14 @@ pub fn group_remote_installed_plugins_by_marketplaces( pub async fn fetch_remote_plugin_detail( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, marketplace_name: &str, plugin_id: &str, ) -> Result { fetch_remote_plugin_detail_with_download_url_option( config, auth, + originator, marketplace_name, plugin_id, /*include_download_urls*/ false, @@ -741,11 +757,12 @@ pub async fn fetch_remote_plugin_detail( pub async fn fetch_remote_plugin_share_context( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, plugin_id: &str, ) -> Result, RemotePluginCatalogError> { let auth = ensure_chatgpt_auth(auth)?; let plugin = fetch_plugin_detail( - config, auth, plugin_id, /*include_download_urls*/ false, + config, auth, originator, plugin_id, /*include_download_urls*/ false, ) .await?; remote_plugin_share_context(&plugin) @@ -754,12 +771,14 @@ pub async fn fetch_remote_plugin_share_context( pub async fn fetch_remote_plugin_detail_with_download_urls( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, marketplace_name: &str, plugin_id: &str, ) -> Result { fetch_remote_plugin_detail_with_download_url_option( config, auth, + originator, marketplace_name, plugin_id, /*include_download_urls*/ true, @@ -770,6 +789,7 @@ pub async fn fetch_remote_plugin_detail_with_download_urls( pub async fn fetch_remote_plugin_skill_detail( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, marketplace_name: &str, plugin_id: &str, skill_name: &str, @@ -782,7 +802,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_reqwest_client(originator); let request = authenticated_request(client.get(&url), auth)?; let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?; if response.plugin_id != plugin_id { @@ -806,30 +826,42 @@ pub async fn fetch_remote_plugin_skill_detail( async fn fetch_remote_plugin_detail_with_download_url_option( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, _marketplace_name: &str, plugin_id: &str, include_download_urls: bool, ) -> Result { let auth = ensure_chatgpt_auth(auth)?; - let plugin = fetch_plugin_detail(config, auth, plugin_id, include_download_urls).await?; + let plugin = + fetch_plugin_detail(config, auth, originator, plugin_id, include_download_urls).await?; let scope = plugin.scope; let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string(); // Remote plugin IDs uniquely identify remote plugins, so the caller-provided // marketplace name is not validated here. The backend detail response is the // source of truth for the plugin's actual scope/marketplace. - build_remote_plugin_detail(config, auth, scope, marketplace_name, plugin_id, plugin).await + build_remote_plugin_detail( + config, + auth, + originator, + scope, + marketplace_name, + plugin_id, + plugin, + ) + .await } async fn build_remote_plugin_detail( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, marketplace_name: String, plugin_id: &str, plugin: RemotePluginDirectoryItem, ) -> Result { - let installed_plugin = fetch_installed_plugins_for_scope(config, auth, scope) + let installed_plugin = fetch_installed_plugins_for_scope(config, auth, originator, scope) .await? .into_iter() .find(|installed_plugin| installed_plugin.plugin.id == plugin_id); @@ -874,6 +906,7 @@ async fn build_remote_plugin_detail( pub async fn install_remote_plugin( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, _marketplace_name: &str, plugin_id: &str, ) -> Result<(), RemotePluginCatalogError> { @@ -883,7 +916,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_reqwest_client(originator); let request = authenticated_request(client.post(&url), auth)?; let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { @@ -906,12 +939,13 @@ pub async fn install_remote_plugin( pub async fn uninstall_remote_plugin( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, codex_home: PathBuf, plugin_id: &str, ) -> Result<(), RemotePluginCatalogError> { let auth = ensure_chatgpt_auth(auth)?; let plugin = fetch_plugin_detail( - config, auth, plugin_id, /*include_download_urls*/ false, + config, auth, originator, plugin_id, /*include_download_urls*/ false, ) .await?; let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string(); @@ -919,7 +953,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_reqwest_client(originator); let request = authenticated_request(client.post(&url), auth)?; let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { @@ -1173,13 +1207,15 @@ fn normalize_remote_default_prompt(prompt: &str) -> Option> { async fn fetch_directory_plugins_for_scope( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, ) -> Result, RemotePluginCatalogError> { let mut plugins = Vec::new(); let mut page_token = None; loop { let response = - get_remote_plugin_list_page(config, auth, scope, page_token.as_deref()).await?; + get_remote_plugin_list_page(config, auth, originator, scope, page_token.as_deref()) + .await?; plugins.extend(response.plugins); let Some(next_page_token) = response.pagination.next_page_token else { break; @@ -1192,12 +1228,18 @@ async fn fetch_directory_plugins_for_scope( async fn fetch_shared_workspace_plugins( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, ) -> Result, RemotePluginCatalogError> { let mut plugins = Vec::new(); let mut page_token = None; loop { - let response = - get_remote_shared_workspace_plugins_page(config, auth, page_token.as_deref()).await?; + let response = get_remote_shared_workspace_plugins_page( + config, + auth, + originator, + page_token.as_deref(), + ) + .await?; plugins.extend(response.plugins); let Some(next_page_token) = response.pagination.next_page_token else { break; @@ -1210,10 +1252,11 @@ async fn fetch_shared_workspace_plugins( async fn fetch_installed_plugins_for_scope( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, ) -> Result, RemotePluginCatalogError> { fetch_installed_plugins_for_scope_with_download_url( - config, auth, scope, /*include_download_urls*/ false, + config, auth, originator, scope, /*include_download_urls*/ false, ) .await } @@ -1221,6 +1264,7 @@ async fn fetch_installed_plugins_for_scope( async fn fetch_installed_plugins_for_scope_with_download_url( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, include_download_urls: bool, ) -> Result, RemotePluginCatalogError> { @@ -1230,6 +1274,7 @@ async fn fetch_installed_plugins_for_scope_with_download_url( let response = get_remote_plugin_installed_page( config, auth, + originator, scope, page_token.as_deref(), include_download_urls, @@ -1247,12 +1292,13 @@ async fn fetch_installed_plugins_for_scope_with_download_url( async fn get_remote_plugin_list_page( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, page_token: Option<&str>, ) -> 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_reqwest_client(originator); 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)]); @@ -1265,11 +1311,12 @@ async fn get_remote_plugin_list_page( async fn get_remote_shared_workspace_plugins_page( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, page_token: Option<&str>, ) -> 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_reqwest_client(originator); 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 { @@ -1281,13 +1328,14 @@ async fn get_remote_shared_workspace_plugins_page( async fn get_remote_plugin_installed_page( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, scope: RemotePluginScope, page_token: Option<&str>, include_download_urls: bool, ) -> 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_reqwest_client(originator); let mut request = authenticated_request(client.get(&url), auth)?; request = request.query(&[("scope", scope.api_value())]); if include_download_urls { @@ -1302,12 +1350,13 @@ async fn get_remote_plugin_installed_page( async fn fetch_plugin_detail( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, plugin_id: &str, include_download_urls: bool, ) -> 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_reqwest_client(originator); 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/remote_installed_plugin_sync.rs b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs index 69cbfe25693f..731b8471ab51 100644 --- a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs +++ b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs @@ -13,6 +13,7 @@ use crate::store::PLUGINS_CACHE_DIR; use crate::store::PluginStore; use crate::store::PluginStoreError; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_plugin::PluginId; use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -128,10 +129,15 @@ pub async fn sync_remote_installed_plugin_bundles_once( auth: Option<&CodexAuth>, ) -> Result { let auth = ensure_chatgpt_auth(auth)?; + let originator = Originator::process_default(); let global = async { let scope = RemotePluginScope::Global; let installed_plugins = fetch_installed_plugins_for_scope_with_download_url( - config, auth, scope, /*include_download_urls*/ true, + config, + auth, + &originator, + scope, + /*include_download_urls*/ true, ) .await?; Ok::<_, RemotePluginCatalogError>((scope, installed_plugins)) @@ -139,7 +145,11 @@ pub async fn sync_remote_installed_plugin_bundles_once( let workspace = async { let scope = RemotePluginScope::Workspace; let installed_plugins = fetch_installed_plugins_for_scope_with_download_url( - config, auth, scope, /*include_download_urls*/ true, + config, + auth, + &originator, + scope, + /*include_download_urls*/ true, ) .await?; Ok::<_, RemotePluginCatalogError>((scope, installed_plugins)) diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index 9b5a2572a471..5e5bf2be22fd 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -1,5 +1,6 @@ use super::*; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_login::default_client::build_reqwest_client; use codex_utils_absolute_path::AbsolutePathBuf; use flate2::Compression; @@ -141,6 +142,7 @@ struct RemotePluginShareUpdateTargetsResponse { pub async fn save_remote_plugin_share( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, codex_home: &Path, plugin_path: &AbsolutePathBuf, remote_plugin_id: Option<&str>, @@ -158,6 +160,7 @@ pub async fn save_remote_plugin_share( let upload = create_workspace_plugin_upload( config, auth, + originator, &filename, archive_bytes.len(), remote_plugin_id, @@ -166,13 +169,14 @@ pub async fn save_remote_plugin_share( let etag = upload .etag .ok_or(RemotePluginCatalogError::MissingUploadEtag)?; - put_workspace_plugin_upload(&upload.upload_url, archive_bytes).await?; + put_workspace_plugin_upload(originator, &upload.upload_url, archive_bytes).await?; let share_targets = access_policy.share_targets; let share_targets = ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?; let response = finalize_workspace_plugin_upload( config, auth, + originator, remote_plugin_id, RemoteWorkspacePluginCreateRequest { file_id: upload.file_id, @@ -208,16 +212,17 @@ pub async fn save_remote_plugin_share( pub async fn list_remote_plugin_shares( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, codex_home: &Path, ) -> Result, RemotePluginCatalogError> { let auth = ensure_chatgpt_auth(auth)?; - let created_plugins = fetch_created_workspace_plugins(config, auth).await?; + let created_plugins = fetch_created_workspace_plugins(config, auth, originator).await?; if created_plugins.is_empty() { return Ok(Vec::new()); } let installed_by_id = - fetch_installed_plugins_for_scope(config, auth, RemotePluginScope::Workspace) + fetch_installed_plugins_for_scope(config, auth, originator, RemotePluginScope::Workspace) .await? .into_iter() .map(|plugin| (plugin.plugin.id.clone(), plugin)) @@ -276,13 +281,14 @@ pub fn load_plugin_share_remote_ids_by_local_path( pub async fn delete_remote_plugin_share( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, codex_home: &Path, remote_plugin_id: &str, ) -> Result<(), RemotePluginCatalogError> { 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_reqwest_client(originator); 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) { @@ -297,6 +303,7 @@ pub async fn delete_remote_plugin_share( pub async fn update_remote_plugin_share_targets( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, remote_plugin_id: &str, targets: Vec, discoverability: RemotePluginShareUpdateDiscoverability, @@ -315,7 +322,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_reqwest_client(originator); let request = authenticated_request(client.put(&url), auth)?.json( &RemotePluginShareUpdateTargetsRequest { discoverability, @@ -359,12 +366,14 @@ fn ensure_unlisted_workspace_target( async fn fetch_created_workspace_plugins( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, ) -> Result, RemotePluginCatalogError> { let mut plugins = Vec::new(); let mut page_token = None; loop { let response = - get_created_workspace_plugins_page(config, auth, page_token.as_deref()).await?; + get_created_workspace_plugins_page(config, auth, originator, page_token.as_deref()) + .await?; plugins.extend(response.plugins); let Some(next_page_token) = response.pagination.next_page_token else { break; @@ -377,11 +386,12 @@ async fn fetch_created_workspace_plugins( async fn get_created_workspace_plugins_page( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, page_token: Option<&str>, ) -> 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_reqwest_client(originator); 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 { @@ -393,13 +403,14 @@ async fn get_created_workspace_plugins_page( async fn create_workspace_plugin_upload( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, filename: &str, size_bytes: usize, remote_plugin_id: Option<&str>, ) -> 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_reqwest_client(originator); let request = authenticated_request(client.post(&url), auth)?.json( &RemoteWorkspacePluginUploadUrlRequest { filename, @@ -412,10 +423,11 @@ async fn create_workspace_plugin_upload( } async fn put_workspace_plugin_upload( + originator: &Originator, upload_url: &str, archive_bytes: Vec, ) -> Result<(), RemotePluginCatalogError> { - let client = build_reqwest_client(); + let client = build_reqwest_client(originator); let request = client .put(upload_url) .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) @@ -444,6 +456,7 @@ async fn put_workspace_plugin_upload( async fn finalize_workspace_plugin_upload( config: &RemotePluginServiceConfig, auth: &CodexAuth, + originator: &Originator, remote_plugin_id: Option<&str>, body: RemoteWorkspacePluginCreateRequest, ) -> Result { @@ -453,7 +466,7 @@ async fn finalize_workspace_plugin_upload( } else { format!("{base_url}/public/plugins/workspace") }; - let client = build_reqwest_client(); + let client = build_reqwest_client(originator); 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/share/checkout.rs b/codex-rs/core-plugins/src/remote/share/checkout.rs index 8aabccff8b1d..b23a03c15035 100644 --- a/codex-rs/core-plugins/src/remote/share/checkout.rs +++ b/codex-rs/core-plugins/src/remote/share/checkout.rs @@ -7,6 +7,7 @@ use super::local_paths; use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginInstallPolicy; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_plugin::PluginId; use codex_plugin::validate_plugin_segment; use codex_utils_absolute_path::AbsolutePathBuf; @@ -37,12 +38,14 @@ pub struct RemotePluginShareCheckoutResult { pub async fn checkout_remote_plugin_share( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, + originator: &Originator, codex_home: &Path, remote_plugin_id: &str, ) -> Result { let detail = super::super::fetch_remote_plugin_detail_with_download_urls( config, auth, + originator, REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME, remote_plugin_id, ) diff --git a/codex-rs/core-plugins/src/remote_bundle.rs b/codex-rs/core-plugins/src/remote_bundle.rs index ed7cd56a86c7..d530d02ad939 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 dcf9f79eb856..f7c3c17fe93f 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 f03aff93f632..2087a97e3ae9 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 1ca7cd0cb768..a126c4a1ff42 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/agent/control.rs b/codex-rs/core/src/agent/control.rs index 42981a5d0a21..62de8a1896c2 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -13,6 +13,7 @@ use crate::thread_manager::ResumeThreadWithHistoryOptions; use crate::thread_manager::ThreadManagerState; use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns; use codex_features::Feature; +use codex_login::default_client::Originator; use codex_protocol::AgentPath; use codex_protocol::SessionId; use codex_protocol::ThreadId; @@ -303,6 +304,7 @@ impl AgentControl { crate::session::session::AppServerClientMetadata { client_name: None, client_version: None, + originator: Originator::process_default(), } } }; diff --git a/codex-rs/core/src/arc_monitor.rs b/codex-rs/core/src/arc_monitor.rs index a4f7e038e020..084f5d029a4b 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 f604a634581e..5d28bb07fe7b 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; @@ -394,6 +396,14 @@ impl ModelClient { std::mem::take(&mut *cached_websocket_session) } + pub(crate) fn default_headers(&self, originator: &Originator) -> ApiHeaderMap { + default_headers(originator) + } + + fn build_reqwest_client(&self, originator: &Originator) -> reqwest::Client { + build_reqwest_client(originator) + } + fn store_cached_websocket_session(&self, websocket_session: WebsocketSession) { *self .state @@ -432,6 +442,7 @@ impl ModelClient { /// session-scoped. pub(crate) async fn compact_conversation_history( &self, + originator: &Originator, prompt: &Prompt, model_info: &ModelInfo, settings: CompactConversationRequestSettings, @@ -442,7 +453,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(self.build_reqwest_client(originator)); let request_telemetry = Self::build_request_telemetry( session_telemetry, AuthRequestTelemetryContext::new( @@ -516,6 +527,7 @@ impl ModelClient { pub(crate) async fn create_realtime_call_with_headers( &self, + originator: &Originator, sdp: String, session_config: ApiRealtimeSessionConfig, mut extra_headers: ApiHeaderMap, @@ -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(self.build_reqwest_client(originator)); let response = ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth) .create_with_session_and_headers(sdp, session_config, extra_headers) @@ -551,6 +563,7 @@ impl ModelClient { /// `ModelClient` session-scoped. pub async fn summarize_memories( &self, + originator: &Originator, raw_memories: Vec, model_info: &ModelInfo, effort: Option, @@ -561,7 +574,7 @@ impl ModelClient { } let client_setup = self.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = ReqwestTransport::new(self.build_reqwest_client(originator)); let request_telemetry = Self::build_request_telemetry( session_telemetry, AuthRequestTelemetryContext::new( @@ -800,6 +813,7 @@ impl ModelClient { #[allow(clippy::too_many_arguments)] async fn connect_websocket( &self, + originator: &Originator, session_telemetry: &SessionTelemetry, api_provider: codex_api::Provider, api_auth: SharedAuthProvider, @@ -823,7 +837,7 @@ impl ModelClient { websocket_connect_timeout, ApiWebSocketResponsesClient::new(api_provider, api_auth).connect( headers, - codex_login::default_client::default_headers(), + self.default_headers(originator), turn_state, Some(websocket_telemetry), ), @@ -1067,6 +1081,7 @@ impl ModelClientSession { /// This performs only connection setup; it never sends prompt payloads. pub async fn preconnect_websocket( &mut self, + originator: &Originator, session_telemetry: &SessionTelemetry, _model_info: &ModelInfo, ) -> std::result::Result<(), ApiError> { @@ -1090,6 +1105,7 @@ impl ModelClientSession { let connection = self .client .connect_websocket( + originator, session_telemetry, client_setup.api_provider, client_setup.api_auth, @@ -1119,6 +1135,7 @@ impl ModelClientSession { )] async fn websocket_connection( &mut self, + originator: &Originator, params: WebsocketConnectParams<'_>, ) -> std::result::Result<&ApiWebSocketConnection, ApiError> { let WebsocketConnectParams { @@ -1145,6 +1162,7 @@ impl ModelClientSession { let new_conn = match self .client .connect_websocket( + originator, session_telemetry, api_provider, api_auth, @@ -1209,6 +1227,7 @@ impl ModelClientSession { )] async fn stream_responses_api( &self, + originator: &Originator, prompt: &Prompt, model_info: &ModelInfo, session_telemetry: &SessionTelemetry, @@ -1225,7 +1244,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(self.client.build_reqwest_client(originator)); let request_auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), client_setup.api_auth.as_ref(), @@ -1322,6 +1341,7 @@ impl ModelClientSession { )] async fn stream_responses_websocket( &mut self, + originator: &Originator, prompt: &Prompt, model_info: &ModelInfo, session_telemetry: &SessionTelemetry, @@ -1371,17 +1391,20 @@ impl ModelClientSession { } match self - .websocket_connection(WebsocketConnectParams { - session_telemetry, - api_provider: client_setup.api_provider, - api_auth: client_setup.api_auth, - turn_metadata_header, - options: &options, - auth_context: request_auth_context, - request_route_telemetry: RequestRouteTelemetry::for_endpoint( - RESPONSES_ENDPOINT, - ), - }) + .websocket_connection( + originator, + WebsocketConnectParams { + session_telemetry, + api_provider: client_setup.api_provider, + api_auth: client_setup.api_auth, + turn_metadata_header, + options: &options, + auth_context: request_auth_context, + request_route_telemetry: RequestRouteTelemetry::for_endpoint( + RESPONSES_ENDPOINT, + ), + }, + ) .await { Ok(_) => {} @@ -1485,6 +1508,7 @@ impl ModelClientSession { #[allow(clippy::too_many_arguments)] pub async fn prewarm_websocket( &mut self, + originator: &Originator, prompt: &Prompt, model_info: &ModelInfo, session_telemetry: &SessionTelemetry, @@ -1503,6 +1527,7 @@ impl ModelClientSession { let disabled_trace = InferenceTraceContext::disabled(); match self .stream_responses_websocket( + originator, prompt, model_info, session_telemetry, @@ -1546,6 +1571,7 @@ impl ModelClientSession { /// branches. pub async fn stream( &mut self, + originator: &Originator, prompt: &Prompt, model_info: &ModelInfo, session_telemetry: &SessionTelemetry, @@ -1562,6 +1588,7 @@ impl ModelClientSession { let request_trace = current_span_w3c_trace_context(); match self .stream_responses_websocket( + originator, prompt, model_info, session_telemetry, @@ -1583,6 +1610,7 @@ impl ModelClientSession { } self.stream_responses_api( + originator, prompt, model_info, session_telemetry, diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index b9d9172c8398..1d63d3b24b1c 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -15,6 +15,7 @@ use codex_api::ResponseEvent; use codex_app_server_protocol::AuthMode; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_model_provider::BearerAuthProvider; use codex_model_provider_info::CHATGPT_CODEX_BASE_URL; use codex_model_provider_info::ModelProviderInfo; @@ -316,9 +317,11 @@ async fn summarize_memories_returns_empty_for_empty_input() { let client = test_model_client(SessionSource::Cli); let model_info = test_model_info(); let session_telemetry = test_session_telemetry(); + let originator = Originator::process_default(); let output = client .summarize_memories( + &originator, Vec::new(), &model_info, /*effort*/ None, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 4ed6b2368cbf..3e825faef452 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -74,6 +74,7 @@ pub(crate) async fn run_codex_thread_interactive( ) -> Result { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); + let client_metadata = parent_session.app_server_client_metadata().await; let CodexSpawnOk { codex, .. } = Box::pin(Codex::spawn(CodexSpawnArgs { config, installation_id: parent_session.installation_id.clone(), @@ -91,6 +92,7 @@ pub(crate) async fn run_codex_thread_interactive( dynamic_tools: Vec::new(), persist_extended_history: false, metrics_service_name: None, + originator: client_metadata.originator.clone(), inherited_shell_snapshot: None, user_shell_override: None, inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)), @@ -104,7 +106,6 @@ pub(crate) async fn run_codex_thread_interactive( .or_cancel(&cancel_token) .await??; let thread_config = codex.thread_config_snapshot().await; - let client_metadata = parent_session.app_server_client_metadata().await; emit_subagent_session_started( &parent_session.services.analytics_events_client, client_metadata, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 1b40387c3f4b..4c5a215334e2 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -6,6 +6,7 @@ use crate::session::Codex; use crate::session::SessionSettingsUpdate; use crate::session::SteerInputError; use codex_features::Feature; +use codex_login::default_client::Originator; use codex_otel::SessionTelemetry; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; @@ -246,16 +247,11 @@ impl CodexThread { pub async fn set_app_server_client_info( &self, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, mcp_elicitations_auto_deny: bool, ) -> ConstraintResult<()> { self.codex - .set_app_server_client_info( - app_server_client_name, - app_server_client_version, - mcp_elicitations_auto_deny, - ) + .set_app_server_client_info(originator, mcp_elicitations_auto_deny) .await } diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index b5802268d349..f5f43d3ae6a5 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -537,6 +537,7 @@ async fn drain_to_completed( ) -> CodexResult<()> { let mut stream = client_session .stream( + &turn_context.originator, prompt, &turn_context.model_info, &turn_context.session_telemetry, diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index c7ba1a314f61..4acdb0a3d7f0 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -189,6 +189,7 @@ async fn run_remote_compact_task_inner_impl( .services .model_client .compact_conversation_history( + &turn_context.originator, &prompt, &turn_context.model_info, CompactConversationRequestSettings { diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 2e261b2e9442..03fc40efb4fa 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -270,6 +270,7 @@ async fn run_remote_compaction_request_v2( ) -> CodexResult<(ResponseItem, String)> { let stream = client_session .stream( + &turn_context.originator, prompt, &turn_context.model_info, &turn_context.session_telemetry, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index f4ee42c82197..30949a0b3667 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -32,7 +32,6 @@ 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_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpConnectionManager; use codex_mcp::McpRuntimeEnvironment; @@ -86,10 +85,13 @@ pub struct AccessibleConnectorsStatus { pub async fn list_accessible_connectors_from_mcp_tools( config: &Config, + originator_value: &str, ) -> anyhow::Result> { Ok( list_accessible_connectors_from_mcp_tools_with_options_and_status( - config, /*force_refetch*/ false, + config, + /*force_refetch*/ false, + originator_value, ) .await? .connectors, @@ -113,6 +115,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( config: &Config, auth: Option<&CodexAuth>, accessible_connectors: &[AppInfo], + originator_value: &str, ) -> anyhow::Result> { let connector_ids = tool_suggest_connector_ids(config).await; let directory_connectors = codex_connectors::merge::merge_plugin_connectors( @@ -124,7 +127,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( directory_connectors, accessible_connectors, &connector_ids, - originator().value.as_str(), + originator_value, ) .into_iter() .map(DiscoverableTool::from); @@ -139,6 +142,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( pub async fn list_cached_accessible_connectors_from_mcp_tools( config: &Config, + originator_value: &str, ) -> Option> { let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await; @@ -151,10 +155,7 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools( } let cache_key = accessible_connectors_cache_key(config, auth.as_ref()); read_cached_accessible_connectors(&cache_key).map(|connectors| { - codex_connectors::filter::filter_disallowed_connectors( - connectors, - originator().value.as_str(), - ) + codex_connectors::filter::filter_disallowed_connectors(connectors, originator_value) }) } @@ -162,6 +163,7 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools( config: &Config, auth: Option<&CodexAuth>, mcp_tools: &[ToolInfo], + originator_value: &str, ) { if !config.features.enabled(Feature::Apps) { return; @@ -170,7 +172,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_value, ); write_cached_accessible_connectors(cache_key, &accessible_connectors); } @@ -178,17 +180,23 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools( pub async fn list_accessible_connectors_from_mcp_tools_with_options( config: &Config, force_refetch: bool, + originator_value: &str, ) -> anyhow::Result> { Ok( - list_accessible_connectors_from_mcp_tools_with_options_and_status(config, force_refetch) - .await? - .connectors, + list_accessible_connectors_from_mcp_tools_with_options_and_status( + config, + force_refetch, + originator_value, + ) + .await? + .connectors, ) } pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config: &Config, force_refetch: bool, + originator_value: &str, ) -> anyhow::Result { // TODO: Wire callers that already own an EnvironmentManager into // list_accessible_connectors_from_mcp_tools_with_environment_manager instead @@ -203,6 +211,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config, force_refetch, &environment_manager, + originator_value, ) .await } @@ -211,6 +220,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( config: &Config, force_refetch: bool, environment_manager: &EnvironmentManager, + originator_value: &str, ) -> anyhow::Result { let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await; @@ -232,7 +242,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_value, ); let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance); return Ok(AccessibleConnectorsStatus { @@ -341,7 +351,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_value, ); if codex_apps_ready || !accessible_connectors.is_empty() { write_cached_accessible_connectors(cache_key, &accessible_connectors); diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index cce249167943..708512ae4d99 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -193,7 +193,12 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal ]; let cached = with_accessible_connectors_cache_cleared(|| { - refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools); + refresh_accessible_connectors_cache_from_mcp_tools( + &config, + /*auth*/ None, + &tools, + "codex_vscode", + ); read_cached_accessible_connectors(&cache_key).expect("cache should be populated") }); @@ -1254,7 +1259,7 @@ discoverables = [ let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let discoverable_tools = - list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[]) + list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[], "codex_vscode") .await .expect("discoverable tools should load"); diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index 2908492ddd5b..8273d0b00b02 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -650,7 +650,10 @@ pub(super) async fn run_guardian_review_session( let available_models = session .services .models_manager - .list_models(codex_models_manager::manager::RefreshStrategy::Offline) + .list_models( + codex_models_manager::manager::RefreshStrategy::Offline, + /*originator*/ None, + ) .await; let preferred_reasoning_effort = |supports_low: bool, fallback| { if supports_low { diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index b8552596b3da..276efc7253f2 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -6,7 +6,6 @@ use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; use codex_config::load_global_mcp_servers; 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 +37,7 @@ 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()) { + if !is_first_party_originator(turn_context.originator.value()) { // Only support first-party clients for now. return; } diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 902ede574ef9..9242c10139f0 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -690,6 +690,7 @@ async fn refresh_codex_apps_after_connector_auth(sess: &Session, turn_context: & &turn_context.config, auth.as_ref(), &mcp_tools, + turn_context.originator.value(), ); } Err(err) => { @@ -1499,15 +1500,17 @@ pub(crate) async fn lookup_mcp_tool_metadata( let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME { let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools( turn_context.config.as_ref(), + turn_context.originator.value(), ) .await { Some(connectors) => Some(connectors), - None => { - connectors::list_accessible_connectors_from_mcp_tools(turn_context.config.as_ref()) - .await - .ok() - } + None => connectors::list_accessible_connectors_from_mcp_tools( + turn_context.config.as_ref(), + turn_context.originator.value(), + ) + .await + .ok(), }; connectors.and_then(|connectors| { let connector_id = tool_info.connector_id.as_deref()?; diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index 8b5aac5f7850..7292711051f2 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 249b3ae15f46..c547e56165d6 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -24,7 +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::default_headers; +use codex_login::default_client::Originator; use codex_login::read_openai_api_key_from_env; use codex_model_provider_info::ModelProviderInfo; use codex_protocol::error::CodexErr; @@ -229,6 +229,7 @@ struct RealtimeStart { extra_headers: Option, session_config: RealtimeSessionConfig, model_client: ModelClient, + originator: Originator, sdp: Option, } @@ -281,6 +282,7 @@ impl RealtimeConversationManager { extra_headers, session_config, model_client, + originator, sdp, } = start; let event_parser = session_config.event_parser; @@ -310,6 +312,7 @@ impl RealtimeConversationManager { let (task, sdp) = if let Some(sdp) = sdp { let call = model_client .create_realtime_call_with_headers( + &originator, sdp, session_config.clone(), extra_headers.unwrap_or_default(), @@ -320,6 +323,7 @@ impl RealtimeConversationManager { session_config, call_id: call.call_id, sideband_headers: call.sideband_headers, + default_headers: model_client.default_headers(&originator), input_channels, events_tx, handoff_state: handoff.clone(), @@ -333,7 +337,7 @@ impl RealtimeConversationManager { .connect( session_config, extra_headers.unwrap_or_default(), - default_headers(), + model_client.default_headers(&originator), ) .await .map_err(map_api_error)?; @@ -789,6 +793,7 @@ async fn handle_start_inner( extra_headers, session_config, model_client: sess.services.model_client.clone(), + originator: sess.originator().await, sdp, }; let start_output = sess.conversation.start(start).await?; @@ -1022,6 +1027,7 @@ struct RealtimeWebrtcSidebandInputTask { session_config: RealtimeSessionConfig, call_id: String, sideband_headers: HeaderMap, + default_headers: HeaderMap, input_channels: RealtimeInputChannels, events_tx: Sender, handoff_state: RealtimeHandoffState, @@ -1036,6 +1042,7 @@ fn spawn_webrtc_sideband_input_task(input: RealtimeWebrtcSidebandInputTask) -> J session_config, call_id, sideband_headers, + default_headers, input_channels, events_tx, handoff_state, @@ -1050,12 +1057,7 @@ fn spawn_webrtc_sideband_input_task(input: RealtimeWebrtcSidebandInputTask) -> J } let connection = match client - .connect_webrtc_sideband( - session_config, - &call_id, - sideband_headers, - default_headers(), - ) + .connect_webrtc_sideband(session_config, &call_id, sideband_headers, default_headers) .await { Ok(connection) => connection, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8f0d2f1c01f7..25542a7e3f01 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -59,7 +59,7 @@ 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_login::default_client::Originator; use codex_mcp::McpConnectionManager; use codex_mcp::McpRuntimeEnvironment; use codex_mcp::codex_apps_tools_cache_key; @@ -404,6 +404,7 @@ pub(crate) struct CodexSpawnArgs { pub(crate) dynamic_tools: Vec, pub(crate) persist_extended_history: bool, pub(crate) metrics_service_name: Option, + pub(crate) originator: Originator, pub(crate) inherited_shell_snapshot: Option>, pub(crate) inherited_exec_policy: Option>, /// Parent rollout trace used only to derive fresh spawned child traces. @@ -468,6 +469,7 @@ impl Codex { dynamic_tools, persist_extended_history, metrics_service_name, + originator, inherited_shell_snapshot, user_shell_override, inherited_exec_policy, @@ -535,7 +537,9 @@ impl Codex { codex_models_manager::manager::RefreshStrategy::Offline ) { - let _ = models_manager.list_models(refresh_strategy).await; + let _ = models_manager + .list_models(refresh_strategy, Some(originator.clone())) + .await; } let model = models_manager .get_default_model(&config.model, refresh_strategy) @@ -607,6 +611,10 @@ impl Codex { account_plan_type, config.features.enabled(Feature::FastMode), ); + let app_server_client = originator.app_server_client(); + let app_server_client_name = app_server_client.map(|client| client.name().to_string()); + let app_server_client_version = + app_server_client.map(|client| client.version().to_string()); let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), collaboration_mode, @@ -628,8 +636,9 @@ impl Codex { environments: environment_selections.to_selections(), original_config_do_not_use: Arc::clone(&config), metrics_service_name, - app_server_client_name: None, - app_server_client_version: None, + app_server_client_name, + app_server_client_version, + app_server_originator: Some(originator), session_source, thread_source, dynamic_tools, @@ -766,14 +775,18 @@ impl Codex { pub(crate) async fn set_app_server_client_info( &self, - app_server_client_name: Option, - app_server_client_version: Option, + originator: Originator, mcp_elicitations_auto_deny: bool, ) -> ConstraintResult<()> { + let app_server_client = originator.app_server_client(); + let app_server_client_name = app_server_client.map(|client| client.name().to_string()); + let app_server_client_version = + app_server_client.map(|client| client.version().to_string()); self.session .update_settings(SessionSettingsUpdate { app_server_client_name, app_server_client_version, + app_server_originator: Some(originator), ..Default::default() }) .await?; @@ -885,9 +898,23 @@ impl Session { .session_configuration .app_server_client_version .clone(), + originator: state + .session_configuration + .app_server_originator + .clone() + .unwrap_or_else(Originator::process_default), } } + pub(crate) async fn originator(&self) -> Originator { + let state = self.state.lock().await; + state + .session_configuration + .app_server_originator + .clone() + .unwrap_or_else(Originator::process_default) + } + fn managed_network_proxy_active_for_permission_profile( permission_profile: &PermissionProfile, ) -> bool { @@ -3278,6 +3305,7 @@ pub(crate) fn emit_subagent_session_started( let AppServerClientMetadata { client_name, client_version, + .. } = client_metadata; let (Some(client_name), Some(client_version)) = (client_name, client_version) else { tracing::warn!("skipping subagent thread analytics: missing inherited client metadata"); diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 059dc8abbd71..c0de773c0c6b 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -28,7 +28,10 @@ pub(super) async fn spawn_review_thread( let available_models = sess .services .models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models( + RefreshStrategy::OnlineIfUncached, + Some(parent_turn_context.originator.clone()), + ) .await; let shell_command_backend = shell_command_backend_for_features(review_features.get()); let unified_exec_shell_mode = UnifiedExecShellMode::for_session( @@ -105,6 +108,7 @@ pub(super) async fn spawn_review_thread( ghost_snapshot: parent_turn_context.ghost_snapshot.clone(), current_date: parent_turn_context.current_date.clone(), timezone: parent_turn_context.timezone.clone(), + originator: parent_turn_context.originator.clone(), app_server_client_name: parent_turn_context.app_server_client_name.clone(), developer_instructions: None, user_instructions: None, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 2d96cf9165b3..5f92b4d47f68 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; @@ -90,6 +91,7 @@ pub(crate) struct SessionConfiguration { pub(super) metrics_service_name: Option, pub(super) app_server_client_name: Option, pub(super) app_server_client_version: Option, + pub(super) app_server_originator: Option, /// Source of the session (cli, vscode, exec, mcp, ...) pub(super) session_source: SessionSource, /// Optional analytics source classification for this thread. @@ -336,6 +338,9 @@ impl SessionConfiguration { if let Some(app_server_client_version) = updates.app_server_client_version.clone() { next_configuration.app_server_client_version = Some(app_server_client_version); } + if let Some(app_server_originator) = updates.app_server_originator.clone() { + next_configuration.app_server_originator = Some(app_server_originator); + } Ok(next_configuration) } @@ -398,11 +403,14 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) personality: Option, pub(crate) app_server_client_name: Option, pub(crate) app_server_client_version: Option, + pub(crate) app_server_originator: Option, } +#[derive(Clone)] pub(crate) struct AppServerClientMetadata { pub(crate) client_name: Option, pub(crate) client_version: Option, + pub(crate) originator: Originator, } impl Session { @@ -670,7 +678,11 @@ 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 = session_configuration + .app_server_originator + .clone() + .unwrap_or_else(Originator::process_default); + let originator_value = originator.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( @@ -684,7 +696,7 @@ impl Session { account_id.clone(), account_email.clone(), auth_mode, - originator.clone(), + originator_value.clone(), config.otel.log_user_prompt, terminal_type.clone(), session_configuration.session_source.clone(), @@ -698,7 +710,7 @@ impl Session { app_version: Some(env!("CARGO_PKG_VERSION").to_string()), user_account_id: account_id, auth_mode: auth_mode.map(|mode| mode.to_string()), - originator: Some(originator), + originator: Some(originator_value), user_email: account_email, terminal_type: Some(terminal_type), model: Some(session_model.clone()), @@ -881,7 +893,19 @@ impl Session { thread_store: &thread_extension_data, }).await; } - + let model_client = ModelClient::new( + Some(Arc::clone(&auth_manager)), + session_id, + thread_id, + installation_id.clone(), + session_configuration.provider.clone(), + session_configuration.session_source.clone(), + config.model_verbosity, + config.features.enabled(Feature::EnableRequestCompression), + config.features.enabled(Feature::RuntimeMetrics), + Self::build_model_client_beta_features_header(config.as_ref()), + attestation_provider.clone(), + ); let services = SessionServices { // Initialize the MCP connection manager with an uninitialized // instance. It will be replaced with one created via @@ -930,19 +954,7 @@ impl Session { live_thread: live_thread_init.as_ref().cloned(), thread_store: Arc::clone(&thread_store), attestation_provider: attestation_provider.clone(), - model_client: ModelClient::new( - Some(Arc::clone(&auth_manager)), - session_id, - thread_id, - installation_id.clone(), - session_configuration.provider.clone(), - session_configuration.session_source.clone(), - config.model_verbosity, - config.features.enabled(Feature::EnableRequestCompression), - config.features.enabled(Feature::RuntimeMetrics), - Self::build_model_client_beta_features_header(config.as_ref()), - attestation_provider, - ), + model_client, code_mode_service: crate::tools::code_mode::CodeModeService::new(), environment_manager, }; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index c41d5e348e62..1238c914d9f0 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2856,6 +2856,7 @@ async fn set_rate_limits_retains_previous_credits() { metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -2960,6 +2961,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -3433,6 +3435,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -4068,6 +4071,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -4177,6 +4181,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -4408,6 +4413,7 @@ async fn make_session_with_config_and_rx( metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools: Vec::new(), @@ -4511,6 +4517,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx( metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: session_source.clone(), thread_source: None, dynamic_tools: Vec::new(), @@ -6004,6 +6011,7 @@ where metrics_service_name: None, app_server_client_name: None, app_server_client_version: None, + app_server_originator: None, session_source: SessionSource::Exec, thread_source: None, dynamic_tools, diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 5db4aab9854e..02348e5fa557 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -18,6 +18,7 @@ use codex_execpolicy::Decision; use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_features::Feature; +use codex_login::default_client::Originator; use codex_model_provider::create_model_provider; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; @@ -697,6 +698,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { dynamic_tools: Vec::new(), persist_extended_history: false, metrics_service_name: None, + originator: Originator::process_default(), inherited_shell_snapshot: None, inherited_exec_policy: Some(Arc::new(parent_exec_policy)), parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 4ac511f4fa0f..4a5a26c8b3de 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1096,6 +1096,7 @@ pub(crate) async fn built_tools( &turn_context.config, auth.as_ref(), accessible_connectors.as_slice(), + turn_context.originator.value(), ) .await .map(|discoverable_tools| { @@ -1735,6 +1736,7 @@ async fn try_run_sampling_request( ); let mut stream = client_session .stream( + &turn_context.originator, prompt, &turn_context.model_info, &turn_context.session_telemetry, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 3338d0a5581f..7b95081279a8 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -2,6 +2,7 @@ use super::*; use crate::SkillLoadOutcome; use crate::config::GhostSnapshotConfig; use crate::environment_selection::ResolvedTurnEnvironments; +use codex_login::default_client::Originator; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::SessionId; @@ -69,6 +70,7 @@ pub struct TurnContext { pub(crate) cwd: AbsolutePathBuf, pub(crate) current_date: Option, pub(crate) timezone: Option, + pub(crate) originator: Originator, pub(crate) app_server_client_name: Option, pub(crate) developer_instructions: Option, pub(crate) compact_prompt: Option, @@ -202,7 +204,10 @@ impl TurnContext { ); let features = self.features.clone(); let available_models = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models( + RefreshStrategy::OnlineIfUncached, + Some(self.originator.clone()), + ) .await; Self { @@ -226,6 +231,7 @@ impl TurnContext { cwd: self.cwd.clone(), current_date: self.current_date.clone(), timezone: self.timezone.clone(), + originator: self.originator.clone(), app_server_client_name: self.app_server_client_name.clone(), developer_instructions: self.developer_instructions.clone(), compact_prompt: self.compact_prompt.clone(), @@ -454,10 +460,17 @@ impl Session { let reasoning_summary = session_configuration .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); - let session_telemetry = session_telemetry.clone().with_model( - session_configuration.collaboration_mode.model(), - model_info.slug.as_str(), - ); + let originator = session_configuration + .app_server_originator + .clone() + .unwrap_or_else(Originator::process_default); + let session_telemetry = session_telemetry + .clone() + .with_model( + session_configuration.collaboration_mode.model(), + model_info.slug.as_str(), + ) + .with_originator(originator.value()); let session_source = session_configuration.session_source.clone(); let auth_manager_for_context = auth_manager.clone(); let provider_for_context = create_model_provider(provider, auth_manager); @@ -507,6 +520,7 @@ impl Session { cwd, current_date: Some(current_date), timezone: Some(timezone), + originator, app_server_client_name: session_configuration.app_server_client_name.clone(), developer_instructions: session_configuration.developer_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index ca21e3ea7219..2347ef59a02c 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -263,6 +263,7 @@ async fn schedule_startup_prewarm_inner( let websocket_warmup_started_at = Instant::now(); client_session .prewarm_websocket( + &startup_turn_context.originator, &startup_prompt, &startup_turn_context.model_info, &startup_turn_context.session_telemetry, diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index e5b4fcf6a23f..aedae1ba6bcf 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -24,6 +24,7 @@ use codex_extension_api::ExtensionRegistry; use codex_extension_api::empty_extension_registry; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_model_provider::create_model_provider; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::OPENAI_PROVIDER_ID; @@ -182,6 +183,7 @@ pub struct StartThreadOptions { pub metrics_service_name: Option, pub parent_trace: Option, pub environments: Vec, + pub originator: Originator, } pub(crate) struct ResumeThreadWithHistoryOptions { @@ -435,10 +437,14 @@ impl ThreadManager { self.state.models_manager.clone() } - pub async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec { + pub async fn list_models( + &self, + refresh_strategy: RefreshStrategy, + originator: Option, + ) -> Vec { self.state .models_manager - .list_models(refresh_strategy) + .list_models(refresh_strategy, originator) .await } @@ -574,6 +580,7 @@ impl ThreadManager { metrics_service_name: None, parent_trace: None, environments, + originator: Originator::process_default(), })) .await } @@ -603,6 +610,7 @@ impl ThreadManager { options.parent_trace, options.environments, /*user_shell_override*/ None, + options.originator, )) .await } @@ -651,6 +659,7 @@ impl ThreadManager { auth_manager, /*persist_extended_history*/ false, parent_trace, + Originator::process_default(), )) .await } @@ -662,6 +671,7 @@ impl ThreadManager { auth_manager: Arc, persist_extended_history: bool, parent_trace: Option, + originator: Originator, ) -> CodexResult { let environments = default_thread_environment_selections( self.state.environment_manager.as_ref(), @@ -680,6 +690,7 @@ impl ThreadManager { parent_trace, environments, /*user_shell_override*/ None, + originator, )) .await } @@ -705,6 +716,7 @@ impl ThreadManager { /*parent_trace*/ None, environments, /*user_shell_override*/ Some(user_shell_override), + Originator::process_default(), )) .await } @@ -734,6 +746,7 @@ impl ThreadManager { /*parent_trace*/ None, environments, /*user_shell_override*/ Some(user_shell_override), + Originator::process_default(), )) .await } @@ -821,6 +834,7 @@ impl ThreadManager { thread_source, persist_extended_history, parent_trace, + Originator::process_default(), ) .await } @@ -852,6 +866,7 @@ impl ThreadManager { thread_source: Option, persist_extended_history: bool, parent_trace: Option, + originator: Originator, ) -> CodexResult where S: Into, @@ -863,6 +878,7 @@ impl ThreadManager { thread_source, persist_extended_history, parent_trace, + originator, ) .await } @@ -875,6 +891,7 @@ impl ThreadManager { thread_source: Option, persist_extended_history: bool, parent_trace: Option, + originator: Originator, ) -> CodexResult { let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config); let history = fork_history_from_snapshot(snapshot, history, interrupted_marker); @@ -894,6 +911,7 @@ impl ThreadManager { parent_trace, environments, /*user_shell_override*/ None, + originator, )) .await } @@ -1040,6 +1058,7 @@ impl ThreadManagerState { /*parent_trace*/ None, environments, /*user_shell_override*/ None, + Originator::process_default(), )) .await } @@ -1074,6 +1093,7 @@ impl ThreadManagerState { /*parent_trace*/ None, environments, /*user_shell_override*/ None, + Originator::process_default(), )) .await } @@ -1109,6 +1129,7 @@ impl ThreadManagerState { /*parent_trace*/ None, environments, /*user_shell_override*/ None, + Originator::process_default(), )) .await } @@ -1128,6 +1149,7 @@ impl ThreadManagerState { parent_trace: Option, environments: Vec, user_shell_override: Option, + originator: Originator, ) -> CodexResult { Box::pin(self.spawn_thread_with_source( config, @@ -1144,6 +1166,7 @@ impl ThreadManagerState { parent_trace, environments, user_shell_override, + originator, )) .await } @@ -1165,6 +1188,7 @@ impl ThreadManagerState { parent_trace: Option, environments: Vec, user_shell_override: Option, + originator: Originator, ) -> CodexResult { let is_resumed_thread = matches!(&initial_history, InitialHistory::Resumed(_)); if let InitialHistory::Resumed(resumed) = &initial_history { @@ -1213,6 +1237,7 @@ impl ThreadManagerState { dynamic_tools, persist_extended_history, metrics_service_name, + originator, inherited_shell_snapshot, inherited_exec_policy, parent_rollout_thread_trace, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 43ec706f1a6a..f27b30bd76a8 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -334,6 +334,7 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i environment_id: "local".to_string(), cwd: config.cwd.clone(), }], + originator: Originator::process_default(), }) .await; let err = match result { @@ -465,6 +466,7 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() { metrics_service_name: None, parent_trace: None, environments: Vec::new(), + originator: Originator::process_default(), }) .await .expect("internal thread should start"); @@ -521,6 +523,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() { metrics_service_name: None, parent_trace: None, environments: environments.clone(), + originator: Originator::process_default(), }) .await .expect("start source thread"); @@ -793,6 +796,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() { metrics_service_name: None, parent_trace: None, environments: Vec::new(), + originator: Originator::process_default(), }) .await .expect("start source thread"); @@ -898,6 +902,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() { auth_manager.clone(), /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("seed rollout path in store"); @@ -975,7 +980,9 @@ async fn new_uses_active_provider_for_model_refresh() { /*attestation_provider*/ None, ); - let _ = manager.list_models(RefreshStrategy::Online).await; + let _ = manager + .list_models(RefreshStrategy::Online, /*originator*/ None) + .await; assert_eq!(models_mock.requests().len(), 1); } @@ -1203,6 +1210,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("create source thread from completed history"); @@ -1317,6 +1325,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("create source thread from explicit partial history"); @@ -1408,6 +1417,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_ auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("create source thread from partial history"); @@ -1548,6 +1558,7 @@ async fn resumed_thread_keeps_paused_goal_paused() -> anyhow::Result<()> { auth_manager.clone(), /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("create source thread"); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 968810494dca..7a56ca18aed9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -302,7 +302,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides( let available_models = session .services .models_manager - .list_models(RefreshStrategy::Offline) + .list_models(RefreshStrategy::Offline, /*originator*/ None) .await; let selected_model_name = find_spawn_agent_model_name(&available_models, requested_model)?; let selected_model_info = session diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 2846cacb70fa..bbeef36b0fc8 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -19,6 +19,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_model_provider::create_model_provider; use codex_model_provider_info::built_in_model_providers; use codex_protocol::AgentPath; @@ -2583,6 +2584,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() { AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")), /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("start thread"); diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install.rs b/codex-rs/core/src/tools/handlers/request_plugin_install.rs index 3b53fd007ab5..4419d77ea5db 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install.rs @@ -123,6 +123,7 @@ impl ToolExecutor for RequestPluginInstallHandler { &turn.config, auth.as_ref(), &accessible_connectors, + turn.originator.value(), ) .await .map(|discoverable_tools| { @@ -334,6 +335,7 @@ async fn refresh_missing_requested_connectors( &turn.config, auth, &mcp_tools, + turn.originator.value(), ); Some(accessible_connectors) } diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index a094545bab13..dcaa59076815 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/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 6f0429e64499..5aef1433810c 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -5,6 +5,7 @@ use codex_core::ModelClient; use codex_core::Prompt; use codex_core::ResponseEvent; 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::SessionTelemetry; @@ -123,8 +124,10 @@ async fn responses_stream_includes_subagent_header_on_review() { phase: None, }]; + let originator = Originator::process_default(); let mut stream = client_session .stream( + &originator, &prompt, &model_info, &session_telemetry, @@ -251,8 +254,10 @@ async fn responses_stream_includes_subagent_header_on_other() { phase: None, }]; + let originator = Originator::process_default(); let mut stream = client_session .stream( + &originator, &prompt, &model_info, &session_telemetry, @@ -368,8 +373,10 @@ async fn responses_respects_model_info_overrides_from_config() { phase: None, }]; + let originator = Originator::process_default(); let mut stream = client_session .stream( + &originator, &prompt, &model_info, &session_telemetry, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f6731a0ea4fa..84f4642185b6 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(), @@ -917,8 +917,10 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth phase: None, }); + let originator = Originator::process_default(); let mut stream = client_session .stream( + &originator, &prompt, &model_info, &session_telemetry, @@ -1052,7 +1054,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!( @@ -2405,8 +2407,10 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { output: FunctionCallOutputPayload::from_text("ok".into()), }); + let originator = Originator::process_default(); let mut stream = client_session .stream( + &originator, &prompt, &model_info, &session_telemetry, diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index feee9d42272f..d50d71d254c6 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; @@ -58,6 +59,11 @@ const USER_AGENT_HEADER: &str = "user-agent"; const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06"; const X_CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id"; const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; + +fn default_user_agent() -> String { + let originator = Originator::process_default(); + codex_login::default_client::get_codex_user_agent(&originator) +} const X_CODEX_WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY: &str = "x-codex-ws-stream-request-start-ms"; @@ -96,6 +102,7 @@ struct WebsocketTestHarness { effort: Option, summary: ReasoningSummary, session_telemetry: SessionTelemetry, + originator: Originator, } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -141,7 +148,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 +392,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); @@ -429,7 +436,11 @@ async fn responses_websocket_preconnect_does_not_replace_turn_trace_payload() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); client_session - .preconnect_websocket(&harness.session_telemetry, &harness.model_info) + .preconnect_websocket( + &harness.originator, + &harness.session_telemetry, + &harness.model_info, + ) .await .expect("websocket preconnect failed"); let prompt = prompt_with_input(vec![message_item("hello")]); @@ -465,7 +476,11 @@ async fn responses_websocket_preconnect_reuses_connection() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); client_session - .preconnect_websocket(&harness.session_telemetry, &harness.model_info) + .preconnect_websocket( + &harness.originator, + &harness.session_telemetry, + &harness.model_info, + ) .await .expect("websocket preconnect failed"); let prompt = prompt_with_input(vec![message_item("hello")]); @@ -474,7 +489,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); @@ -497,6 +512,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() { let prompt = prompt_with_input(vec![message_item("hello")]); client_session .prewarm_websocket( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -512,7 +528,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); @@ -576,12 +592,17 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); client_session - .preconnect_websocket(&harness.session_telemetry, &harness.model_info) + .preconnect_websocket( + &harness.originator, + &harness.session_telemetry, + &harness.model_info, + ) .await .expect("websocket preconnect failed"); let prompt = prompt_with_input(vec![message_item("hello")]); let mut stream = client_session .stream( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -621,6 +642,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes( let prompt = prompt_with_input(vec![message_item("hello")]); client_session .prewarm_websocket( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -633,6 +655,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes( .expect("websocket prewarm failed"); let mut stream = client_session .stream( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -687,6 +710,7 @@ async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets() let prompt = prompt_with_input(vec![message_item("hello")]); client_session .prewarm_websocket( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -742,7 +766,11 @@ async fn responses_websocket_preconnect_runs_when_only_v2_feature_enabled() { let harness = websocket_harness_with_options(&server, /*runtime_metrics_enabled*/ true).await; let mut client_session = harness.client.new_session(); client_session - .preconnect_websocket(&harness.session_telemetry, &harness.model_info) + .preconnect_websocket( + &harness.originator, + &harness.session_telemetry, + &harness.model_info, + ) .await .expect("websocket preconnect failed"); @@ -1036,6 +1064,7 @@ async fn responses_websocket_emits_reasoning_included_event() { let mut stream = client_session .stream( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -1110,6 +1139,7 @@ async fn responses_websocket_emits_rate_limit_events() { let mut stream = client_session .stream( + &harness.originator, &prompt, &harness.model_info, &harness.session_telemetry, @@ -1359,10 +1389,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; @@ -1763,6 +1790,7 @@ async fn responses_websocket_v2_after_error_uses_full_create_without_previous_re let mut second_stream = session .stream( + &harness.originator, &prompt_two, &harness.model_info, &harness.session_telemetry, @@ -1851,6 +1879,7 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake( let mut second_stream = session .stream( + &harness.originator, &prompt_two, &harness.model_info, &harness.session_telemetry, @@ -2035,6 +2064,7 @@ async fn websocket_harness_with_provider_options( .with_metrics(metrics); let effort = None; let summary = ReasoningSummary::Auto; + let originator = Originator::process_default(); let client = ModelClient::new( /*auth_manager*/ None, session_id, @@ -2058,6 +2088,7 @@ async fn websocket_harness_with_provider_options( effort, summary, session_telemetry, + originator, } } @@ -2117,6 +2148,7 @@ async fn stream_until_complete_with_request_metadata( ) { let mut stream = client_session .stream( + &harness.originator, prompt, &harness.model_info, &harness.session_telemetry, diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index 68a167f79022..8df9fec7710b 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -1,6 +1,7 @@ use codex_core::ForkSnapshot; use codex_core::NewThread; use codex_core::parse_turn_item; +use codex_login::default_client::Originator; use codex_protocol::items::TurnItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InitialHistory; @@ -204,6 +205,7 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() { /*thread_source*/ None, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("fork from stored history"); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 009dfda60f52..a4d3de82590e 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -418,7 +418,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=" .to_string(); @@ -537,7 +537,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { let _ = std::fs::remove_file(&saved_path); let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; test.codex @@ -651,7 +651,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima let _ = std::fs::remove_file(&saved_path); let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; test.codex @@ -767,7 +767,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() let _ = std::fs::remove_file(&saved_path); let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; test.codex @@ -920,7 +920,9 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); - let available_models = models_manager.list_models(RefreshStrategy::Online).await; + let available_models = models_manager + .list_models(RefreshStrategy::Online, /*originator*/ None) + .await; assert!( available_models .iter() diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 3b729738df38..86a55610b51e 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -70,7 +70,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { // Populate cache via initial refresh. let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; let cache_path = config.codex_home.join(CACHE_FILE); @@ -134,7 +134,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { // Cached models remain usable offline. let offline_models = test .thread_manager - .list_models(RefreshStrategy::Offline) + .list_models(RefreshStrategy::Offline, /*originator*/ None) .await; assert!( offline_models @@ -177,7 +177,7 @@ async fn uses_cache_when_version_matches() -> Result<()> { let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; assert!( @@ -224,7 +224,7 @@ async fn refreshes_when_cache_version_missing() -> Result<()> { let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; assert!( @@ -272,7 +272,7 @@ async fn refreshes_when_cache_version_differs() -> Result<()> { let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) .await; assert!( diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index bbe8178f8d8c..42bf5fc350c0 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -785,7 +785,9 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str) { let deadline = Instant::now() + Duration::from_secs(2); loop { - let models = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let models = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; if models.iter().any(|model| model.model == slug) { return; } diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 45fdf3df0764..4beef3fc4a5d 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/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 232d2380b0a1..f233f160ad87 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -5,6 +5,7 @@ use codex_config::config_toml::RealtimeWsVersion; use codex_core::test_support::auth_manager_from_auth; use codex_login::CodexAuth; use codex_login::OPENAI_API_KEY_ENV_VAR; +use codex_login::default_client::Originator; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -1890,6 +1891,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge auth_manager_from_auth(CodexAuth::from_api_key("dummy")), /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await?; let codex = resumed_thread.thread; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index e143cb8e3ed9..68ec0dc0694b 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -102,7 +102,9 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( provider, ); - manager.list_models(RefreshStrategy::OnlineIfUncached).await; + manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let model_info = manager .get_model_info("gpt-5.3-codex-test", &config.to_models_manager_config()) @@ -848,7 +850,9 @@ async fn remote_models_do_not_append_removed_builtin_presets() -> Result<()> { provider, ); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let remote = available .iter() .find(|model| model.model == "remote-alpha") @@ -909,7 +913,9 @@ async fn remote_models_merge_adds_new_high_priority_first() -> Result<()> { provider, ); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; assert_eq!( available.first().map(|model| model.model.as_str()), Some("remote-top") @@ -956,7 +962,9 @@ async fn remote_models_merge_replaces_overlapping_model() -> Result<()> { provider, ); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let overridden = available .iter() .find(|model| model.model == slug) @@ -1000,7 +1008,9 @@ async fn remote_models_merge_preserves_bundled_models_on_empty_response() -> Res provider, ); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let bundled_slug = bundled_model_slug(); assert!( available.iter().any(|model| model.model == bundled_slug), @@ -1117,7 +1127,9 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { .await; assert_eq!(selected, bundled_default_model_slug()); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let hidden = available .iter() .find(|model| model.model == "codex-auto-balanced") @@ -1138,7 +1150,9 @@ async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str) -> let deadline = Instant::now() + Duration::from_secs(2); loop { if let Some(model) = { - let guard = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let guard = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; guard.iter().find(|model| model.model == slug).cloned() } { return model; diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index d2bdcd1d3203..dc787d0661b3 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -2,6 +2,7 @@ use codex_core::NewThread; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_protocol::ThreadId; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; @@ -106,6 +107,7 @@ async fn emits_warning_when_resumed_model_differs() { auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("resume conversation"); diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 7c811a88e042..25a1e14d6e8c 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1349,7 +1349,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re fixture .thread_manager .get_models_manager() - .list_models(RefreshStrategy::Online) + .list_models(RefreshStrategy::Online, /*originator*/ None) .await; assert_eq!(models_mock.requests().len(), 1); diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 9a7d70adebb1..3faf4d6c3bde 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -87,7 +87,9 @@ fn test_model_info( async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str) { let deadline = Instant::now() + Duration::from_secs(2); loop { - let available_models = manager.list_models(RefreshStrategy::Online).await; + let available_models = manager + .list_models(RefreshStrategy::Online, /*originator*/ None) + .await; if available_models.iter().any(|model| model.model == slug) { return; } diff --git a/codex-rs/core/tests/suite/unstable_features_warning.rs b/codex-rs/core/tests/suite/unstable_features_warning.rs index 66a736658964..9f543a3e05d4 100644 --- a/codex-rs/core/tests/suite/unstable_features_warning.rs +++ b/codex-rs/core/tests/suite/unstable_features_warning.rs @@ -4,6 +4,7 @@ use codex_config::CONFIG_TOML_FILE; use codex_core::NewThread; use codex_features::Feature; use codex_login::CodexAuth; +use codex_login::default_client::Originator; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::WarningEvent; @@ -48,6 +49,7 @@ async fn emits_warning_when_unstable_features_enabled_via_config() { auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("spawn conversation"); @@ -95,6 +97,7 @@ async fn suppresses_warning_when_configured() { auth_manager, /*persist_extended_history*/ false, /*parent_trace*/ None, + Originator::process_default(), ) .await .expect("spawn conversation"); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 9e54a72abd0b..0cf67aefd68d 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/exec/tests/suite/originator.rs b/codex-rs/exec/tests/suite/originator.rs index e63f57ab0ff3..f6a3b0aa6ec8 100644 --- a/codex-rs/exec/tests/suite/originator.rs +++ b/codex-rs/exec/tests/suite/originator.rs @@ -31,7 +31,7 @@ async fn send_codex_exec_originator() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn supports_originator_override() -> anyhow::Result<()> { +async fn app_server_client_info_ignores_originator_override() -> anyhow::Result<()> { let test = test_codex_exec(); let server = responses::start_mock_server().await; @@ -40,8 +40,7 @@ async fn supports_originator_override() -> anyhow::Result<()> { responses::ev_assistant_message("response_1", "Hello, world!"), responses::ev_completed("response_1"), ]); - responses::mount_sse_once_match(&server, header("Originator", "codex_exec_override"), body) - .await; + responses::mount_sse_once_match(&server, header("Originator", "codex_exec"), body).await; test.cmd_with_server(&server) .env("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "codex_exec_override") diff --git a/codex-rs/login/src/auth/agent_identity.rs b/codex-rs/login/src/auth/agent_identity.rs index 3644713328fa..ba693df37cc1 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 ee51edf9b56d..0efdc3bbb401 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, + kind: OriginatorKind, } -static ORIGINATOR: LazyLock>> = LazyLock::new(|| RwLock::new(None)); -static REQUIREMENTS_RESIDENCY: LazyLock>> = - LazyLock::new(|| RwLock::new(None)); -#[derive(Debug)] -pub enum SetOriginatorError { - InvalidHeaderValue, - AlreadyInitialized, +#[derive(Debug, Clone, PartialEq, Eq)] +enum OriginatorKind { + Process { value: String }, + AppServerClient { client: AppServerClient }, } -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()); - - 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 + } +} + +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"), + } } - *guard = Some(originator); +} + +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 be8e6bc392af..c9833a76647e 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 a2e4e8e0d866..a54af62335e5 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 b6a9fef4bbec..9c384c5635d5 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 b72bc946f279..0859bfbb0e50 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 9e536d930cfd..3b808ef2c1f8 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 f50f25f49ff3..27803bc22a19 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 b1ffb2d215f7..296e5165409a 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; @@ -69,6 +69,7 @@ pub(crate) struct MemoryStartupContext { thread_manager: Arc, auth_manager: Arc, session_telemetry: SessionTelemetry, + originator: Originator, } impl MemoryStartupContext { @@ -86,6 +87,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 +99,7 @@ impl MemoryStartupContext { account_id, account_email, auth_mode, - originator().value, + originator.value().to_string(), config.otel.log_user_prompt, user_agent(), source, @@ -110,6 +112,7 @@ impl MemoryStartupContext { thread_manager, auth_manager, session_telemetry, + originator, } } @@ -189,6 +192,7 @@ impl MemoryStartupContext { let mut client_session = model_client.new_session(); let mut stream = client_session .stream( + &self.originator, prompt, &context.model_info, &context.session_telemetry, @@ -250,6 +254,7 @@ impl MemoryStartupContext { metrics_service_name: None, parent_trace: None, environments, + originator: self.originator.clone(), }) .await?; diff --git a/codex-rs/model-provider/src/models_endpoint.rs b/codex-rs/model-provider/src/models_endpoint.rs index 8a72beea7012..5b3b84053b38 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; @@ -81,6 +82,7 @@ impl ModelsEndpointClient for OpenAiModelsEndpoint { async fn list_models( &self, client_version: &str, + originator: Option, ) -> CoreResult<(Vec, Option)> { let _timer = codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]); @@ -88,7 +90,9 @@ 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.unwrap_or_else(Originator::process_default); + let reqwest_client = build_reqwest_client(&originator); + let transport = ReqwestTransport::new(reqwest_client); 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/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 1ef7c22962b5..696b4726049e 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -500,7 +500,9 @@ mod tests { let manager = provider.models_manager(test_codex_home(), /*config_model_catalog*/ None); - let catalog = manager.raw_model_catalog(RefreshStrategy::Online).await; + let catalog = manager + .raw_model_catalog(RefreshStrategy::Online, /*originator*/ None) + .await; let model_ids = catalog .models .iter() @@ -517,7 +519,7 @@ mod tests { ); let default_model = manager - .list_models(RefreshStrategy::Online) + .list_models(RefreshStrategy::Online, /*originator*/ None) .await .into_iter() .find(|preset| preset.is_default) @@ -542,7 +544,9 @@ mod tests { }), ); - let catalog = manager.raw_model_catalog(RefreshStrategy::Online).await; + let catalog = manager + .raw_model_catalog(RefreshStrategy::Online, /*originator*/ None) + .await; assert_eq!(catalog.models.len(), 1); assert_eq!(catalog.models[0].slug, "custom-bedrock-model"); @@ -578,7 +582,9 @@ mod tests { let manager = provider.models_manager(test_codex_home(), /*config_model_catalog*/ None); - let catalog = manager.raw_model_catalog(RefreshStrategy::Online).await; + let catalog = manager + .raw_model_catalog(RefreshStrategy::Online, /*originator*/ None) + .await; assert!( catalog diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index af510c8d73f2..8355eb27950b 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -5,6 +5,7 @@ use crate::model_info; use async_trait::async_trait; use codex_app_server_protocol::AuthMode; use codex_login::AuthManager; +use codex_login::default_client::Originator; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::error::Result as CoreResult; use codex_protocol::openai_models::ModelInfo; @@ -41,6 +42,7 @@ pub trait ModelsEndpointClient: fmt::Debug + Send + Sync { async fn list_models( &self, client_version: &str, + originator: Option, ) -> CoreResult<(Vec, Option)>; } @@ -79,9 +81,13 @@ pub trait ModelsManager: fmt::Debug + Send + Sync { /// List all available models, refreshing according to the specified strategy. /// /// Returns model presets sorted by priority and filtered by auth mode and visibility. - async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec { + async fn list_models( + &self, + refresh_strategy: RefreshStrategy, + originator: Option, + ) -> Vec { async move { - let catalog = self.raw_model_catalog(refresh_strategy).await; + let catalog = self.raw_model_catalog(refresh_strategy, originator).await; self.build_available_models(catalog.models) } .instrument(tracing::info_span!( @@ -92,7 +98,11 @@ pub trait ModelsManager: fmt::Debug + Send + Sync { } /// Return the active raw model catalog, refreshing according to the specified strategy. - async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse; + async fn raw_model_catalog( + &self, + refresh_strategy: RefreshStrategy, + originator: Option, + ) -> ModelsResponse; /// Return the current in-memory remote model catalog without refreshing or loading cache state. async fn get_remote_models(&self) -> Vec; @@ -147,7 +157,10 @@ pub trait ModelsManager: fmt::Debug + Send + Sync { if let Some(model) = model.as_ref() { return model.to_string(); } - default_model_from_available(self.list_models(refresh_strategy).await) + default_model_from_available( + self.list_models(refresh_strategy, /*originator*/ None) + .await, + ) } .instrument(tracing::info_span!( "get_default_model", @@ -226,8 +239,15 @@ impl StaticModelsManager { #[async_trait] impl ModelsManager for OpenAiModelsManager { - async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse { - if let Err(err) = self.refresh_available_models(refresh_strategy).await { + async fn raw_model_catalog( + &self, + refresh_strategy: RefreshStrategy, + originator: Option, + ) -> ModelsResponse { + if let Err(err) = self + .refresh_available_models(refresh_strategy, originator) + .await + { error!("failed to refresh available models: {err}"); } ModelsResponse { @@ -259,7 +279,10 @@ impl ModelsManager for OpenAiModelsManager { } return; } - if let Err(err) = self.refresh_available_models(RefreshStrategy::Online).await { + if let Err(err) = self + .refresh_available_models(RefreshStrategy::Online, /*originator*/ None) + .await + { error!("failed to refresh available models: {err}"); } } @@ -267,7 +290,11 @@ impl ModelsManager for OpenAiModelsManager { impl OpenAiModelsManager { /// Refresh available models according to the specified strategy. - async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()> { + async fn refresh_available_models( + &self, + refresh_strategy: RefreshStrategy, + originator: Option, + ) -> CoreResult<()> { if !self.should_refresh_models().await { if matches!( refresh_strategy, @@ -291,18 +318,21 @@ impl OpenAiModelsManager { return Ok(()); } info!("models cache: cache miss, fetching remote models"); - self.fetch_and_update_models().await + self.fetch_and_update_models(originator).await } RefreshStrategy::Online => { // Always fetch from network - self.fetch_and_update_models().await + self.fetch_and_update_models(originator).await } } } - async fn fetch_and_update_models(&self) -> CoreResult<()> { + async fn fetch_and_update_models(&self, originator: Option) -> CoreResult<()> { let client_version = crate::client_version_to_whole(); - let (models, etag) = self.endpoint_client.list_models(&client_version).await?; + let (models, etag) = self + .endpoint_client + .list_models(&client_version, originator) + .await?; self.apply_remote_models(models.clone()).await; *self.etag.write().await = etag.clone(); self.cache_manager @@ -381,7 +411,11 @@ impl OpenAiModelsManager { #[async_trait] impl ModelsManager for StaticModelsManager { - async fn raw_model_catalog(&self, _refresh_strategy: RefreshStrategy) -> ModelsResponse { + async fn raw_model_catalog( + &self, + _refresh_strategy: RefreshStrategy, + _originator: Option, + ) -> ModelsResponse { ModelsResponse { models: self.get_remote_models().await, } diff --git a/codex-rs/models-manager/src/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs index ede7cfe79580..fb9acf930cff 100644 --- a/codex-rs/models-manager/src/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -157,6 +157,7 @@ impl ModelsEndpointClient for TestModelsEndpoint { async fn list_models( &self, _client_version: &str, + _originator: Option, ) -> CoreResult<(Vec, Option)> { self.fetch_count.fetch_add(1, Ordering::SeqCst); let models = self @@ -350,7 +351,9 @@ async fn refresh_available_models_sorts_by_priority() { let cached_remote = manager.get_remote_models().await; assert_models_contain(&cached_remote, &remote_models); - let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; + let available = manager + .list_models(RefreshStrategy::OnlineIfUncached, /*originator*/ None) + .await; let high_idx = available .iter() .position(|model| model.model == "priority-high") @@ -733,6 +736,7 @@ impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { async fn list_models( &self, _client_version: &str, + _originator: Option, ) -> CoreResult<(Vec, Option)> { self.fetch_count.fetch_add(1, Ordering::SeqCst); let models = self @@ -897,7 +901,9 @@ async fn static_manager_reads_latest_auth_mode() { }, ); - let chatgpt_models = manager.list_models(RefreshStrategy::Online).await; + let chatgpt_models = manager + .list_models(RefreshStrategy::Online, /*originator*/ None) + .await; assert_eq!( chatgpt_models .iter() @@ -907,7 +913,9 @@ async fn static_manager_reads_latest_auth_mode() { ); auth_manager.set_external_auth(Arc::new(TestExternalApiKeyAuth)); - let api_models = manager.list_models(RefreshStrategy::Online).await; + let api_models = manager + .list_models(RefreshStrategy::Online, /*originator*/ None) + .await; assert_eq!( api_models diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 6394c73ea2ed..fb2e51426dce 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -117,6 +117,11 @@ impl SessionTelemetry { self } + pub fn with_originator(mut self, originator: &str) -> Self { + self.metadata.originator = sanitize_metric_tag_value(originator); + self + } + pub fn with_metrics_service_name(mut self, service_name: &str) -> Self { self.metadata.service_name = Some(sanitize_metric_tag_value(service_name)); self diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index 23b05b9defad..cde6d14dc629 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 cf499c7703cd..8c9472981412 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -34,6 +34,7 @@ use codex_core_api::Notice; use codex_core_api::OAuthCredentialsStoreMode; use codex_core_api::OPENAI_PROVIDER_ID; use codex_core_api::Op; +use codex_core_api::Originator; use codex_core_api::OtelConfig; use codex_core_api::PermissionProfile; use codex_core_api::Permissions; @@ -42,6 +43,7 @@ use codex_core_api::RealtimeAudioConfig; use codex_core_api::RealtimeConfig; use codex_core_api::SessionPickerViewMode; use codex_core_api::SessionSource; +use codex_core_api::StartThreadOptions; use codex_core_api::TerminalResizeReflowConfig; use codex_core_api::ThreadManager; use codex_core_api::ThreadStoreConfig; @@ -59,7 +61,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 +83,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() { @@ -132,10 +129,24 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { /*attestation_provider*/ None, ); + let originator = Originator::for_process("codex_thread_manager_sample".to_string()) + .expect("codex_thread_manager_sample should be a valid originator header value"); + let environments = thread_manager.default_environment_selections(&config.cwd); let NewThread { thread_id, thread, .. } = thread_manager - .start_thread(config) + .start_thread_with_options(StartThreadOptions { + config, + initial_history: codex_core_api::InitialHistory::New, + session_source: None, + thread_source: None, + dynamic_tools: Vec::new(), + persist_extended_history: false, + metrics_service_name: None, + parent_trace: None, + environments, + originator, + }) .await .context("start Codex thread")?; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5cfb520dbed7..e3ce85540df3 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 9ece5e3776f5..38cdf66ee911 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 e99852ead9b7..74ee34b220ef 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 0c293936c9a9..ded5e26539fd 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 {