Acquire and cache opt-in executor capability inventories via V2 (#53736)

## What changed

- Add the disabled-by-default `executor_capability_discovery_v2` feature to acquire `capabilities/discoverV2` inventories from supporting remote executors using each selected environment's working directory and sandbox.
- Share snapshots with MCP and world-state contributors and skill providers without activating discovered plugins or publishing discovered skills to the model.
- Cache inventories by environment instance, executor session, working directory, and sandbox. Coalesce concurrent requests, refresh the cache at turn start, retry transient failures, and refresh MCP projections after recovery.
- Resolve the system skills root in the executor and pass it explicitly to capability location discovery.

## Testing

Add coverage for feature gating, unsupported executors, inert inventories, cache reuse and eviction, turn refreshes, failure recovery, executor replacement, and system skill locations.

GitOrigin-RevId: 1bdca28fa7f17622b27e2713ab3dcc0c13426889
This commit is contained in:
Anthony Tafoya
2026-10-13 18:15:50 +00:00
committed by copyberry
parent 4d82d36ccf
commit cf0ed4c600
39 changed files with 1027 additions and 12 deletions
+79
View File
@@ -3,8 +3,11 @@ use super::*;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::environment_selection::combine_selected_capability_roots;
use crate::mcp_tool_call::McpToolApprovalContext;
use codex_exec_server::DiscoverV2CapabilitiesRequest;
use codex_exec_server::ExecutorCapabilityDiscoveryCache;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ExecutorCapabilityDiscoveryV2Cache;
use codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_exec_server::MAX_SELECTED_CAPABILITY_ROOTS;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
@@ -122,6 +125,9 @@ impl Session {
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery_v2 = self
.executor_capability_discovery_for_step_v2(config, &environments)
.await;
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
config,
@@ -147,6 +153,7 @@ impl Session {
},
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
executor_capability_discovery_v2.as_deref(),
)
.await;
let mcp_config = self
@@ -229,6 +236,9 @@ impl Session {
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery_v2 = self
.executor_capability_discovery_for_step_v2(&desired.config, &desired.environments)
.await;
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&desired.config,
@@ -255,6 +265,7 @@ impl Session {
},
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
executor_capability_discovery_v2.as_deref(),
)
.await;
self.publish_mcp_runtime(
@@ -302,6 +313,9 @@ impl Session {
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery_v2 = self
.executor_capability_discovery_for_step_v2(&desired.config, &desired.environments)
.await;
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&desired.config,
@@ -328,6 +342,7 @@ impl Session {
},
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
executor_capability_discovery_v2.as_deref(),
)
.await;
let mcp_projection = self
@@ -499,6 +514,66 @@ impl Session {
Some(Arc::new(discovery))
}
/// Acquires V2 inventories from the thread's current cache.
pub(super) async fn executor_capability_discovery_for_step_v2(
&self,
config: &Config,
environments: &TurnEnvironmentSnapshot,
) -> Option<Vec<ExecutorCapabilityDiscoveryV2Snapshot>> {
if !config
.features
.enabled(Feature::ExecutorCapabilityDiscoveryV2)
{
return None;
}
// Retain this cache if a new turn replaces the thread's current cache.
let cache = self
.services
.thread_extension_data
.get_or_init(ExecutorCapabilityDiscoveryV2Cache::default);
let remote_environment_count = environments
.turn_environments()
.filter(|selected| selected.environment.is_remote())
.count();
cache.ensure_capacity(remote_environment_count);
let mut snapshots = Vec::new();
for selected in environments.turn_environments() {
if !selected.environment.is_remote() {
continue;
}
let environment_id = &selected.selection.environment_id;
let info = match selected.environment.info().await {
Ok(info) => info,
Err(_) => {
tracing::warn!(
environment_id,
"could not read executor discovery V2 support"
);
continue;
}
};
if !info.capabilities.capability_discovery_v2 {
continue;
}
let request = DiscoverV2CapabilitiesRequest {
cwd: selected.cwd().clone(),
sandbox: Some(selected.sandbox_context(/*additional_permissions*/ None)),
};
let result = cache
.discover(environment_id, &selected.environment, request)
.await;
snapshots.push(ExecutorCapabilityDiscoveryV2Snapshot {
environment_id: environment_id.clone(),
result,
});
}
if cache.take_recovered_discovery() {
// Refresh MCP after discovery recovers.
self.mark_mcp_runtime_dirty();
}
Some(snapshots)
}
pub(crate) async fn resolve_selected_capability_roots_for_step(
&self,
environments: &TurnEnvironmentSnapshot,
@@ -715,6 +790,9 @@ impl Session {
let environments = self.services.turn_environments.snapshot().await;
let environment_selections = environments.all_selections();
let desired = self.latest_mcp_desired_state(auth, environments).await;
let executor_capability_discovery_v2 = self
.executor_capability_discovery_for_step_v2(&desired.config, &desired.environments)
.await;
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&desired.config,
@@ -741,6 +819,7 @@ impl Session {
},
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
executor_capability_discovery_v2.as_deref(),
)
.await;
self.publish_mcp_runtime(
+10
View File
@@ -3923,6 +3923,9 @@ impl Session {
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery_v2 = self
.executor_capability_discovery_for_step_v2(&turn_context.config, &environments)
.await;
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&turn_context.config,
@@ -3932,6 +3935,9 @@ impl Session {
.await;
let extension_data =
codex_extension_api::ExtensionData::new(turn_context.sub_id.clone());
if let Some(snapshots) = &executor_capability_discovery_v2 {
extension_data.insert(snapshots.clone());
}
if let Some(messages) = settings
.model_info
.model_messages
@@ -3969,6 +3975,7 @@ impl Session {
&turn_context.originator,
&ready_selected_capability_roots,
executor_capability_discovery.as_deref(),
executor_capability_discovery_v2.as_deref(),
)
.with_session_source(&turn_context.session_source)
.with_selected_environments(&environments.all_selections()),
@@ -3990,6 +3997,7 @@ impl Session {
Ok::<_, CodexErr>((
selected_capability_roots,
executor_capability_discovery,
executor_capability_discovery_v2,
mcp,
tool_router,
extension_data,
@@ -4005,6 +4013,7 @@ impl Session {
let (
selected_capability_roots,
executor_capability_discovery,
executor_capability_discovery_v2,
mcp,
tool_router,
extension_data,
@@ -4024,6 +4033,7 @@ impl Session {
selected_capability_roots,
executor_capability_discovery,
extension_data,
executor_capability_discovery_v2,
mcp,
tool_router,
loaded_agents_md,
+2
View File
@@ -1208,6 +1208,7 @@ impl Session {
},
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
)
.await;
(auth, mcp_projection)
@@ -1931,6 +1932,7 @@ impl Session {
},
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
)
.await
} else {
@@ -11,6 +11,7 @@ use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use crate::tools::router::ToolRouter;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_extension_api::ExtensionData;
use codex_file_system::EnvironmentAccess;
@@ -40,6 +41,7 @@ pub(crate) struct StepContext {
pub(crate) executor_capability_discovery: Option<Arc<ExecutorCapabilityDiscoverySnapshot>>,
/// Keeps the extension inputs used to build this step's tools for its World State as well.
pub(crate) extension_data: ExtensionData,
pub(crate) executor_capability_discovery_v2: Option<Vec<ExecutorCapabilityDiscoveryV2Snapshot>>,
/// The exact MCP connections, configuration, and catalog captured for this step.
pub(crate) mcp: Arc<McpBinding>,
/// The finalized tool plan advertised and executed for this exact sampling request.
+1
View File
@@ -292,6 +292,7 @@ impl StepContext {
selected_capability_roots: Vec::new(),
executor_capability_discovery: None,
extension_data: codex_extension_api::ExtensionData::new(turn.sub_id.clone()),
executor_capability_discovery_v2: None,
mcp: Arc::new(codex_mcp::McpBinding::empty(mcp_config_for_test(
&turn.config,
))),
+3
View File
@@ -335,6 +335,9 @@ impl Session {
for contributor in self.services.extensions.context_contributors() {
for section in contributor
.contribute_world_state(WorldStateContributionInput {
executor_capability_discovery_v2: step_context
.executor_capability_discovery_v2
.as_deref(),
thread_id: self.thread_id(),
turn_id: turn_context.sub_id.as_str(),
model_info: &step_context.settings.model_info,