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
@@ -2211,8 +2211,17 @@ impl ThreadRequestProcessor {
"thread/revert only supports paginated threads",
));
}
let mut config = thread.config().await.as_ref().clone();
// Give the replacement its own pending lifetime before removal revokes the old runtime.
let ema_cancellation = config
.config_layer_stack
.mcp_ema_cancellation()
.for_startup();
config.config_layer_stack = config
.config_layer_stack
.with_mcp_ema_cancellation(ema_cancellation);
let runtime_snapshot = ThreadRevertRuntimeSnapshot {
config: thread.config().await.as_ref().clone(),
config,
settings: thread.restorable_thread_settings().await,
client_mcp_extensions: thread.client_mcp_extensions(),
};