Add batched executor capability discovery (#33852)

## Why

Selected capability roots can contribute plugins, MCP servers, connectors, and
skills. Discovering each contribution separately requires repeated access to the
executor filesystem.

## What changed

- Add the `capabilityRoots/discoverV1` exec-server RPC to scan selected roots and
  materialize recognized plugin manifests, configuration files, skill
  instructions, and skill metadata in one bounded request.
- Add the opt-in `executor_capability_discovery` feature, with a thread-scoped
  cache and per-step snapshot shared by MCP and skill discovery.
- Parse MCP, connector, and skill contributions from the materialized snapshot,
  including serving cached skill instructions without another filesystem read.

## Testing

- Cover discovery limits, manifest precedence, root-local failures, cache reuse,
  plugin contributions, and parity with the existing environment skill loader.

GitOrigin-RevId: f98fd2321cafb58c596db02da1f83c09d8eb375d
This commit is contained in:
Bryan Ashley
2026-07-17 15:45:48 +00:00
committed by copyberry
parent caa6e554d6
commit 08e30a2e4e
47 changed files with 1957 additions and 47 deletions
+6
View File
@@ -518,6 +518,9 @@
"exec_permission_approvals": {
"type": "boolean"
},
"executor_capability_discovery": {
"type": "boolean"
},
"experimental_use_unified_exec_tool": {
"type": "boolean"
},
@@ -4912,6 +4915,9 @@
"exec_permission_approvals": {
"type": "boolean"
},
"executor_capability_discovery": {
"type": "boolean"
},
"experimental_use_unified_exec_tool": {
"type": "boolean"
},
+3
View File
@@ -7,6 +7,7 @@ use codex_connectors::ConnectorRuntimeManager;
use codex_connectors::ConnectorSnapshot;
use codex_connectors::PluginConnectorSource;
use codex_core_plugins::PluginsManager;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::ExtensionRegistry;
@@ -109,6 +110,7 @@ impl McpManager {
thread_store: &ExtensionData,
originator: &str,
ready_selected_capability_roots: &[SelectedCapabilityRoot],
executor_capability_discovery: Option<&ExecutorCapabilityDiscoverySnapshot>,
) -> McpRuntimeProjection {
self.runtime_config_with_context(
McpServerContributionContext::for_step(
@@ -117,6 +119,7 @@ impl McpManager {
thread_store,
originator,
ready_selected_capability_roots,
executor_capability_discovery,
),
Some(originator),
)
+49 -1
View File
@@ -1,5 +1,7 @@
use super::*;
use crate::mcp::McpRuntimeProjection;
use codex_exec_server::ExecutorCapabilityDiscoveryCache;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::MAX_SELECTED_CAPABILITY_ROOTS;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::ElicitationReviewRequest;
@@ -87,6 +89,9 @@ impl Session {
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery = self
.executor_capability_discovery_for_step(config, &ready_selected_capability_roots)
.await;
self.services
.mcp_manager
.runtime_config_for_step(
@@ -95,6 +100,7 @@ impl Session {
&self.services.thread_extension_data,
&originator,
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
)
.await
.config
@@ -117,6 +123,7 @@ impl Session {
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
selected_capability_roots: &[ResolvedSelectedCapabilityRoot],
executor_capability_discovery: Option<&ExecutorCapabilityDiscoverySnapshot>,
) -> Arc<McpRuntimeSnapshot> {
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(selected_capability_roots);
@@ -141,6 +148,7 @@ impl Session {
&self.services.thread_extension_data,
&turn_context.originator,
&ready_selected_capability_roots,
executor_capability_discovery,
)
.await;
let mcp_config = &mcp_projection.config;
@@ -190,6 +198,32 @@ impl Session {
.await
}
#[tracing::instrument(
name = "capability_roots.snapshot_for_step",
skip_all,
fields(root_count = ready_selected_capability_roots.len())
)]
pub(crate) async fn executor_capability_discovery_for_step(
&self,
config: &Config,
ready_selected_capability_roots: &[SelectedCapabilityRoot],
) -> Option<Arc<ExecutorCapabilityDiscoverySnapshot>> {
if !config
.features
.enabled(Feature::ExecutorCapabilityDiscovery)
{
return None;
}
let environment_manager = self.services.turn_environments.environment_manager();
let cache = self
.services
.thread_extension_data
.get_or_init(|| ExecutorCapabilityDiscoveryCache::new(environment_manager));
Some(Arc::new(
cache.snapshot(ready_selected_capability_roots).await,
))
}
pub(crate) async fn resolve_selected_capability_roots_for_step(
&self,
environments: &TurnEnvironmentSnapshot,
@@ -511,6 +545,12 @@ impl Session {
let current_runtime = self.services.latest_mcp_runtime();
let ready_selected_capability_roots =
current_runtime.ready_selected_capability_roots().to_vec();
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&refresh_config,
&ready_selected_capability_roots,
)
.await;
let mut mcp_projection = self
.services
.mcp_manager
@@ -520,6 +560,7 @@ impl Session {
&self.services.thread_extension_data,
&turn_context.originator,
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
)
.await;
mcp_projection.config.mcp_server_catalog = mcp_projection
@@ -578,6 +619,12 @@ impl Session {
let current_runtime = self.services.latest_mcp_runtime();
let ready_selected_capability_roots =
current_runtime.ready_selected_capability_roots().to_vec();
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
refresh_config,
&ready_selected_capability_roots,
)
.await;
let mcp_projection = self
.services
.mcp_manager
@@ -587,6 +634,7 @@ impl Session {
&self.services.thread_extension_data,
&turn_context.originator,
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
)
.await;
self.refresh_mcp_servers_inner(
@@ -613,7 +661,7 @@ impl Session {
available
}
fn ready_selected_capability_roots(
pub(crate) fn ready_selected_capability_roots(
selected_capability_roots: &[ResolvedSelectedCapabilityRoot],
) -> Vec<SelectedCapabilityRoot> {
selected_capability_roots
+10
View File
@@ -2859,17 +2859,27 @@ impl Session {
let selected_capability_roots = self
.resolve_selected_capability_roots_for_step(&environments)
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&turn_context.config,
&ready_selected_capability_roots,
)
.await;
let mcp = self
.mcp_runtime_for_step(
turn_context.as_ref(),
&environments,
&selected_capability_roots,
executor_capability_discovery.as_deref(),
)
.await;
Arc::new(StepContext::new(
turn_context,
environments,
selected_capability_roots,
executor_capability_discovery,
mcp,
loaded_agents_md,
))
+1
View File
@@ -717,6 +717,7 @@ impl Session {
thread_extension_data_for_mcp,
&mcp_originator,
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
)
.await;
let mcp_config = &mcp_projection.config;
@@ -4,6 +4,7 @@ use crate::agents_md::LoadedAgentsMd;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::McpRuntimeSnapshot;
use crate::session::turn_context::TurnContext;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::ToolInfo;
use tokio::sync::OnceCell;
@@ -15,6 +16,8 @@ pub(crate) struct StepContext {
pub(crate) environments: TurnEnvironmentSnapshot,
/// Capability roots bound to ready environments in this exact step.
pub(crate) selected_capability_roots: Vec<ResolvedSelectedCapabilityRoot>,
/// Executor-materialized capability files shared by MCP and skills in this exact step.
pub(crate) executor_capability_discovery: Option<Arc<ExecutorCapabilityDiscoverySnapshot>>,
/// The exact MCP config and manager used to advertise and execute tools for this step.
pub(crate) mcp: Arc<McpRuntimeSnapshot>,
/// The fixed MCP tool list used for this exact sampling request.
@@ -28,6 +31,7 @@ impl StepContext {
turn: Arc<TurnContext>,
environments: TurnEnvironmentSnapshot,
selected_capability_roots: Vec<ResolvedSelectedCapabilityRoot>,
executor_capability_discovery: Option<Arc<ExecutorCapabilityDiscoverySnapshot>>,
mcp: Arc<McpRuntimeSnapshot>,
loaded_agents_md: Option<Arc<LoadedAgentsMd>>,
) -> Self {
@@ -35,6 +39,7 @@ impl StepContext {
turn,
environments,
selected_capability_roots,
executor_capability_discovery,
mcp,
mcp_tool_snapshot: OnceCell::new(),
loaded_agents_md,
+7 -1
View File
@@ -200,6 +200,7 @@ impl StepContext {
Arc::clone(&turn),
environments,
Vec::new(),
/*executor_capability_discovery*/ None,
crate::session::McpRuntimeSnapshot::new_uninitialized_for_test(&turn.config),
/*loaded_agents_md*/ None,
))
@@ -7813,7 +7814,12 @@ async fn deferred_environment_roots_refresh_plugin_availability() {
);
let new_runtime = session
.mcp_runtime_for_step(&turn_context, &environments, &resolved_roots)
.mcp_runtime_for_step(
&turn_context,
&environments,
&resolved_roots,
/*executor_capability_discovery*/ None,
)
.await;
assert!(!old_runtime.plugins_available());
+3
View File
@@ -77,6 +77,9 @@ impl Session {
turn_id: turn_context.sub_id.as_str(),
environments: &environments,
ready_selected_capability_roots: &ready_selected_capability_roots,
executor_capability_discovery: step_context
.executor_capability_discovery
.as_deref(),
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
@@ -687,6 +687,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
&first_session.services.thread_extension_data,
&first_originator,
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
)
.await;
let second_session = &second_thread.thread.session;
@@ -700,6 +701,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
&second_session.services.thread_extension_data,
&second_originator,
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
)
.await;
@@ -685,6 +685,7 @@ async fn environment_tools_follow_the_step_context() {
Arc::clone(&turn),
environments,
Vec::new(),
/*executor_capability_discovery*/ None,
crate::session::McpRuntimeSnapshot::new_uninitialized_for_test(&turn.config),
/*loaded_agents_md*/ None,
));