Extend runtime auth to account, plugin, and file requests (#59564)

## What changed

- Use the shared runtime and effective configuration for account API clients and remote plugin requests, including standalone plugin commands.
- Support dual-header and assertion-only authentication for remote plugins and MCP file uploads when runtime agent instances are enabled.
- Keep plugin credentials and caches scoped to the thread's account when resuming with different credentials. Bind request pools and multi-step plugin storage operations to the selected account so account replacement revokes stale operations.
- Protect runtime credentials on cross-origin redirects while preserving configured routing policies and legacy authentication for custom backends.
- Apply server-provided `upload_headers` to file storage uploads.

## Testing

Add coverage for shared runtime registration, both authentication modes, configuration reloads, resumed-thread account selection, account replacement during plugin uploads and downloads, and credential stripping on redirects. Extend file upload tests to verify runtime assertions and storage-only upload headers.

GitOrigin-RevId: 68aef81051a6f9bf919fffda52f372b108fce805
This commit is contained in:
cooper-oai
2026-11-05 03:49:13 +00:00
committed by copyberry
parent c90354172c
commit 5c0b1ac71f
55 changed files with 2145 additions and 252 deletions
+143 -57
View File
@@ -19,6 +19,7 @@ use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::ScheduledTaskSummary;
use codex_app_server_protocol::SkillInterface;
use codex_features::RuntimeAuthMode;
use codex_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_http_client::RequestBuilder;
@@ -32,7 +33,9 @@ use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use codex_plugin::app_connector_ids_from_declarations;
use codex_plugin::prompt_safe_plugin_description;
use codex_runtime_agent_instance::RuntimeAuthSlot;
use codex_utils_absolute_path::AbsolutePathBuf;
use http::HeaderMap;
use http::Method;
use http::StatusCode;
use serde::Deserialize;
@@ -153,13 +156,20 @@ pub struct RemotePluginServiceConfig {
pub chatgpt_base_url: String,
pub(crate) product_sku: String,
pub(crate) http_clients: Arc<dyn HttpClientSelector>,
pub(crate) runtime_auth: Option<(RuntimeAuthSlot, RuntimeAuthMode)>,
}
impl RemotePluginServiceConfig {
/// Keeps metadata and subsequent storage requests on the account that starts the operation.
pub(crate) fn for_current_account(&self) -> Self {
Self {
http_clients: self.http_clients.for_current_account(),
..self.clone()
}
}
/// Creates remote plugin service state from the effective application HTTP configuration.
///
/// Keeping the factory mandatory ensures every catalog, mutation, upload, and bundle request
/// follows the same outbound proxy policy.
/// Every catalog, mutation, upload, and bundle request retains the configured proxy policy.
pub fn new(
chatgpt_base_url: String,
http_client_factory: HttpClientFactory,
@@ -170,13 +180,109 @@ impl RemotePluginServiceConfig {
http_client_factory,
ClientRouteClass::Api,
);
let http_clients: Arc<dyn HttpClientSelector> = Arc::new(http_clients);
Self {
chatgpt_base_url,
product_sku: product_sku.unwrap_or_else(|| CODEX_PRODUCT_SKU.to_string()),
http_clients: Arc::new(http_clients),
http_clients,
runtime_auth: None,
}
}
#[cfg(test)]
pub(crate) fn new_with_runtime_auth(
chatgpt_base_url: String,
http_client_factory: HttpClientFactory,
product_sku: Option<String>,
runtime_auth: (RuntimeAuthSlot, RuntimeAuthMode),
) -> Self {
if codex_http_client::classify_openai_api_url(&chatgpt_base_url)
== codex_http_client::OpenAiDestination::Other
{
return Self::new(chatgpt_base_url, http_client_factory, product_sku);
}
Self::new_with_selected_runtime_auth(
chatgpt_base_url,
http_client_factory,
product_sku,
runtime_auth,
)
}
#[cfg(test)]
pub(crate) fn new_with_selected_runtime_auth(
chatgpt_base_url: String,
http_client_factory: HttpClientFactory,
product_sku: Option<String>,
runtime_auth: (RuntimeAuthSlot, RuntimeAuthMode),
) -> Self {
let mut config = Self::new(
chatgpt_base_url,
http_client_factory.with_sensitive_header_protection(),
product_sku,
);
config.runtime_auth = Some(runtime_auth);
config
}
// Keep runtime registration out of callers' async stack frames.
pub(crate) fn authenticated_request<'a>(
&'a self,
method: Method,
url: &'a str,
auth: &'a CodexAuth,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<RequestBuilder, codex_api::AuthError>>
+ Send
+ 'a,
>,
> {
Box::pin(async move {
if self.runtime_auth.is_some() {
codex_model_provider::validate_runtime_destination(Some(&self.chatgpt_base_url))
.map_err(|error| codex_api::AuthError::Build(error.to_string()))?;
}
let runtime = self
.runtime_auth
.as_ref()
.map(|(slot, mode)| {
slot.get()
.map(|manager| (Arc::clone(manager), *mode))
.ok_or_else(|| {
codex_api::AuthError::Build(
"runtime authentication has not been initialized".to_string(),
)
})
})
.transpose()?;
let resolved = codex_model_provider::auth_provider_for_request(
auth,
runtime.as_ref(),
&HeaderMap::new(),
)
.await?;
let request = match &resolved.runtime {
Some(runtime) => self
.http_clients
.request_for_account(
method,
url,
runtime.http_client_factory().network_policy(),
)
.ok_or_else(|| {
codex_api::AuthError::Build(
"plugin credentials and transport must share the application network policy"
.to_string(),
)
})?,
None => self.http_clients.request(method, url),
};
let headers = resolved.auth.apply_auth_headers(HeaderMap::new()).await?;
Ok(request.headers(default_headers()).headers(headers))
})
}
pub(crate) fn http_request(&self, method: Method, url: &str) -> RequestBuilder {
self.http_clients
.request(method, url)
@@ -188,6 +294,13 @@ impl PartialEq for RemotePluginServiceConfig {
fn eq(&self, other: &Self) -> bool {
self.chatgpt_base_url == other.chatgpt_base_url
&& self.product_sku == other.product_sku
&& match (&self.runtime_auth, &other.runtime_auth) {
(Some((left, left_mode)), Some((right, right_mode))) => {
Arc::ptr_eq(left, right) && left_mode == right_mode
}
(None, None) => true,
_ => false,
}
&& self.http_clients.outbound_proxy_policy()
== other.http_clients.outbound_proxy_policy()
}
@@ -1045,12 +1158,9 @@ pub async fn fetch_recommended_plugins(
.map_err(RemotePluginCatalogError::InvalidBaseUrl)?;
url.query_pairs_mut().append_pair("scope", "GLOBAL");
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?
.timeout(RECOMMENDED_PLUGINS_TIMEOUT);
let request = authenticated_request(config, Method::GET, &url, auth)
.await?
.timeout(RECOMMENDED_PLUGINS_TIMEOUT);
let response: RecommendedPluginsResponse = send_and_decode(request, &url).await?;
Ok(recommended_plugins_mode(response))
}
@@ -1445,11 +1555,7 @@ pub async fn fetch_remote_plugin_skill_detail(
}
let url = remote_plugin_skill_detail_url(config, plugin_id, skill_name)?;
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::GET, &url, auth).await?;
let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?;
if response.plugin_id != plugin_id {
return Err(RemotePluginCatalogError::UnexpectedPluginId {
@@ -1656,11 +1762,7 @@ async fn install_remote_plugin_inner(
url.query_pairs_mut()
.append_pair("includeAppsNeedingAuth", "true");
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::POST, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::POST, &url, auth).await?;
let request = if let Some(install_attempt_id) = install_attempt_id {
request.json(&RemotePluginInstallRequest { install_attempt_id })
} else {
@@ -1753,11 +1855,7 @@ pub async fn uninstall_remote_plugin(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/uninstall");
let request = authenticated_request(
config.http_request(Method::POST, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::POST, &url, auth).await?;
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
if response.id != remote_plugin_id {
return Err(RemotePluginCatalogError::UnexpectedPluginId {
@@ -2265,11 +2363,7 @@ async fn get_remote_plugin_list_page(
url.query_pairs_mut().append_pair("pageToken", page_token);
}
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::GET, &url, auth).await?;
send_and_decode(request, &url).await
}
@@ -2287,11 +2381,7 @@ async fn get_remote_shared_workspace_plugins_page(
url.query_pairs_mut().append_pair("pageToken", page_token);
}
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::GET, &url, auth).await?;
send_and_decode(request, &url).await
}
@@ -2324,11 +2414,7 @@ async fn get_remote_plugin_installed_page(
url.query_pairs_mut().append_pair("pageToken", page_token);
}
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?;
let request = authenticated_request(config, Method::GET, &url, auth).await?;
send_and_decode(request, &url).await
}
@@ -2363,12 +2449,9 @@ async fn fetch_plugin_detail_with_timeout(
.append_pair("includeDownloadUrls", "true");
}
let url = url.to_string();
let request = authenticated_request(
config.http_request(Method::GET, &url),
auth,
&config.product_sku,
)?
.timeout(timeout);
let request = authenticated_request(config, Method::GET, &url, auth)
.await?
.timeout(timeout);
send_and_decode(request, &url).await
}
@@ -2403,20 +2486,19 @@ fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePlu
Ok(auth)
}
fn authenticated_request(
request: RequestBuilder,
async fn authenticated_request(
config: &RemotePluginServiceConfig,
method: Method,
url: &str,
auth: &CodexAuth,
product_sku: &str,
) -> Result<RequestBuilder, RemotePluginCatalogError> {
Ok(request
Ok(config
.authenticated_request(method, url, auth)
.await
.map_err(std::io::Error::other)
.map_err(RemotePluginCatalogError::AuthToken)?
.timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT)
.headers(
codex_model_provider::auth_provider_from_auth(auth)
.to_auth_headers()
.map_err(std::io::Error::other)
.map_err(RemotePluginCatalogError::AuthToken)?,
)
.header(OAI_PRODUCT_SKU_HEADER, product_sku))
.header(OAI_PRODUCT_SKU_HEADER, &config.product_sku))
}
async fn send_and_decode<T: for<'de> Deserialize<'de>>(
@@ -2451,3 +2533,7 @@ async fn send_and_decode<T: for<'de> Deserialize<'de>>(
source,
})
}
#[cfg(test)]
#[path = "remote_runtime_auth_tests.rs"]
mod runtime_auth_tests;