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:
jif
2026-10-20 23:03:56 +00:00
committed by copyberry
parent 6fb1c3903d
commit feadd9441e
20 changed files with 468 additions and 85 deletions
+20 -2
View File
@@ -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(&current.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,