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:
@@ -92,8 +92,8 @@ impl Config {
|
||||
let is_enterprise = |server: &McpServerConfig| {
|
||||
matches!(server.auth, McpServerAuth::EmaAuth) || server.ema_registration().is_some()
|
||||
};
|
||||
// Refresh ordinary servers from the complete host-owned set. Keep the
|
||||
// initially admitted EMA configuration until a fresh session is constructed.
|
||||
// Refresh ordinary servers from the complete host-owned set. Keep EMA
|
||||
// admission pinned, while applying current tool restrictions and approvals.
|
||||
let mut servers: HashMap<_, _> = incoming
|
||||
.mcp_servers
|
||||
.get()
|
||||
@@ -109,7 +109,17 @@ impl Config {
|
||||
.get()
|
||||
.iter()
|
||||
.filter(|(_, server)| is_enterprise(server))
|
||||
.map(|(name, server)| (name.clone(), server.clone())),
|
||||
.map(|(name, server)| {
|
||||
let mut server = server.clone();
|
||||
if let Some(incoming) = incoming.mcp_servers.get().get(name) {
|
||||
server.enabled_tools.clone_from(&incoming.enabled_tools);
|
||||
server.disabled_tools.clone_from(&incoming.disabled_tools);
|
||||
server.omit_tools_from.clone_from(&incoming.omit_tools_from);
|
||||
server.default_tools_approval_mode = incoming.default_tools_approval_mode;
|
||||
server.tools.clone_from(&incoming.tools);
|
||||
}
|
||||
(name.clone(), server)
|
||||
}),
|
||||
);
|
||||
let mut config = self.clone();
|
||||
if !matches!(scope, RuntimeConfigRefresh::UserFiles) {
|
||||
|
||||
@@ -14,18 +14,20 @@ use tracing::warn;
|
||||
|
||||
impl Session {
|
||||
pub(crate) async fn disable_mcp_enterprise_auth(&self) {
|
||||
let mut state = self.state.lock().await;
|
||||
let mut config = state
|
||||
.session_configuration
|
||||
.original_config_do_not_use
|
||||
.as_ref()
|
||||
.clone();
|
||||
config.config_layer_stack.mcp_ema_cancellation().cancel();
|
||||
config.disable_mcp_enterprise_auth();
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
self.services.mcp_runtime.invalidate_resource_caches();
|
||||
self.mark_mcp_runtime_dirty();
|
||||
drop(state);
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut config = state
|
||||
.session_configuration
|
||||
.original_config_do_not_use
|
||||
.as_ref()
|
||||
.clone();
|
||||
config.config_layer_stack.mcp_ema_cancellation().cancel();
|
||||
config.disable_mcp_enterprise_auth();
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
self.services.mcp_runtime.invalidate_resource_caches();
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
self.services.mcp_runtime.quiesce_ema().await;
|
||||
self.schedule_mcp_prewarm();
|
||||
}
|
||||
|
||||
@@ -164,6 +166,17 @@ impl Session {
|
||||
if config.mcp_enterprise_managed_auth.is_none() {
|
||||
config.config_layer_stack.mcp_ema_cancellation().cancel();
|
||||
}
|
||||
if !matches!(scope, RuntimeConfigRefresh::UserFiles) {
|
||||
let cancellation = config
|
||||
.config_layer_stack
|
||||
.mcp_ema_cancellation()
|
||||
.refreshed(next_config.config_layer_stack.mcp_ema_cancellation());
|
||||
let config = Arc::make_mut(&mut config);
|
||||
config.config_layer_stack = config
|
||||
.config_layer_stack
|
||||
.clone()
|
||||
.with_mcp_ema_cancellation(cancellation);
|
||||
}
|
||||
state.session_configuration.original_config_do_not_use = Arc::clone(&config);
|
||||
drop(policy_guard);
|
||||
if matches!(scope, RuntimeConfigRefresh::Mcp) {
|
||||
@@ -174,6 +187,13 @@ impl Session {
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
(previous_config, new_config)
|
||||
};
|
||||
if config
|
||||
.config_layer_stack
|
||||
.mcp_ema_cancellation()
|
||||
.is_cancelled()
|
||||
{
|
||||
self.services.mcp_runtime.quiesce_ema().await;
|
||||
}
|
||||
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
|
||||
self.schedule_mcp_prewarm();
|
||||
self.refresh_hooks(config).await;
|
||||
|
||||
@@ -1408,7 +1408,7 @@ impl ThreadManager {
|
||||
/// as `Arc<CodexThread>`, it is possible that other references to it exist elsewhere.
|
||||
/// Returns the thread if the thread was found and removed.
|
||||
pub async fn remove_thread(&self, thread_id: &ThreadId) -> Option<Arc<CodexThread>> {
|
||||
self.state.threads.write().await.remove(thread_id)
|
||||
self.state.remove_thread(thread_id).await
|
||||
}
|
||||
|
||||
/// Removes a runtime for a client request, leaving internal workers with their owner.
|
||||
@@ -1426,7 +1426,9 @@ impl ThreadManager {
|
||||
"live internal threads can only be removed by their owner".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(threads.remove(thread_id))
|
||||
Ok(threads.remove(thread_id).inspect(|thread| {
|
||||
thread.session.services.mcp_runtime.revoke_ema();
|
||||
}))
|
||||
}
|
||||
|
||||
/// Removes a thread only if `thread_id` still maps to `expected`.
|
||||
@@ -1443,7 +1445,9 @@ impl ThreadManager {
|
||||
.get(thread_id)
|
||||
.is_some_and(|thread| Arc::ptr_eq(thread, expected))
|
||||
{
|
||||
threads.remove(thread_id)
|
||||
threads.remove(thread_id).inspect(|thread| {
|
||||
thread.session.services.mcp_runtime.revoke_ema();
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1485,7 +1489,9 @@ impl ThreadManager {
|
||||
|
||||
let mut tracked_threads = self.state.threads.write().await;
|
||||
for thread_id in &report.completed {
|
||||
tracked_threads.remove(thread_id);
|
||||
if let Some(thread) = tracked_threads.remove(thread_id) {
|
||||
thread.session.services.mcp_runtime.revoke_ema();
|
||||
}
|
||||
}
|
||||
|
||||
report
|
||||
@@ -1850,7 +1856,13 @@ impl ThreadManagerState {
|
||||
|
||||
/// Remove a thread from the manager by ID, returning it when present.
|
||||
pub(crate) async fn remove_thread(&self, thread_id: &ThreadId) -> Option<Arc<CodexThread>> {
|
||||
self.threads.write().await.remove(thread_id)
|
||||
self.threads
|
||||
.write()
|
||||
.await
|
||||
.remove(thread_id)
|
||||
.inspect(|thread| {
|
||||
thread.session.services.mcp_runtime.revoke_ema();
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn effective_multi_agent_version_for_spawn(
|
||||
@@ -2094,6 +2106,17 @@ impl ThreadManagerState {
|
||||
|
||||
/// Spawn a new thread with optional history and register it with the manager.
|
||||
async fn spawn_thread(&self, mut request: ThreadSpawnRequest) -> CodexResult<NewThread> {
|
||||
let cancellation = request
|
||||
.options
|
||||
.config
|
||||
.config_layer_stack
|
||||
.mcp_ema_cancellation()
|
||||
.for_startup();
|
||||
request.options.config.config_layer_stack = request
|
||||
.options
|
||||
.config
|
||||
.config_layer_stack
|
||||
.with_mcp_ema_cancellation(cancellation);
|
||||
let runtime = request.agent_control.runtime().clone();
|
||||
let membership = runtime.admit_start()?;
|
||||
let owns_startup = request.startup.is_none();
|
||||
@@ -2241,6 +2264,7 @@ impl ThreadManagerState {
|
||||
});
|
||||
}
|
||||
threads.remove(&resumed.conversation_id);
|
||||
thread.session.services.mcp_runtime.revoke_ema();
|
||||
}
|
||||
}
|
||||
let agent_control = match (&self.agent_control_factory, agent_control) {
|
||||
@@ -2511,6 +2535,12 @@ impl ThreadManagerState {
|
||||
}
|
||||
};
|
||||
|
||||
let ema_cancellation = session
|
||||
.get_config()
|
||||
.await
|
||||
.config_layer_stack
|
||||
.mcp_ema_cancellation()
|
||||
.clone();
|
||||
{
|
||||
let mut threads = self.threads.write().await;
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = threads.entry(thread_id) {
|
||||
@@ -2520,6 +2550,7 @@ impl ThreadManagerState {
|
||||
.agent_control
|
||||
.propagate_config_update(update);
|
||||
}
|
||||
ema_cancellation.publish();
|
||||
let thread = Arc::new(CodexThread::new(
|
||||
session,
|
||||
io,
|
||||
|
||||
Reference in New Issue
Block a user