mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## What changed - Let deferred environments provide selected capability roots with their ready signal. - Validate that those roots have unique, non-empty IDs, belong to the registering environment, and stay within the root limit. - Include roots from ready turn environments when resolving MCP contributions, and refresh the MCP runtime when the selected root set changes. - Expose the exact ready root set to MCP contributors so executor plugins become available with their environment. ## Testing - Cover ready-root propagation, validation failures, replacement isolation, reconnection, and MCP plugin availability refresh. GitOrigin-RevId: ec3498aab1164824025094e96a9b1063b7b731ad
190 lines
7.4 KiB
Rust
190 lines
7.4 KiB
Rust
use codex_connectors_extension::ExecutorPluginConnectorProvider;
|
|
use codex_core::config::Config;
|
|
use codex_core_plugins::ExecutorPluginProvider;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use codex_extension_api::ExtensionFuture;
|
|
use codex_extension_api::McpServerContribution;
|
|
use codex_extension_api::McpServerContributionContext;
|
|
use codex_extension_api::McpServerContributor;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
|
|
use self::provider::ExecutorPluginMcpProvider;
|
|
|
|
mod provider;
|
|
|
|
/// Frozen MCP and connector declarations for one selected package.
|
|
///
|
|
/// Each server config retains the stable logical environment ID. Reconnection may replace the
|
|
/// concrete environment instance without changing that authority.
|
|
#[derive(Clone)]
|
|
struct SelectedPluginMetadata {
|
|
plugin_id: String,
|
|
plugin_display_name: String,
|
|
servers: Vec<(String, codex_config::McpServerConfig)>,
|
|
connector_ids: Vec<String>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct SelectedExecutorPluginMcpState {
|
|
cache: Mutex<Vec<CachedSelectedRoot>>,
|
|
}
|
|
|
|
struct CachedSelectedRoot {
|
|
root: SelectedCapabilityRoot,
|
|
metadata: Option<SelectedPluginMetadata>,
|
|
}
|
|
|
|
pub(crate) struct SelectedExecutorPluginMcpContributor {
|
|
plugin_provider: ExecutorPluginProvider,
|
|
mcp_provider: ExecutorPluginMcpProvider,
|
|
connector_provider: ExecutorPluginConnectorProvider,
|
|
}
|
|
|
|
impl SelectedExecutorPluginMcpContributor {
|
|
pub(crate) fn new(environment_manager: Arc<EnvironmentManager>) -> Self {
|
|
Self {
|
|
plugin_provider: ExecutorPluginProvider::new(Arc::clone(&environment_manager)),
|
|
mcp_provider: ExecutorPluginMcpProvider,
|
|
connector_provider: ExecutorPluginConnectorProvider,
|
|
}
|
|
}
|
|
|
|
/// Returns metadata for one stable selected root.
|
|
///
|
|
/// Successful resolution, including a root that is not a plugin or declares no capabilities,
|
|
/// is cached until the thread state is dropped. Environment availability never invalidates
|
|
/// this cache; it only controls whether the cached metadata is projected into a model step.
|
|
#[tracing::instrument(name = "mcp.executor_plugin.metadata.load", skip_all)]
|
|
async fn metadata_for_root(
|
|
&self,
|
|
state: &SelectedExecutorPluginMcpState,
|
|
selected_root: &SelectedCapabilityRoot,
|
|
) -> Option<SelectedPluginMetadata> {
|
|
if let Some(cached) = state
|
|
.cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.iter()
|
|
.find(|cached| cached.root == *selected_root)
|
|
{
|
|
return cached.metadata.clone();
|
|
}
|
|
|
|
let plugin = match self.plugin_provider.resolve_bound(selected_root).await {
|
|
Ok(plugin) => plugin,
|
|
Err(err) => {
|
|
tracing::warn!(
|
|
selected_root = selected_root.id,
|
|
error = %err,
|
|
"failed to resolve selected executor plugin"
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
let metadata = match plugin {
|
|
Some(plugin) => {
|
|
// MCP server declarations and app connector declarations are separate
|
|
// executor-owned files. Read them together so a remote environment only
|
|
// pays for the slower read instead of both reads back-to-back.
|
|
let (servers, connector_declarations) = tokio::join!(
|
|
self.mcp_provider.load(&plugin),
|
|
self.connector_provider.load(&plugin)
|
|
);
|
|
let servers = servers.unwrap_or_else(|err| {
|
|
tracing::warn!(
|
|
selected_root = selected_root.id,
|
|
error = %err,
|
|
"failed to load selected executor plugin MCP servers"
|
|
);
|
|
Vec::new()
|
|
});
|
|
let connector_ids = connector_declarations
|
|
.unwrap_or_else(|err| {
|
|
tracing::warn!(
|
|
selected_root = selected_root.id,
|
|
error = %err,
|
|
"failed to load selected executor plugin connectors"
|
|
);
|
|
Vec::new()
|
|
})
|
|
.into_iter()
|
|
.map(|declaration| declaration.connector_id.0)
|
|
.collect();
|
|
Some(SelectedPluginMetadata {
|
|
plugin_id: plugin.plugin().selected_root_id().to_string(),
|
|
plugin_display_name: plugin.plugin().manifest().display_name().to_string(),
|
|
servers,
|
|
connector_ids,
|
|
})
|
|
}
|
|
None => None,
|
|
};
|
|
let mut cache = state
|
|
.cache
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if let Some(cached) = cache.iter().find(|cached| cached.root == *selected_root) {
|
|
return cached.metadata.clone();
|
|
}
|
|
cache.push(CachedSelectedRoot {
|
|
root: selected_root.clone(),
|
|
metadata: metadata.clone(),
|
|
});
|
|
metadata
|
|
}
|
|
}
|
|
|
|
impl McpServerContributor<Config> for SelectedExecutorPluginMcpContributor {
|
|
fn id(&self) -> &'static str {
|
|
"selected_executor_plugin_mcp"
|
|
}
|
|
|
|
fn contribute<'a>(
|
|
&'a self,
|
|
context: McpServerContributionContext<'a, Config>,
|
|
) -> ExtensionFuture<'a, Vec<McpServerContribution>> {
|
|
Box::pin(async move {
|
|
let Some(thread_store) = context.thread_store() else {
|
|
return Vec::new();
|
|
};
|
|
let Some(selected_roots) = context.ready_selected_capability_roots() else {
|
|
return Vec::new();
|
|
};
|
|
let state = thread_store.get_or_init(SelectedExecutorPluginMcpState::default);
|
|
let mut contributions = Vec::new();
|
|
|
|
for (selection_order, selected_root) in selected_roots.iter().enumerate() {
|
|
let Some(plugin) = self.metadata_for_root(&state, selected_root).await else {
|
|
continue;
|
|
};
|
|
let mut servers = plugin.servers.iter().cloned().collect::<HashMap<_, _>>();
|
|
context
|
|
.config()
|
|
.apply_plugin_mcp_server_requirements(&plugin.plugin_id, &mut servers);
|
|
let mut servers = servers.into_iter().collect::<Vec<_>>();
|
|
servers.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
|
contributions.extend(servers.into_iter().map(|(name, config)| {
|
|
McpServerContribution::SelectedPlugin {
|
|
name,
|
|
plugin_id: plugin.plugin_id.clone(),
|
|
plugin_display_name: plugin.plugin_display_name.clone(),
|
|
selection_order,
|
|
config: Box::new(config),
|
|
}
|
|
}));
|
|
// Keep the package visible even when it contributes only skills.
|
|
contributions.push(McpServerContribution::SelectedPluginPackage {
|
|
plugin_id: plugin.plugin_id,
|
|
plugin_display_name: plugin.plugin_display_name,
|
|
connector_ids: plugin.connector_ids,
|
|
});
|
|
}
|
|
|
|
contributions
|
|
})
|
|
}
|
|
}
|