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
@@ -1,5 +1,6 @@
use codex_config::McpServerConfig;
use codex_exec_server_protocol::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server_protocol::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -32,6 +33,8 @@ pub struct McpServerContributionContext<'a, C> {
ready_selected_capability_roots: Option<&'a [SelectedCapabilityRoot]>,
/// Executor-materialized capability files shared by all consumers in this exact step.
executor_capability_discovery: Option<&'a ExecutorCapabilityDiscoverySnapshot>,
/// V2 inventories supplied by the host for this contribution.
executor_capability_discovery_v2: Option<&'a [ExecutorCapabilityDiscoveryV2Snapshot]>,
}
impl<C> Clone for McpServerContributionContext<'_, C> {
@@ -55,6 +58,7 @@ impl<'a, C> McpServerContributionContext<'a, C> {
selected_environments: None,
ready_selected_capability_roots: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: None,
}
}
@@ -66,6 +70,7 @@ impl<'a, C> McpServerContributionContext<'a, C> {
originator: &'a str,
ready_selected_capability_roots: &'a [SelectedCapabilityRoot],
executor_capability_discovery: Option<&'a ExecutorCapabilityDiscoverySnapshot>,
executor_capability_discovery_v2: Option<&'a [ExecutorCapabilityDiscoveryV2Snapshot]>,
) -> Self {
Self {
config,
@@ -77,6 +82,7 @@ impl<'a, C> McpServerContributionContext<'a, C> {
selected_environments: None,
ready_selected_capability_roots: Some(ready_selected_capability_roots),
executor_capability_discovery,
executor_capability_discovery_v2,
}
}
@@ -145,6 +151,13 @@ impl<'a, C> McpServerContributionContext<'a, C> {
self.ready_selected_capability_roots
}
/// Returns the V2 inventories supplied by the host, when available.
pub fn executor_capability_discovery_v2(
&self,
) -> Option<&'a [ExecutorCapabilityDiscoveryV2Snapshot]> {
self.executor_capability_discovery_v2
}
/// Returns the executor-materialized capability files for this model step, when enabled.
pub fn executor_capability_discovery(&self) -> Option<&'a ExecutorCapabilityDiscoverySnapshot> {
self.executor_capability_discovery
@@ -1,6 +1,7 @@
use std::sync::Arc;
use codex_exec_server_protocol::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server_protocol::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_protocol::ThreadId;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::openai_models::ModelInfo;
@@ -21,6 +22,8 @@ pub struct WorldStateContributionInput<'a> {
pub ready_selected_capability_roots: &'a [SelectedCapabilityRoot],
/// Executor-materialized capability files shared by all consumers in this exact step.
pub executor_capability_discovery: Option<&'a ExecutorCapabilityDiscoverySnapshot>,
/// Opted-in executor inventories captured for this exact step.
pub executor_capability_discovery_v2: Option<&'a [ExecutorCapabilityDiscoveryV2Snapshot]>,
/// Metrics bound to the captured model for this sampling step.
pub extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
pub session_store: &'a ExtensionData,
@@ -59,6 +59,7 @@ fn mcp_contribution_context_identifies_the_running_thread() {
"codex_work_cca",
&[],
/*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
)
.with_session_source(&session_source);
+1
View File
@@ -102,6 +102,7 @@ impl TurnLifecycleContributor for PluginContributor {
thread_id: input.thread_store.level_id().to_string(),
turn_id: input.turn_id.to_string(),
mcp_resources: resources.map(Arc::new),
snapshots: Vec::new(),
})
.await
{
@@ -169,6 +169,7 @@ async fn disabling_plugins_clears_cloud_catalog_and_skips_discovery() -> anyhow:
"test",
&[],
/*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
))
.await;
if let Some(state) = thread.get::<PluginsThreadState>() {
@@ -235,6 +236,7 @@ async fn account_change_clears_hosted_plugins_before_waiting_for_executor() -> a
let init = codex_extension_api::ExtensionDataInit::default();
let context = McpServerContributionContext::for_step(
&config, &init, &thread, "test", &roots, /*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
)
.with_auth_changed(/*auth_changed*/ true);
let mut loading = std::pin::pin!(contributor.selected_plugins(context, &config.plugins));
+1
View File
@@ -29,6 +29,7 @@ async fn hosted_plugin_runtime_forwards_thread_originator() -> Result<(), Box<dy
"codex_work_desktop",
/*ready_selected_capability_roots*/ &[],
/*executor_capability_discovery*/ None,
/*executor_capability_discovery_v2*/ None,
))
.await;
let [
+6
View File
@@ -380,6 +380,7 @@ async fn selected_root_ownership_retries_errors_and_caches_success() -> TestResu
"test_originator",
&selected_roots,
discovery.as_ref(),
/*executor_capability_discovery_v2*/ None,
),
&config.plugins,
)
@@ -661,6 +662,10 @@ async fn raw_selected_plugin_contributions(
None
};
let v2 = [codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
result: Err("V2 must not change existing contributions".to_string()),
}];
let selected = registry.mcp_server_contributors()[0]
.selected_plugins(
McpServerContributionContext::for_step(
@@ -670,6 +675,7 @@ async fn raw_selected_plugin_contributions(
"test_originator",
&selected_capability_roots,
executor_capability_discovery.as_ref(),
Some(&v2),
),
&config.plugins,
)
@@ -89,6 +89,7 @@ fn query(client: Option<Arc<McpResourceClient>>) -> SkillListQuery {
include_cloud_skills: true,
mcp_resources: client,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
}
}
+1
View File
@@ -53,6 +53,7 @@ impl<C: Send + Sync + 'static> TurnLifecycleContributor for SkillsExtension<C> {
.get::<SkillsSessionState>()
.and_then(|state| state.mcp_resources.clone()),
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
};
if let Err(error) = state.refresh_cloud_catalog(&self.providers, query).await {
self.emit_warning(
+7
View File
@@ -4,6 +4,7 @@ use crate::HostSkillsSnapshot;
use crate::InjectedHostSkillPrompts;
use codex_analytics::InvocationType;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_extension_api::ConfigContributor;
@@ -210,6 +211,7 @@ where
.get::<SkillsSessionState>()
.and_then(|state| state.mcp_resources.clone()),
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
},
&thread_state,
)
@@ -308,6 +310,10 @@ where
executor_capability_discovery: step_store
.get::<ExecutorCapabilityDiscoverySnapshot>()
.map(|discovery| discovery.as_ref().clone()),
executor_capability_discovery_v2: step_store
.get::<Vec<ExecutorCapabilityDiscoveryV2Snapshot>>()
.map(|discovery| discovery.as_ref().clone())
.unwrap_or_default(),
});
self.build_skill_tools(
session_store,
@@ -382,6 +388,7 @@ where
include_cloud_skills: thread_state.cloud_skill_enabled(),
mcp_resources: mcp_resources.clone(),
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
};
let mut catalog = turn_store
.get::<ExecutorSkillsStepState>()
+3
View File
@@ -8,6 +8,7 @@ mod host;
use crate::HostSkillsSnapshot;
use codex_exec_server::EnvironmentAccess;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_mcp::McpResourceClient;
use codex_protocol::capabilities::SelectedCapabilityRoot;
@@ -38,6 +39,8 @@ pub struct SkillListQuery {
pub mcp_resources: Option<Arc<McpResourceClient>>,
/// Present only when the opt-in high-level executor discovery path is selected.
pub executor_capability_discovery: Option<ExecutorCapabilityDiscoverySnapshot>,
/// Inert V2 inventories; empty when none were supplied. Existing discovery is unchanged.
pub executor_capability_discovery_v2: Vec<ExecutorCapabilityDiscoveryV2Snapshot>,
}
/// A skill read with the source access supplied by its callback.
+2
View File
@@ -547,6 +547,7 @@ async fn host_alias_roots_follow_core_discovery_order() -> Result<(), Box<dyn st
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
})
.await?;
let mut entries = catalog.entries.iter().collect::<Vec<_>>();
@@ -915,6 +916,7 @@ async fn singleton_plugin_versions_share_the_marketplace_alias_root()
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
})
.await?;
let entries = catalog.entries.iter().collect::<Vec<_>>();
@@ -168,6 +168,11 @@ impl<'a> CatalogContext<'a> {
.get::<SkillsSessionState>()
.and_then(|state| state.mcp_resources.clone()),
executor_capability_discovery: self.input.executor_capability_discovery.cloned(),
executor_capability_discovery_v2: self
.input
.executor_capability_discovery_v2
.map(Vec::from)
.unwrap_or_default(),
};
let cloud = self.cloud_catalog_contribution(&query);
@@ -244,6 +249,7 @@ impl<'a> CatalogContext<'a> {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
})
.await;
self.input
@@ -417,6 +417,7 @@ async fn selected_root_id_distinguishes_identical_executor_paths() {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
})
.await
.expect("list executor skills");
@@ -507,6 +508,7 @@ async fn executor_discovery_preserves_posix_and_windows_locator_alias_roots() {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: Some(discovery),
executor_capability_discovery_v2: Vec::new(),
})
.await
.expect("list executor skills");
@@ -624,6 +626,12 @@ async fn executor_discovery_routes_produce_equivalent_catalog_metadata() {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery,
executor_capability_discovery_v2: vec![
codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot {
environment_id: "local".to_string(),
result: Err("V2 must not change V1 or direct discovery".to_string()),
},
],
};
let direct = provider
@@ -776,6 +784,7 @@ async fn pre_discovered_executor_catalog_snapshot() {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: Some(executor_capability_discovery),
executor_capability_discovery_v2: Vec::new(),
})
.await
.expect("list pre-discovered executor skills");
@@ -893,6 +902,7 @@ async fn direct_executor_discovery_preserves_hidden_nested_and_probed_metadata()
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery_v2: Vec::new(),
})
.await
.expect("list directly discovered executor skills");
@@ -953,6 +963,7 @@ async fn high_level_discovery_reuses_materialized_skill_contents_for_reads() {
include_cloud_skills: false,
mcp_resources: None,
executor_capability_discovery: Some(executor_capability_discovery),
executor_capability_discovery_v2: Vec::new(),
})
.await
.expect("list executor skills");
@@ -134,6 +134,7 @@ async fn skill_world_state_fragments(
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id,
@@ -322,6 +323,7 @@ async fn host_world_state_records_catalog_metrics_on_publish_and_change() -> Tes
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -349,6 +351,7 @@ async fn host_world_state_records_catalog_metrics_on_publish_and_change() -> Tes
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -389,6 +392,7 @@ async fn host_world_state_records_catalog_metrics_on_publish_and_change() -> Tes
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -463,6 +467,7 @@ async fn persisted_host_snapshot_deduplicates_warning_after_reinitialization() -
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &model_info,
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -504,6 +509,7 @@ async fn persisted_host_snapshot_deduplicates_warning_after_reinitialization() -
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &model_info,
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-2",
@@ -597,6 +603,7 @@ async fn executor_cloud_and_host_share_catalog_world_state_flow() -> TestResult
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -708,6 +715,7 @@ async fn nonempty_executor_empty_host_records_catalog_metrics() -> TestResult {
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -790,6 +798,7 @@ async fn host_world_state_uses_provider_catalog_with_core_compatible_rendering()
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -873,6 +882,7 @@ async fn shadow_selection_uses_host_catalog_when_instructions_are_disabled() ->
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -1167,6 +1177,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
let available_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -1232,6 +1243,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
let unavailable_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: Some(&available_world_state),
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-2",
@@ -1271,6 +1283,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
let restored_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: Some(&unavailable_world_state),
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-3",
@@ -1330,6 +1343,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id,
@@ -1358,6 +1372,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
let listing_disabled_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-4",
@@ -1487,6 +1502,7 @@ async fn overlapping_executor_catalogs_render_their_own_roots() -> TestResult {
environments: &[],
ready_selected_capability_roots: &[root],
executor_capability_discovery: None,
executor_capability_discovery_v2: None,
extension_metrics: None,
session_store: &session_store,
thread_store: &thread_store,
@@ -1762,6 +1778,7 @@ async fn catalog_rebalances_only_to_avoid_omissions_and_retains_the_allocation()
environments: &[],
ready_selected_capability_roots: if ready { &selected_roots } else { &[] },
executor_capability_discovery: None,
executor_capability_discovery_v2: None,
extension_metrics: None,
session_store: &session_store,
thread_store: &thread_store,
@@ -2382,6 +2399,7 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te
registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -2519,6 +2537,7 @@ async fn model_context_window_scales_executor_and_cloud_catalogs() -> TestResult
let sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &model_info,
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -2536,6 +2555,7 @@ async fn model_context_window_scales_executor_and_cloud_catalogs() -> TestResult
let _repeated_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &model_info,
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -2636,6 +2656,7 @@ async fn executor_catalog_emits_at_most_four_warnings() -> TestResult {
registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
previous_world_state: None,
executor_capability_discovery_v2: None,
model_info: &catalog_model_info(),
thread_id: codex_protocol::ThreadId::new(),
turn_id: "turn-1",
@@ -119,6 +119,7 @@ async fn cloud_preference_preserves_aliases_reads_and_executor_fallback() -> Tes
environments: &[],
ready_selected_capability_roots: if ready { &roots } else { &[] },
executor_capability_discovery: None,
executor_capability_discovery_v2: None,
extension_metrics: None,
session_store: &session_store,
thread_store: &thread_store,