diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f387d2c1ac..dbe708149e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2660,12 +2660,14 @@ dependencies = [ "anyhow", "chrono", "codex-analytics", + "codex-api", "codex-app-server-protocol", "codex-config", "codex-core-skills", "codex-exec-server", "codex-git-utils", "codex-hooks", + "codex-http-state", "codex-login", "codex-model-provider", "codex-otel", diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 2335694856..b3422e03e9 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -616,9 +616,8 @@ impl PluginRequestProcessor { // TODO(remote plugins): Remove this once remote plugins are ready and vertical plugins are // served directly from the normal remote catalog. if include_vertical && !config.features.enabled(Feature::RemotePlugin) { - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); match codex_core_plugins::remote::fetch_openai_curated_remote_collection_marketplace( &remote_plugin_service_config, auth.as_ref(), @@ -655,9 +654,8 @@ impl PluginRequestProcessor { remote_sources.push(RemoteMarketplaceSource::SharedWithMe); } if !remote_sources.is_empty() { - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); match codex_core_plugins::remote::fetch_remote_marketplaces( &remote_plugin_service_config, auth.as_ref(), @@ -966,9 +964,8 @@ impl PluginRequestProcessor { let share_context = match share_context { Some(context) => { let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); match codex_core_plugins::remote::fetch_remote_plugin_share_context( &remote_plugin_service_config, auth.as_ref(), @@ -1072,9 +1069,8 @@ impl PluginRequestProcessor { ))); } let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); validate_remote_plugin_id(&plugin_name)?; let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail( &remote_plugin_service_config, @@ -1130,9 +1126,8 @@ impl PluginRequestProcessor { } let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail( &remote_plugin_service_config, auth.as_ref(), @@ -1183,9 +1178,8 @@ impl PluginRequestProcessor { validate_client_plugin_share_targets(share_targets)?; } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let access_policy = codex_core_plugins::remote::RemotePluginShareAccessPolicy { discoverability: discoverability.map(remote_plugin_share_discoverability), share_targets: share_targets.map(remote_plugin_share_targets), @@ -1226,9 +1220,8 @@ impl PluginRequestProcessor { } validate_client_plugin_share_targets(&share_targets)?; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let result = codex_core_plugins::remote::update_remote_plugin_share_targets( &remote_plugin_service_config, auth.as_ref(), @@ -1256,9 +1249,8 @@ impl PluginRequestProcessor { _params: PluginShareListParams, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let data = codex_core_plugins::remote::list_remote_plugin_shares( &remote_plugin_service_config, auth.as_ref(), @@ -1295,9 +1287,8 @@ impl PluginRequestProcessor { return Err(invalid_request("invalid remote plugin id")); } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let result = codex_core_plugins::remote::checkout_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), @@ -1328,9 +1319,8 @@ impl PluginRequestProcessor { return Err(invalid_request("invalid remote plugin id")); } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); codex_core_plugins::remote::delete_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), @@ -1448,9 +1438,8 @@ impl PluginRequestProcessor { validate_remote_plugin_id(&remote_plugin_id)?; let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail_with_download_urls( &remote_plugin_service_config, @@ -1796,9 +1785,8 @@ impl PluginRequestProcessor { validate_remote_plugin_id(&plugin_id)?; let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin( &remote_plugin_service_config, auth.as_ref(), diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index d0096e4455..2499e07ae7 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -15,12 +15,14 @@ workspace = true [dependencies] anyhow = { workspace = true } codex-analytics = { workspace = true } +codex-api = { workspace = true } codex-app-server-protocol = { workspace = true } codex-config = { workspace = true } codex-core-skills = { workspace = true } codex-exec-server = { workspace = true } codex-git-utils = { workspace = true } codex-hooks = { workspace = true } +codex-http-state = { workspace = true } codex-login = { workspace = true } codex-model-provider = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 8022c6c86d..0263b6e565 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -173,9 +173,7 @@ struct ConfiguredMarketplaceUpgradeState { } fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig { - RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - } + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()) } fn featured_plugin_ids_cache_key( diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 3c86f6b14b..7be30a8f97 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -1,16 +1,19 @@ use crate::store::PLUGINS_CACHE_DIR; use crate::store::PluginStore; +use codex_api::SharedAuthProvider; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginAvailability; use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginInterface; use codex_app_server_protocol::SkillInterface; +use codex_http_state::HttpStateContext; use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; use codex_plugin::PluginId; use codex_utils_absolute_path::AbsolutePathBuf; use reqwest::RequestBuilder; +use reqwest::header::HeaderMap; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -24,6 +27,7 @@ use std::time::Duration; use url::Url; mod catalog_cache; +pub(crate) mod http_state; mod remote_installed_plugin_sync; mod share; @@ -91,9 +95,24 @@ const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 5] = [ ), ]; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct RemotePluginServiceConfig { pub chatgpt_base_url: String, + http_state: Option, +} + +impl RemotePluginServiceConfig { + pub fn new(chatgpt_base_url: String) -> Self { + Self { + chatgpt_base_url, + http_state: None, + } + } + + pub fn with_http_state(mut self, http_state: HttpStateContext) -> Self { + self.http_state = Some(http_state); + self + } } #[derive(Debug, Clone, PartialEq)] @@ -871,7 +890,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 request = authenticated_request(client.get(&url), auth)?; + let request = authenticated_request(config, client.get(&url), auth, &url); let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?; if response.plugin_id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { @@ -973,7 +992,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 request = authenticated_request(client.post(&url), auth)?; + let request = authenticated_request(config, client.post(&url), auth, &url); let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { @@ -1009,7 +1028,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 request = authenticated_request(client.post(&url), auth)?; + let request = authenticated_request(config, client.post(&url), auth, &url); let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { @@ -1399,7 +1418,7 @@ async fn get_remote_plugin_list_page( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/list"); let client = build_reqwest_client(); - let mut request = authenticated_request(client.get(&url), auth)?; + let mut request = authenticated_request(config, client.get(&url), auth, &url); request = request.query(&[("scope", scope.api_value())]); request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); if let Some(collection) = collection { @@ -1419,7 +1438,7 @@ async fn get_remote_shared_workspace_plugins_page( 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 mut request = authenticated_request(client.get(&url), auth)?; + let mut request = authenticated_request(config, client.get(&url), auth, &url); request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); @@ -1437,7 +1456,7 @@ async fn get_remote_plugin_installed_page( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/installed"); let client = build_reqwest_client(); - let mut request = authenticated_request(client.get(&url), auth)?; + let mut request = authenticated_request(config, client.get(&url), auth, &url); request = request.query(&[("scope", scope.api_value())]); if include_download_urls { request = request.query(&[("includeDownloadUrls", true)]); @@ -1457,7 +1476,7 @@ async fn fetch_plugin_detail( 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 mut request = authenticated_request(client.get(&url), auth)?; + let mut request = authenticated_request(config, client.get(&url), auth, &url); if include_download_urls { request = request.query(&[("includeDownloadUrls", true)]); } @@ -1496,25 +1515,62 @@ fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePlu } fn authenticated_request( + config: &RemotePluginServiceConfig, request: RequestBuilder, auth: &CodexAuth, -) -> Result { - Ok(request - .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) - .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers())) + url: &str, +) -> AuthenticatedRequest { + let (auth_provider, request_headers) = http_state::request_headers(config, auth, url); + AuthenticatedRequest { + request: request + .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) + .headers(request_headers.clone()), + auth_provider, + request_headers, + } +} + +struct AuthenticatedRequest { + request: RequestBuilder, + auth_provider: SharedAuthProvider, + request_headers: HeaderMap, +} + +impl AuthenticatedRequest { + fn query(mut self, query: &T) -> Self { + self.request = self.request.query(query); + self + } + + fn json(mut self, body: &T) -> Self { + self.request = self.request.json(body); + self + } + + async fn send(self, url: &str) -> Result { + let response = + self.request + .send() + .await + .map_err(|source| RemotePluginCatalogError::Request { + url: url.to_string(), + source, + })?; + http_state::observe_response_headers( + &self.auth_provider, + url, + &self.request_headers, + response.headers(), + ); + Ok(response) + } } async fn send_and_decode Deserialize<'de>>( - request: RequestBuilder, + request: AuthenticatedRequest, url: &str, ) -> Result { - let response = request - .send() - .await - .map_err(|source| RemotePluginCatalogError::Request { - url: url.to_string(), - source, - })?; + let response = request.send(url).await?; let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { diff --git a/codex-rs/core-plugins/src/remote/http_state.rs b/codex-rs/core-plugins/src/remote/http_state.rs new file mode 100644 index 0000000000..2cabfb156f --- /dev/null +++ b/codex-rs/core-plugins/src/remote/http_state.rs @@ -0,0 +1,32 @@ +use super::RemotePluginServiceConfig; +use codex_api::SharedAuthProvider; +use codex_login::CodexAuth; +use reqwest::header::HeaderMap; + +pub(crate) fn request_headers( + config: &RemotePluginServiceConfig, + auth: &CodexAuth, + url: &str, +) -> (SharedAuthProvider, HeaderMap) { + let auth_provider = codex_model_provider::with_native_integrity_state( + codex_model_provider::auth_provider_from_auth(auth), + Some(auth), + config.http_state.clone(), + ); + let mut request_headers = HeaderMap::new(); + auth_provider.add_auth_headers_for_url(url, &mut request_headers); + (auth_provider, request_headers) +} + +pub(crate) fn observe_response_headers( + auth_provider: &SharedAuthProvider, + url: &str, + request_headers: &HeaderMap, + response_headers: &HeaderMap, +) { + auth_provider.observe_response_headers(url, request_headers, response_headers); +} + +#[cfg(test)] +#[path = "http_state_tests.rs"] +mod tests; diff --git a/codex-rs/core-plugins/src/remote/http_state_tests.rs b/codex-rs/core-plugins/src/remote/http_state_tests.rs new file mode 100644 index 0000000000..f874c2fe43 --- /dev/null +++ b/codex-rs/core-plugins/src/remote/http_state_tests.rs @@ -0,0 +1,42 @@ +use super::*; +use codex_http_state::HttpStateContext; +use codex_http_state::HttpStateSurface; +use pretty_assertions::assert_eq; +use reqwest::header::HeaderValue; +use tempfile::TempDir; + +const REQUEST_URL: &str = "https://chatgpt.com/backend-api/ps/plugins/list"; + +#[test] +fn plugin_request_headers_attach_and_rotate_native_integrity_state() { + let codex_home = TempDir::new().expect("create temp dir"); + let state = HttpStateContext::new(codex_home.path().to_path_buf(), HttpStateSurface::CodexCli); + state + .set_for_surface(HttpStateSurface::CodexCli, "state-before".to_string()) + .expect("seed state"); + let config = RemotePluginServiceConfig::new("https://chatgpt.com/backend-api".to_string()) + .with_http_state(state.clone()); + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + + let (auth_provider, request_headers) = request_headers(&config, &auth, REQUEST_URL); + assert_eq!( + request_headers.get("X-OAI-IS"), + Some(&HeaderValue::from_static("state-before")) + ); + + let mut response_headers = HeaderMap::new(); + response_headers.insert("X-OAI-IS-Update", HeaderValue::from_static("state-after")); + observe_response_headers( + &auth_provider, + REQUEST_URL, + &request_headers, + &response_headers, + ); + + assert_eq!( + state + .get_for_surface(HttpStateSurface::CodexCli) + .expect("read state"), + Some("state-after".to_string()) + ); +} diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index c400d27c47..c462b7d7fc 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -4,7 +4,6 @@ use crate::plugin_bundle_archive::pack_plugin_bundle_tar_gz; use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; use codex_utils_absolute_path::AbsolutePathBuf; -use reqwest::RequestBuilder; use reqwest::StatusCode; use serde::Deserialize; use serde::Serialize; @@ -280,7 +279,7 @@ pub async fn delete_remote_plugin_share( 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 request = authenticated_request(client.delete(&url), auth)?; + let request = authenticated_request(config, client.delete(&url), auth, &url); 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) { warn!( @@ -313,7 +312,7 @@ pub async fn update_remote_plugin_share_targets( 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 request = authenticated_request(client.put(&url), auth)?.json( + let request = authenticated_request(config, client.put(&url), auth, &url).json( &RemotePluginShareUpdateTargetsRequest { discoverability, targets, @@ -379,7 +378,7 @@ async fn get_created_workspace_plugins_page( 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 mut request = authenticated_request(client.get(&url), auth)?; + let mut request = authenticated_request(config, client.get(&url), auth, &url); request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]); if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); @@ -397,7 +396,7 @@ async fn create_workspace_plugin_upload( 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 request = authenticated_request(client.post(&url), auth)?.json( + let request = authenticated_request(config, client.post(&url), auth, &url).json( &RemoteWorkspacePluginUploadUrlRequest { filename, mime_type: "application/gzip", @@ -451,7 +450,7 @@ async fn finalize_workspace_plugin_upload( format!("{base_url}/public/plugins/workspace") }; let client = build_reqwest_client(); - let request = authenticated_request(client.post(&url), auth)?.json(&body); + let request = authenticated_request(config, client.post(&url), auth, &url).json(&body); send_and_decode(request, &url).await } @@ -489,17 +488,11 @@ fn archive_plugin_for_upload_with_limit( } async fn send_and_expect_status( - request: RequestBuilder, + request: AuthenticatedRequest, url_for_error: &str, expected_statuses: &[StatusCode], ) -> Result<(), RemotePluginCatalogError> { - let response = request - .send() - .await - .map_err(|source| RemotePluginCatalogError::Request { - url: url_for_error.to_string(), - source, - })?; + let response = request.send(url_for_error).await?; let status = response.status(); let body = response.text().await.unwrap_or_default(); if !expected_statuses.contains(&status) { diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index e33d3c5d51..1f6ee85f26 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -23,9 +23,7 @@ use wiremock::matchers::query_param; use wiremock::matchers::query_param_is_missing; fn test_config(server: &MockServer) -> RemotePluginServiceConfig { - RemotePluginServiceConfig { - chatgpt_base_url: format!("{}/backend-api", server.uri()), - } + RemotePluginServiceConfig::new(format!("{}/backend-api", server.uri())) } fn test_auth() -> CodexAuth { diff --git a/codex-rs/core-plugins/src/remote_legacy.rs b/codex-rs/core-plugins/src/remote_legacy.rs index dcf9f79eb8..177d91e0b1 100644 --- a/codex-rs/core-plugins/src/remote_legacy.rs +++ b/codex-rs/core-plugins/src/remote_legacy.rs @@ -1,4 +1,5 @@ use crate::remote::RemotePluginServiceConfig; +use crate::remote::http_state; use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; use codex_protocol::protocol::Product; @@ -130,10 +131,11 @@ 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 (auth_provider, request_headers) = http_state::request_headers(config, auth, &url); let request = client .get(&url) .timeout(REMOTE_PLUGIN_FETCH_TIMEOUT) - .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); + .headers(request_headers.clone()); let response = request .send() @@ -142,6 +144,12 @@ pub async fn fetch_remote_plugin_status( url: url.clone(), source, })?; + http_state::observe_response_headers( + &auth_provider, + &url, + &request_headers, + response.headers(), + ); let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { @@ -170,9 +178,11 @@ pub async fn fetch_remote_featured_plugin_ids( )]) .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT); - if let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) { - request = - request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); + let auth_context = auth + .filter(|auth| auth.uses_codex_backend()) + .map(|auth| http_state::request_headers(config, auth, &url)); + if let Some((_, request_headers)) = &auth_context { + request = request.headers(request_headers.clone()); } let response = request @@ -182,6 +192,14 @@ pub async fn fetch_remote_featured_plugin_ids( url: url.clone(), source, })?; + if let Some((auth_provider, request_headers)) = &auth_context { + http_state::observe_response_headers( + auth_provider, + &url, + request_headers, + response.headers(), + ); + } let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { @@ -237,10 +255,11 @@ async fn post_remote_plugin_mutation( let auth = ensure_codex_backend_auth(auth)?; let url = remote_plugin_mutation_url(config, plugin_id, action)?; let client = build_reqwest_client(); + let (auth_provider, request_headers) = http_state::request_headers(config, auth, &url); let request = client .post(url.clone()) .timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT) - .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()); + .headers(request_headers.clone()); let response = request .send() @@ -249,6 +268,12 @@ async fn post_remote_plugin_mutation( url: url.clone(), source, })?; + http_state::observe_response_headers( + &auth_provider, + &url, + &request_headers, + response.headers(), + ); let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 992bf93f45..1a4601449a 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -320,11 +320,11 @@ remote_plugin = true let mut config = load_plugins_config(codex_home.path()).await; config.chatgpt_base_url = format!("{}/backend-api", server.uri()); let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf()); + let remote_plugin_service_config = + RemotePluginServiceConfig::new(config.chatgpt_base_url.clone()); fetch_and_cache_global_remote_plugin_catalog( codex_home.path(), - &RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }, + &remote_plugin_service_config, Some(&auth), ) .await