[codex] cover app-owned HTTP requests [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-03 23:44:25 +00:00
parent ec2e111e04
commit d766866d3c
52 changed files with 1415 additions and 358 deletions
+2
View File
@@ -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 }
+88 -8
View File
@@ -58,6 +58,7 @@ use codex_config::types::PluginConfig;
use codex_config::version_for_toml;
use codex_core_skills::SkillMetadata;
use codex_hooks::plugin_hook_declarations;
use codex_http_state::HttpStateContext;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_plugin::AppConnectorId;
@@ -173,8 +174,17 @@ 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 remote_plugin_service_config_with_http_state(
config: &PluginsConfigInput,
http_state: Option<HttpStateContext>,
) -> RemotePluginServiceConfig {
let service_config = remote_plugin_service_config(config);
match http_state {
Some(http_state) => service_config.with_http_state(http_state),
None => service_config,
}
}
@@ -624,11 +634,40 @@ impl PluginsManager {
visible_scopes: &[RemotePluginScope],
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
let plugins = crate::remote::fetch_remote_installed_plugins(
&remote_plugin_service_config(config),
self.build_and_cache_remote_installed_plugin_marketplaces_with_service_config(
remote_plugin_service_config(config),
auth,
visible_scopes,
on_effective_plugins_changed,
)
.await?;
.await
}
pub async fn build_and_cache_remote_installed_plugin_marketplaces_with_http_state(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
visible_scopes: &[RemotePluginScope],
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: HttpStateContext,
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
self.build_and_cache_remote_installed_plugin_marketplaces_with_service_config(
remote_plugin_service_config(config).with_http_state(http_state),
auth,
visible_scopes,
on_effective_plugins_changed,
)
.await
}
async fn build_and_cache_remote_installed_plugin_marketplaces_with_service_config(
&self,
service_config: RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
visible_scopes: &[RemotePluginScope],
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
let plugins = crate::remote::fetch_remote_installed_plugins(&service_config, auth).await?;
let marketplaces =
crate::remote::group_remote_installed_plugins_by_marketplaces(&plugins, visible_scopes);
let changed = self.write_remote_installed_plugins_cache(plugins);
@@ -671,12 +710,14 @@ impl PluginsManager {
config: &PluginsConfigInput,
auth: Option<CodexAuth>,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: Option<HttpStateContext>,
) {
self.maybe_start_remote_installed_plugins_cache_refresh_with_notify(
config,
auth,
RemoteInstalledPluginsCacheRefreshNotify::IfCacheChanged,
on_effective_plugins_changed,
http_state,
);
}
@@ -685,12 +726,14 @@ impl PluginsManager {
config: &PluginsConfigInput,
auth: Option<CodexAuth>,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: Option<HttpStateContext>,
) {
self.maybe_start_remote_installed_plugins_cache_refresh_with_notify(
config,
auth,
RemoteInstalledPluginsCacheRefreshNotify::AfterSuccessfulRefresh,
on_effective_plugins_changed,
http_state,
);
}
@@ -700,6 +743,7 @@ impl PluginsManager {
auth: Option<CodexAuth>,
notify: RemoteInstalledPluginsCacheRefreshNotify,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: Option<HttpStateContext>,
) {
if !config.plugins_enabled {
return;
@@ -707,7 +751,7 @@ impl PluginsManager {
self.schedule_remote_installed_plugins_cache_refresh(
RemoteInstalledPluginsCacheRefreshRequest {
service_config: remote_plugin_service_config(config),
service_config: remote_plugin_service_config_with_http_state(config, http_state),
auth,
notify,
on_effective_plugins_changed,
@@ -720,6 +764,7 @@ impl PluginsManager {
config: &PluginsConfigInput,
auth: Option<CodexAuth>,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: Option<HttpStateContext>,
) {
if !config.plugins_enabled {
return;
@@ -728,17 +773,19 @@ impl PluginsManager {
let manager = Arc::clone(self);
let config_for_refresh = config.clone();
let auth_for_refresh = auth.clone();
let http_state_for_refresh = http_state.clone();
let on_local_cache_changed = Arc::new(move || {
manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
&config_for_refresh,
auth_for_refresh.clone(),
on_effective_plugins_changed.clone(),
http_state_for_refresh.clone(),
);
});
crate::remote::maybe_start_remote_installed_plugin_bundle_sync(
self.codex_home.clone(),
remote_plugin_service_config(config),
remote_plugin_service_config_with_http_state(config, http_state),
auth,
Some(on_local_cache_changed),
);
@@ -750,17 +797,20 @@ impl PluginsManager {
auth: Option<CodexAuth>,
roots: &[AbsolutePathBuf],
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
http_state: Option<HttpStateContext>,
) {
self.maybe_start_non_curated_plugin_cache_refresh(roots);
self.maybe_start_remote_installed_plugins_cache_refresh(
config,
auth.clone(),
on_effective_plugins_changed.clone(),
http_state.clone(),
);
self.maybe_start_remote_installed_plugin_bundle_sync(
config,
auth,
on_effective_plugins_changed,
http_state,
);
}
@@ -816,6 +866,34 @@ impl PluginsManager {
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
) -> Result<Vec<String>, RemotePluginFetchError> {
self.featured_plugin_ids_for_config_with_service_config(
config,
auth,
remote_plugin_service_config(config),
)
.await
}
pub async fn featured_plugin_ids_for_config_with_http_state(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
http_state: HttpStateContext,
) -> Result<Vec<String>, RemotePluginFetchError> {
self.featured_plugin_ids_for_config_with_service_config(
config,
auth,
remote_plugin_service_config(config).with_http_state(http_state),
)
.await
}
async fn featured_plugin_ids_for_config_with_service_config(
&self,
config: &PluginsConfigInput,
auth: Option<&CodexAuth>,
service_config: RemotePluginServiceConfig,
) -> Result<Vec<String>, RemotePluginFetchError> {
if !config.plugins_enabled {
return Ok(Vec::new());
@@ -826,7 +904,7 @@ impl PluginsManager {
return Ok(featured_plugin_ids);
}
let featured_plugin_ids = crate::remote_legacy::fetch_remote_featured_plugin_ids(
&remote_plugin_service_config(config),
&service_config,
auth,
self.restriction_product,
)
@@ -1570,11 +1648,13 @@ impl PluginsManager {
&config_for_remote_sync,
auth.clone(),
on_effective_plugins_changed.clone(),
/*http_state*/ None,
);
manager.maybe_start_remote_installed_plugin_bundle_sync(
&config_for_remote_sync,
auth.clone(),
on_effective_plugins_changed,
/*http_state*/ None,
);
if config_for_remote_sync.remote_plugin_enabled {
match crate::remote::fetch_and_cache_global_remote_plugin_catalog(
+76 -20
View File
@@ -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<HttpStateContext>,
}
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<RequestBuilder, RemotePluginCatalogError> {
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<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
self.request = self.request.query(query);
self
}
fn json<T: Serialize + ?Sized>(mut self, body: &T) -> Self {
self.request = self.request.json(body);
self
}
async fn send(self, url: &str) -> Result<reqwest::Response, RemotePluginCatalogError> {
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<T: for<'de> Deserialize<'de>>(
request: RequestBuilder,
request: AuthenticatedRequest,
url: &str,
) -> Result<T, RemotePluginCatalogError> {
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() {
@@ -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;
@@ -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())
);
}
+7 -14
View File
@@ -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) {
@@ -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 {
+30 -5
View File
@@ -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() {