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
@@ -17,6 +17,7 @@ use codex_model_provider::is_supported_amazon_bedrock_region;
use codex_otel::auth_storage::AuthStorageOriginator;
use codex_rmcp_client::EnterpriseOAuthCredentialGuard;
mod backend_client;
mod bedrock_gov_cloud;
mod bedrock_setup;
mod enterprise_login;
@@ -290,6 +291,7 @@ impl AccountRequestProcessor {
.await
{
Ok(config) => {
let runtime_auth = thread_manager.runtime_auth_for_config(&config);
Self::spawn_effective_plugins_changed_task(
Arc::clone(thread_manager),
config_manager.clone(),
@@ -311,6 +313,10 @@ impl AccountRequestProcessor {
&plugins_config,
Some(Arc::clone(&on_effective_plugins_changed)),
);
if let Err(err) = runtime_auth {
warn!("failed to initialize runtime auth for remote plugin refresh: {err}");
return;
}
thread_manager
.plugins_manager()
.maybe_start_remote_plugin_caches_refresh(
@@ -1186,11 +1192,7 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(
self.config.chatgpt_base_url.clone(),
&auth,
http_client_factory,
);
let client = self.backend_client(&auth, http_client_factory).await?;
let usage_request = async {
if params.supports_luna_reserve
@@ -1303,11 +1305,7 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(
self.config.chatgpt_base_url.clone(),
&auth,
http_client_factory,
);
let client = self.backend_client(&auth, http_client_factory).await?;
if let Some(thread_id) = thread_id {
let thread_id = thread_id.to_string();
let usage = tokio::time::timeout(
@@ -1391,11 +1389,7 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(
self.config.chatgpt_base_url.clone(),
&auth,
http_client_factory,
);
let client = self.backend_client(&auth, http_client_factory).await?;
let messages = tokio::time::timeout(
ACCOUNT_WORKSPACE_MESSAGES_FETCH_TIMEOUT,
client.list_workspace_messages(),
@@ -1485,11 +1479,7 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(
self.config.chatgpt_base_url.clone(),
&auth,
http_client_factory,
);
let client = self.backend_client(&auth, http_client_factory).await?;
match client
.send_add_credits_nudge_email(Self::backend_credit_type(params.credit_type))
@@ -0,0 +1,31 @@
//! Build account API clients from the latest config and captured account authority.
use super::*;
use futures::future::BoxFuture;
impl AccountRequestProcessor {
// Keep config loading and runtime bootstrap out of request-handler future types.
#[inline(never)]
pub(super) fn backend_client<'a>(
&'a self,
auth: &'a CodexAuth,
http_client_factory: codex_http_client::HttpClientFactory,
) -> BoxFuture<'a, Result<BackendClient, JSONRPCErrorError>> {
Box::pin(async move {
let config = self.load_latest_config().await;
let runtime = self
.thread_manager
.runtime_auth_for_config(&config)
.map_err(|error| internal_error(error.to_string()))?;
BackendClient::from_auth_with_runtime(
config.chatgpt_base_url.clone(),
auth,
http_client_factory,
runtime.as_ref(),
/*headers*/ &Default::default(),
)
.await
.map_err(|error| internal_error(error.to_string()))
})
}
}
@@ -111,11 +111,7 @@ impl AccountRequestProcessor {
));
}
Ok(BackendClient::from_auth(
self.config.chatgpt_base_url.clone(),
&auth,
http_client_factory,
))
self.backend_client(&auth, http_client_factory).await
}
}
@@ -399,15 +399,23 @@ impl AccountRequestProcessor {
let routing = if let Some(cached) = cached {
cached
} else {
let runtime = self
.thread_manager
.runtime_auth_for_config(&config)
.map_err(|_| WorkspaceRoutingError::DiscoveryFailed)?;
let mut recovery = self.auth_manager.unauthorized_recovery();
let mut discovery_auth = auth.clone();
let mut discovery_generation = auth_state.generation;
let response = loop {
let client = BackendClient::from_auth(
let client = BackendClient::from_auth_with_runtime(
&config.chatgpt_base_url,
&discovery_auth,
config.http_client_factory(),
);
runtime.as_ref(),
/*headers*/ &Default::default(),
)
.await
.map_err(|_| WorkspaceRoutingError::DiscoveryFailed)?;
match client.get_accounts_check().await {
Ok(response) => break response,
Err(error) => {
@@ -159,8 +159,16 @@ fn load_shared_plugin_ids_by_local_path(
})
}
fn remote_plugin_service_config(config: &Config) -> RemotePluginServiceConfig {
config.plugins_config_input().remote_plugin_service_config()
fn remote_plugin_service_config(
thread_manager: &ThreadManager,
config: &Config,
) -> Result<RemotePluginServiceConfig, JSONRPCErrorError> {
let _ = thread_manager
.runtime_auth_for_config(config)
.map_err(|error| internal_error(error.to_string()))?;
Ok(thread_manager
.plugins_manager()
.remote_plugin_service_config(&config.plugins_config_input()))
}
fn share_context_for_source(
@@ -599,7 +607,8 @@ impl PluginRequestProcessor {
let include_global_remote = context.remote_plugins_enabled() && !explicit_marketplace_kinds;
let use_remote_global_catalog =
include_global_remote && auth_mode.is_some_and(DomainAuthMode::uses_codex_backend);
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let plugin_policy = &config.plugins;
let remote_catalog_cache_mode = if force_refetch {
RemotePluginCatalogCacheMode::ForceRefetch
@@ -1056,7 +1065,8 @@ impl PluginRequestProcessor {
);
let share_context = match share_context {
Some(context) => {
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
match codex_core_plugins::remote::fetch_remote_plugin_share_context(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1184,7 +1194,8 @@ impl PluginRequestProcessor {
"remote plugin read is not enabled for marketplace {remote_marketplace_name}"
)));
}
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
validate_remote_plugin_id(&plugin_name)?;
let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail(
&remote_plugin_service_config,
@@ -1246,7 +1257,8 @@ impl PluginRequestProcessor {
}
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1297,7 +1309,8 @@ impl PluginRequestProcessor {
validate_client_plugin_share_targets(share_targets)?;
}
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let access_policy = codex_core_plugins::remote::RemotePluginShareAccessPolicy {
discoverability: discoverability.map(remote_plugin_share_discoverability),
share_targets: share_targets.map(remote_plugin_share_targets),
@@ -1345,7 +1358,8 @@ impl PluginRequestProcessor {
}
validate_client_plugin_share_targets(&share_targets)?;
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let result = codex_core_plugins::remote::update_remote_plugin_share_targets(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1379,7 +1393,8 @@ impl PluginRequestProcessor {
_params: PluginShareListParams,
) -> Result<PluginShareListResponse, JSONRPCErrorError> {
let (config, auth) = self.load_plugin_share_config_and_auth().await?;
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let plugin_policy = &config.plugins;
let data = codex_core_plugins::remote::list_remote_plugin_shares(
&remote_plugin_service_config,
@@ -1417,7 +1432,8 @@ impl PluginRequestProcessor {
return Err(invalid_request("invalid remote plugin id"));
}
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
let result = codex_core_plugins::remote::checkout_remote_plugin_share(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1448,7 +1464,8 @@ impl PluginRequestProcessor {
return Err(invalid_request("invalid remote plugin id"));
}
let remote_plugin_service_config = remote_plugin_service_config(&config);
let remote_plugin_service_config =
remote_plugin_service_config(&self.thread_manager, &config)?;
codex_core_plugins::remote::delete_remote_plugin_share(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1599,6 +1616,10 @@ impl PluginRequestProcessor {
install_attempt_id: Option<String>,
) -> Result<PluginInstallResponse, JSONRPCErrorError> {
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let _ = self
.thread_manager
.runtime_auth_for_config(&config)
.map_err(|error| internal_error(error.to_string()))?;
let auth = match self.auth_manager.auth_with_http_client_factory().await {
Some((auth, factory)) => {
config.application_network_policy = factory.network_policy().clone();
@@ -2105,6 +2126,10 @@ impl PluginRequestProcessor {
plugin_id: String,
) -> Result<PluginUninstallResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let _ = self
.thread_manager
.runtime_auth_for_config(&config)
.map_err(|error| internal_error(error.to_string()))?;
let auth = self.auth_manager.auth().await;
let outcome = self
.thread_manager
@@ -78,7 +78,7 @@ impl PluginRequestProcessor {
&& let Some(remote_scope) = remote_scope
{
let page = search_remote_plugins(
&remote_plugin_service_config(&config),
&remote_plugin_service_config(&self.thread_manager, &config)?,
auth.as_ref(),
RemotePluginSearchRequest {
query: search_term,