mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Preserve enterprise MCP authorization across runtime refreshes (#55696)
## Why Configuration reloads must cancel stale enterprise MCP startups while preserving access for running threads that still pass validation. Replacing a runtime during `thread/revert` must also leave the replacement able to use enterprise tools. ## What changed - Separate pending startup cancellation from published thread lifetimes, with independent startup state for forks and replacement runtimes. - Retain eligible enterprise MCP connections across refreshes and explicit reconnects while applying updated tool restrictions and approvals. Do not create replacement enterprise connections after initial startup. - Revoke enterprise authorization when a thread is removed, drain requests when access is disabled, and revoke a connection's authority when token renewal requires authentication. ## Testing Extend regression coverage to verify enterprise tool calls survive `thread/revert`, inherited fork configurations are cancelled after a reload, and enterprise calls fail after logout while ordinary MCP calls remain available. GitOrigin-RevId: 2d634be3011854b57f73c741bd1ec6b5aed94ee5
This commit is contained in:
@@ -283,6 +283,7 @@ impl McpConnectionSet {
|
||||
/// notifications are then skipped and interactive elicitations are declined.
|
||||
pub async fn new(
|
||||
ema_auth: Option<crate::ema_auth::EmaAuthContext>,
|
||||
existing_ema: Option<&Self>,
|
||||
previous: Option<&Self>,
|
||||
publication_gate: McpPublicationGate,
|
||||
input: McpRuntimeInput,
|
||||
@@ -337,15 +338,27 @@ impl McpConnectionSet {
|
||||
// Explicit reconnects have no previous set and must replace their clients eagerly.
|
||||
let allow_deferred_startup =
|
||||
startup_policy == McpStartupPolicy::LazyWhenCached && previous.is_some();
|
||||
let reusable_previous = previous.filter(|previous| {
|
||||
!previous.servers.is_empty()
|
||||
&& previous.elicitation_requests.update(
|
||||
Arc::clone(&config),
|
||||
elicitation_reviewer.clone(),
|
||||
elicitation_lifecycle.clone(),
|
||||
)
|
||||
});
|
||||
let elicitation_requests = if let Some(previous) = reusable_previous {
|
||||
// Retained EMA clients keep callbacks attached to this manager even when
|
||||
// an explicit reconnect replaces the ordinary clients.
|
||||
let elicitation_previous = previous
|
||||
.or_else(|| {
|
||||
existing_ema.filter(|previous| {
|
||||
previous
|
||||
.servers
|
||||
.values()
|
||||
.any(|view| view.connection.client.ema_authority.is_some())
|
||||
})
|
||||
})
|
||||
.filter(|previous| {
|
||||
!previous.servers.is_empty()
|
||||
&& previous.elicitation_requests.update(
|
||||
Arc::clone(&config),
|
||||
elicitation_reviewer.clone(),
|
||||
elicitation_lifecycle.clone(),
|
||||
)
|
||||
});
|
||||
let reusable_previous = previous.filter(|_| elicitation_previous.is_some());
|
||||
let elicitation_requests = if let Some(previous) = elicitation_previous {
|
||||
previous.elicitation_requests.clone()
|
||||
} else {
|
||||
ElicitationRequestManager::new(
|
||||
@@ -557,6 +570,67 @@ impl McpConnectionSet {
|
||||
Some(_) => None,
|
||||
},
|
||||
};
|
||||
if matches!(configured_config.auth, McpServerAuth::EmaAuth) && existing_ema.is_some() {
|
||||
if let Some(previous_view) = existing_ema
|
||||
.filter(|_| elicitation_previous.is_some())
|
||||
.and_then(|previous| previous.servers.get(&server_name))
|
||||
{
|
||||
let connection = &previous_view.connection;
|
||||
let authority_is_current = || {
|
||||
connection
|
||||
.client
|
||||
.ema_authority
|
||||
.as_ref()
|
||||
.is_some_and(|authority| authority.check_new_request().is_ok())
|
||||
};
|
||||
let same_identity = connection.identity.as_ref() == Some(&connection_identity)
|
||||
&& previous_view.catalog_item_limit == catalog_item_limit
|
||||
&& previous_view.protocol_mode == protocol_mode;
|
||||
let pending = same_identity
|
||||
&& !connection.client.startup_complete.load(Ordering::Acquire)
|
||||
&& connection.startup_timeout == startup_timeout
|
||||
&& !connection.startup_is_dormant()
|
||||
&& !connection.client.cancel_token.is_cancelled();
|
||||
let ready = same_identity
|
||||
&& !pending
|
||||
&& connection
|
||||
.reusable_client(&connection_identity)
|
||||
.await
|
||||
.is_some_and(|client| {
|
||||
expected_protocol_mode == Some(client.client.protocol_mode())
|
||||
});
|
||||
if authority_is_current() && (pending || ready) {
|
||||
servers.insert(
|
||||
server_name.clone(),
|
||||
McpServerView {
|
||||
connection: Arc::clone(connection),
|
||||
protocol_mode,
|
||||
startup_readiness: configured_config.startup_readiness,
|
||||
refresh_tool_list: config.mcp_tool_list_changed,
|
||||
refresh_timeout: startup_timeout,
|
||||
metadata,
|
||||
tool_filter: configured_tool_filter,
|
||||
tool_timeout: configured_tool_timeout,
|
||||
catalog_item_limit,
|
||||
},
|
||||
);
|
||||
if pending {
|
||||
let client = connection.client.clone();
|
||||
let publication_gate = publication_gate.clone();
|
||||
join_set.spawn(async move {
|
||||
if !publication_gate.wait().await {
|
||||
return (server_name, Err(StartupOutcomeError::Cancelled));
|
||||
}
|
||||
(server_name, client.client().await)
|
||||
});
|
||||
} else {
|
||||
reused_ready.push(server_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Post-start publications may only retain the original EMA connection.
|
||||
continue;
|
||||
}
|
||||
if let Some(previous_view) =
|
||||
reusable_previous.and_then(|previous| previous.servers.get(&server_name))
|
||||
&& previous_view.refresh_tool_list == config.mcp_tool_list_changed
|
||||
|
||||
@@ -579,6 +579,7 @@ async fn prepared_call_timeout_includes_trusted_access_lookup() {
|
||||
|
||||
pub(crate) async fn create_ready_async_managed_client(tools: Vec<ToolInfo>) -> AsyncManagedClient {
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Ok(
|
||||
create_test_managed_client(tools).await,
|
||||
))
|
||||
@@ -749,6 +750,7 @@ fn create_gated_async_managed_client(
|
||||
|
||||
(
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -817,6 +819,7 @@ pub(crate) async fn create_test_manager_with_ready_apps_client(
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Ok(
|
||||
managed_client,
|
||||
))
|
||||
@@ -873,6 +876,7 @@ fn create_test_manager_with_failed_apps_startup(
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -2060,6 +2064,7 @@ async fn read_only_apps_discovery_never_uses_a_shared_writable_catalog() -> anyh
|
||||
cancellation.cancel();
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -2134,6 +2139,7 @@ async fn hosted_apps_protocol_mode_is_independent_of_generic_mode() -> anyhow::R
|
||||
startup_cancellation_token.cancel();
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -2241,6 +2247,7 @@ async fn codex_apps_extension_does_not_share_host_owned_tools_cache() -> anyhow:
|
||||
startup_cancellation_token.cancel();
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -2318,6 +2325,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_while_client_is_pending() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -2390,6 +2398,7 @@ async fn capture_binding_preserves_ready_client_catalog_and_shares_prepared_meta
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::ready(Ok(ready_client)).boxed().shared(),
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -2786,6 +2795,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -2883,6 +2893,7 @@ async fn capture_binding_exposes_cached_tools_before_startup() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -2983,6 +2994,7 @@ async fn capture_binding_skips_pending_optional_servers_after_configured_shared_
|
||||
manager.insert_test_client(
|
||||
server_name.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared(),
|
||||
@@ -3235,6 +3247,7 @@ async fn capture_binding_shares_optional_startup_grace_across_connection_sets()
|
||||
manager.insert_test_client(
|
||||
"pending",
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared(),
|
||||
@@ -3916,6 +3929,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tools() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -3974,6 +3988,7 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -4021,6 +4036,7 @@ async fn shutdown_continues_after_caller_is_aborted() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: blocking_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -4074,6 +4090,7 @@ async fn list_all_tools_does_not_block_when_shared_codex_apps_cache_is_empty() {
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -4127,6 +4144,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_when_client_startup_fails()
|
||||
manager.insert_test_client(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: failed_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -4814,6 +4832,7 @@ async fn executor_owned_chatgpt_mcp_accepts_only_safe_explicit_authorization() -
|
||||
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -4933,6 +4952,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
let cancel_token = CancellationToken::new();
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -5382,6 +5402,7 @@ async fn reconcile_reusable_server_with_mcp_config(
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
Some(previous),
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -5778,6 +5799,7 @@ async fn reconciliation_reuses_connection_without_relisting_regular_tools() -> a
|
||||
connection: Arc::new(McpServerConnection {
|
||||
identity: Some(reusable_server_identity("docs", &config, &runtime_context)),
|
||||
client: AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::ready(Ok(managed_client)).boxed().shared(),
|
||||
is_codex_apps_mcp_server: false,
|
||||
server_capabilities: Arc::new(std::sync::Mutex::new(None)),
|
||||
@@ -6339,6 +6361,7 @@ async fn reconciliation_replaces_connection_when_protocol_mode_changes() {
|
||||
|
||||
let reconciled = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
Some(&previous),
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -6398,6 +6421,7 @@ async fn reconciliation_reuses_legacy_stdio_server_when_modern_protocol_is_enabl
|
||||
|
||||
let reconciled = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
Some(&previous),
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -6597,6 +6621,7 @@ async fn reconciliation_replaces_closed_connections() -> anyhow::Result<()> {
|
||||
view.connection = Arc::new(McpServerConnection {
|
||||
identity: Some(reusable_server_identity("docs", &config, &runtime_context)),
|
||||
client: AsyncManagedClient {
|
||||
ema_authority: None,
|
||||
client: futures::future::ready(Ok(connected_client))
|
||||
.boxed()
|
||||
.shared(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Dedicated enterprise grants and one thread's EMA authorization lifetime.
|
||||
//! Account credentials identify the owner; only the separate keyring grant is exchanged.
|
||||
//! One thread's EMA authorization lifetime and guarded HTTP dispatch.
|
||||
//! Account and delivered-policy changes cancel the same owner and its admitted requests.
|
||||
//! Credential selection and recovery belong to each connection's token source.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -7,7 +8,6 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use codex_config::CloudConfigBundleBindingStatus;
|
||||
use codex_config::McpEmaAuthScope;
|
||||
use codex_config::McpEmaRegistration;
|
||||
use codex_config::McpServerIdpOAuthConfig;
|
||||
@@ -51,6 +51,12 @@ use crate::rmcp_client::StartupOutcomeError;
|
||||
|
||||
pub(crate) type EmaAuthContext = Result<Arc<EmaAuth>, StartupOutcomeError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EmaConnectionAuth {
|
||||
pub(crate) owner: Arc<EmaAuth>,
|
||||
pub(crate) authority: EmaRequestAuthority,
|
||||
}
|
||||
|
||||
pub fn ema_auth_scope(auth: Option<&CodexAuth>) -> Option<McpEmaAuthScope> {
|
||||
let auth = auth?;
|
||||
McpEmaAuthScope::new(auth.get_chatgpt_user_id()?, auth.get_account_id()?)
|
||||
@@ -58,11 +64,11 @@ pub fn ema_auth_scope(auth: Option<&CodexAuth>) -> Option<McpEmaAuthScope> {
|
||||
|
||||
/// Retained by the thread across MCP refreshes; a revoked owner cannot be replaced.
|
||||
pub(crate) struct EmaAuth {
|
||||
scope: McpEmaAuthScope,
|
||||
config: Arc<McpConfig>,
|
||||
manager: Arc<AuthManager>,
|
||||
account: CodexAuth,
|
||||
authority: EmaRequestAuthority,
|
||||
pub(crate) scope: McpEmaAuthScope,
|
||||
pub(crate) config: Arc<McpConfig>,
|
||||
pub(crate) manager: Arc<AuthManager>,
|
||||
pub(crate) account: CodexAuth,
|
||||
pub(crate) authority: EmaRequestAuthority,
|
||||
_revocation: AbortOnDropHandle<()>,
|
||||
}
|
||||
|
||||
@@ -84,24 +90,23 @@ impl EmaAuth {
|
||||
}
|
||||
let cancellation = config.mcp_ema_cancellation.clone();
|
||||
let policy = config.config_layer_stack.cloud_config_binding().cloned();
|
||||
let authority = EmaRequestAuthority::new(cancellation.clone(), {
|
||||
let authority = EmaRequestAuthority::new(cancellation.token(), {
|
||||
let cancellation = cancellation.clone();
|
||||
let changes = changes.clone();
|
||||
let policy = policy.clone();
|
||||
move || {
|
||||
changes.borrow().login_generation == login_generation
|
||||
&& policy.as_ref().is_none_or(|binding| {
|
||||
binding.read().status == CloudConfigBundleBindingStatus::Current
|
||||
})
|
||||
!cancellation.is_cancelled()
|
||||
&& changes.borrow().login_generation == login_generation
|
||||
}
|
||||
});
|
||||
authority.check()?;
|
||||
})
|
||||
.with_policy(policy.clone());
|
||||
authority.check_new_request()?;
|
||||
let revocation = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = cancellation.cancelled() => {}
|
||||
() = async {
|
||||
match policy {
|
||||
Some(binding) => binding.invalidated().await,
|
||||
Some(policy) => policy.invalidated().await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
} => {}
|
||||
@@ -142,12 +147,13 @@ impl EmaAuth {
|
||||
|
||||
pub(crate) async fn connect(
|
||||
self: &Arc<Self>,
|
||||
authority: EmaRequestAuthority,
|
||||
server_name: &str,
|
||||
server: &EffectiveMcpServer,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
protocol_mode: McpProtocolMode,
|
||||
) -> Result<RmcpClient> {
|
||||
self.check_account().await?;
|
||||
self.authority.check()?;
|
||||
let config = server.config();
|
||||
config
|
||||
.validate_ema_auth_transport()
|
||||
@@ -176,8 +182,8 @@ impl EmaAuth {
|
||||
.await?;
|
||||
let exchange_client: Arc<dyn HttpClient> = Arc::new(EnterpriseExchangeHttpClient {
|
||||
inner: Arc::clone(&http_client),
|
||||
authority: self.authority.clone(),
|
||||
cancellation: self.config.mcp_ema_cancellation.clone(),
|
||||
authority: authority.clone(),
|
||||
cancellation: self.config.mcp_ema_cancellation.token(),
|
||||
});
|
||||
let url = registration.server_url().to_owned();
|
||||
let source = EnterpriseTokenSource {
|
||||
@@ -192,7 +198,7 @@ impl EmaAuth {
|
||||
http_client,
|
||||
source,
|
||||
token,
|
||||
self.authority.clone(),
|
||||
authority,
|
||||
));
|
||||
RmcpClient::new_streamable_http_client_with_protocol_mode_and_redirect_mode(
|
||||
server_name,
|
||||
@@ -258,7 +264,7 @@ impl HttpClient for EnterpriseExchangeHttpClient {
|
||||
params: HttpRequestParams,
|
||||
) -> BoxFuture<'_, Result<HttpRequestResponse, ExecServerError>> {
|
||||
Box::pin(async move {
|
||||
self.authority.check()?;
|
||||
self.authority.check_new_request()?;
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = self.cancellation.cancelled() => Err(ExecServerError::HttpRequest("enterprise MCP authorization was revoked".into())),
|
||||
@@ -275,7 +281,7 @@ impl HttpClient for EnterpriseExchangeHttpClient {
|
||||
params: HttpRequestParams,
|
||||
) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>> {
|
||||
Box::pin(async move {
|
||||
self.authority.check()?;
|
||||
self.authority.check_new_request()?;
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = self.cancellation.cancelled() => Err(ExecServerError::HttpRequest("enterprise MCP authorization was revoked".into())),
|
||||
|
||||
@@ -133,7 +133,7 @@ pub struct McpConfig {
|
||||
/// Trusted enterprise IdP inherited after normal catalog and policy resolution.
|
||||
pub mcp_enterprise_managed_auth: Option<McpEnterpriseManagedAuthConfig>,
|
||||
/// Retires only EMA connections and pending exchanges for this config snapshot.
|
||||
pub mcp_ema_cancellation: tokio_util::sync::CancellationToken,
|
||||
pub mcp_ema_cancellation: codex_config::McpEmaCancellation,
|
||||
pub xaa_enabled: bool,
|
||||
/// Preferred credential store for MCP OAuth tokens.
|
||||
pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode,
|
||||
@@ -464,6 +464,7 @@ pub async fn read_mcp_resource(
|
||||
let runtime_config = config.for_threadless_operations(&mcp_servers);
|
||||
let manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
@@ -547,6 +548,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
let runtime_config = config.for_threadless_operations(&mcp_servers);
|
||||
let mcp_connection_manager = McpConnectionSet::new(
|
||||
/*ema_auth*/ None,
|
||||
/*existing_ema*/ None,
|
||||
/*previous*/ None,
|
||||
McpPublicationGate::already_published(),
|
||||
McpRuntimeInput {
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
requires_read_only_mcp_tools: false,
|
||||
codex_home,
|
||||
mcp_enterprise_managed_auth: None,
|
||||
mcp_ema_cancellation: tokio_util::sync::CancellationToken::new(),
|
||||
mcp_ema_cancellation: codex_config::McpEmaCancellation::default(),
|
||||
xaa_enabled: false,
|
||||
mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::default(),
|
||||
oauth_refresh_mode: McpOAuthRefreshMode::Legacy,
|
||||
|
||||
@@ -297,7 +297,7 @@ fn codex_apps_reconnect_backoff(consecutive_failures: u32) -> Duration {
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ManagedClientStartup {
|
||||
ema_auth: Option<crate::ema_auth::EmaAuthContext>,
|
||||
ema_auth: Option<Result<crate::ema_auth::EmaConnectionAuth, StartupOutcomeError>>,
|
||||
originator: AuthStorageOriginator,
|
||||
server_name: String,
|
||||
server: EffectiveMcpServer,
|
||||
@@ -447,6 +447,7 @@ impl ManagedClientStartup {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AsyncManagedClient {
|
||||
pub(crate) ema_authority: Option<codex_rmcp_client::EmaRequestAuthority>,
|
||||
pub(crate) client: ManagedClientFuture,
|
||||
pub(crate) is_codex_apps_mcp_server: bool,
|
||||
pub(crate) cached_server_info: Option<McpServerInfo>,
|
||||
@@ -498,6 +499,18 @@ impl AsyncManagedClient {
|
||||
};
|
||||
let startup_complete = Arc::new(AtomicBool::new(false));
|
||||
let server_capabilities = Arc::new(StdMutex::new(None));
|
||||
let ema_auth = ema_auth
|
||||
.filter(|_| matches!(server.config().auth, McpServerAuth::EmaAuth))
|
||||
.map(|auth| {
|
||||
auth.map(|owner| crate::ema_auth::EmaConnectionAuth {
|
||||
authority: owner.authority.child(),
|
||||
owner,
|
||||
})
|
||||
});
|
||||
let ema_authority = ema_auth
|
||||
.as_ref()
|
||||
.and_then(|auth| auth.as_ref().ok())
|
||||
.map(|auth| auth.authority.clone());
|
||||
let startup = Arc::new(ManagedClientStartup {
|
||||
ema_auth,
|
||||
originator: AuthStorageOriginator::current(),
|
||||
@@ -536,6 +549,7 @@ impl AsyncManagedClient {
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
ema_authority,
|
||||
is_codex_apps_mcp_server,
|
||||
cached_server_info,
|
||||
server_capabilities,
|
||||
@@ -1241,7 +1255,7 @@ struct StartServerTaskParams {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(level = "trace", skip_all, fields(server_name = %server_name))]
|
||||
pub(crate) async fn make_rmcp_client(
|
||||
ema_auth: Option<crate::ema_auth::EmaAuthContext>,
|
||||
ema_auth: Option<Result<crate::ema_auth::EmaConnectionAuth, StartupOutcomeError>>,
|
||||
server_name: &str,
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
@@ -1277,7 +1291,9 @@ pub(crate) async fn make_rmcp_client(
|
||||
))
|
||||
})??;
|
||||
return auth
|
||||
.owner
|
||||
.connect(
|
||||
auth.authority,
|
||||
server_name,
|
||||
&server,
|
||||
runtime_context.local_http_client(),
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! This module contains the thread-owned MCP runtime and data that describes the
|
||||
//! environment in which MCP servers execute. Transport startup lives in
|
||||
//! [`crate::rmcp_client`] and connection-set behavior lives in
|
||||
//! [`crate::connection_manager`].
|
||||
//! [`crate::connection_manager`]. Registry removal revokes EMA authority before
|
||||
//! the thread becomes untracked.
|
||||
|
||||
mod status;
|
||||
|
||||
@@ -337,7 +338,24 @@ impl McpRuntime {
|
||||
self.latest_hard_refresh_codex_apps_tools_cache().await
|
||||
}
|
||||
|
||||
/// Revoke a removed thread's EMA access before releasing the registry lock.
|
||||
pub fn revoke_ema(&self) {
|
||||
if let Some(Ok(auth)) = self.ema_auth.get() {
|
||||
auth.authority.revoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain EMA responses after its configuration binding has been revoked.
|
||||
pub async fn quiesce_ema(&self) {
|
||||
if let Some(Ok(auth)) = self.ema_auth.get() {
|
||||
auth.authority.revoke();
|
||||
auth.authority.quiesce().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish(&self, input: McpRuntimeInput, previous: Option<&McpConnectionSet>) {
|
||||
let current = self.current.load_full();
|
||||
let retain_ema = current.config.is_some();
|
||||
let ema_auth = (input.config.xaa_enabled
|
||||
&& input.mcp_servers.values().any(|server| {
|
||||
server.config().enabled
|
||||
@@ -359,7 +377,6 @@ impl McpRuntime {
|
||||
let config = Arc::clone(&input.config);
|
||||
let auth = input.auth.clone();
|
||||
let auth_token = auth.as_ref().and_then(|auth| auth.get_token().ok());
|
||||
let current = self.current.load_full();
|
||||
let auth_generation = if current.auth_matches(auth.as_ref()) {
|
||||
Arc::clone(¤t.auth_generation)
|
||||
} else {
|
||||
@@ -372,6 +389,7 @@ impl McpRuntime {
|
||||
let connections = Arc::new(
|
||||
McpConnectionSet::new(
|
||||
ema_auth,
|
||||
retain_ema.then_some(current.connections.as_ref()),
|
||||
previous,
|
||||
publication_gate,
|
||||
input,
|
||||
|
||||
Reference in New Issue
Block a user