mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +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:
@@ -343,10 +343,13 @@ pub enum CloudConfigBundleBindingStatus {
|
||||
}
|
||||
|
||||
impl CloudConfigBundleBinding {
|
||||
/// Completes when this captured policy can no longer authorize EMA requests.
|
||||
/// Wait until this delivered policy no longer authorizes its EMA owner.
|
||||
pub async fn invalidated(&self) {
|
||||
let mut changes = self.policy.0.subscribe();
|
||||
while self.read().status == CloudConfigBundleBindingStatus::Current {
|
||||
loop {
|
||||
if self.read().status != CloudConfigBundleBindingStatus::Current {
|
||||
return;
|
||||
}
|
||||
if changes.changed().await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ pub mod loader;
|
||||
mod marketplace_edit;
|
||||
mod mcp_edit;
|
||||
mod mcp_ema;
|
||||
mod mcp_ema_cancellation;
|
||||
pub use mcp_ema_cancellation::McpEmaCancellation;
|
||||
mod mcp_requirements;
|
||||
mod mcp_types;
|
||||
mod merge;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Carries EMA startup cancellation across configuration loading and thread publication.
|
||||
//! Once published, a thread's existing config refresh gate owns selective revocation.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct McpEmaCancellation(Arc<State>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct State {
|
||||
startup: CancellationToken,
|
||||
owner: CancellationToken,
|
||||
lifetime: CancellationToken,
|
||||
published: AtomicBool,
|
||||
}
|
||||
|
||||
impl McpEmaCancellation {
|
||||
/// Capture before loading configuration so reload can fence a delayed startup.
|
||||
pub fn pending(startup: CancellationToken, owner: CancellationToken) -> Self {
|
||||
let lifetime = owner.child_token();
|
||||
Self(Arc::new(State {
|
||||
startup,
|
||||
owner,
|
||||
lifetime,
|
||||
published: AtomicBool::new(false),
|
||||
}))
|
||||
}
|
||||
|
||||
/// A fork gets its own publication state, even when it inherits loaded config.
|
||||
pub fn for_startup(&self) -> Self {
|
||||
let pending = Self::pending(self.0.startup.clone(), self.0.owner.clone());
|
||||
if self.is_cancelled() {
|
||||
pending.cancel();
|
||||
}
|
||||
pending
|
||||
}
|
||||
|
||||
/// A validated session keeps its authority and records the current startup fence
|
||||
/// for later forks. The session publication gate must protect this replacement.
|
||||
pub fn refreshed(&self, incoming: &Self) -> Self {
|
||||
Self(Arc::new(State {
|
||||
startup: incoming.0.startup.clone(),
|
||||
owner: self.0.owner.clone(),
|
||||
lifetime: self.0.lifetime.clone(),
|
||||
published: AtomicBool::new(true),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Call while holding the thread registry's publication lock. A reload either
|
||||
/// cancels this pending snapshot or discovers the published thread for validation.
|
||||
pub fn publish(&self) {
|
||||
if !self.0.published.swap(true, Ordering::AcqRel) && self.0.startup.is_cancelled() {
|
||||
self.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.0.lifetime.cancel();
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.0.lifetime.is_cancelled()
|
||||
|| (!self.0.published.load(Ordering::Acquire) && self.0.startup.is_cancelled())
|
||||
}
|
||||
|
||||
pub fn token(&self) -> CancellationToken {
|
||||
self.0.lifetime.clone()
|
||||
}
|
||||
|
||||
pub async fn cancelled(&self) {
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = self.0.lifetime.cancelled() => return,
|
||||
() = self.0.startup.cancelled() => {}
|
||||
}
|
||||
if self.0.published.load(Ordering::Acquire) {
|
||||
self.0.lifetime.cancelled().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ pub struct ConfigLayerStack {
|
||||
/// Runtime-only EMA authority; it does not change the configuration's contents.
|
||||
cloud_config_binding: Option<crate::CloudConfigBundleBinding>,
|
||||
/// Runtime-only cancellation inherited by config snapshots and pending EMA startups.
|
||||
mcp_ema_cancellation: tokio_util::sync::CancellationToken,
|
||||
mcp_ema_cancellation: crate::McpEmaCancellation,
|
||||
/// Cached TOML projection derived only from `requirements_toml`.
|
||||
/// Construction validates provider definitions and reports serialization errors,
|
||||
/// so `effective_config()` can replace complete entries without a fallible conversion.
|
||||
@@ -314,7 +314,7 @@ impl ConfigLayerStack {
|
||||
)?);
|
||||
Ok(Self {
|
||||
cloud_config_binding: None,
|
||||
mcp_ema_cancellation: tokio_util::sync::CancellationToken::new(),
|
||||
mcp_ema_cancellation: crate::McpEmaCancellation::default(),
|
||||
model_provider_requirements,
|
||||
layers,
|
||||
requirements,
|
||||
@@ -349,15 +349,12 @@ impl ConfigLayerStack {
|
||||
self.cloud_config_binding.as_ref()
|
||||
}
|
||||
|
||||
pub fn with_mcp_ema_cancellation(
|
||||
mut self,
|
||||
cancellation: tokio_util::sync::CancellationToken,
|
||||
) -> Self {
|
||||
pub fn with_mcp_ema_cancellation(mut self, cancellation: crate::McpEmaCancellation) -> Self {
|
||||
self.mcp_ema_cancellation = cancellation;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mcp_ema_cancellation(&self) -> &tokio_util::sync::CancellationToken {
|
||||
pub fn mcp_ema_cancellation(&self) -> &crate::McpEmaCancellation {
|
||||
&self.mcp_ema_cancellation
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user