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
@@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::sync::Arc;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_file_system::FileSystemSandboxContext;
pub use codex_file_system::WalkOptions;
pub use codex_file_system::WalkOutcome;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_shell_command::shell_detect::DetectedShell;
use codex_utils_path_uri::PathUri;
@@ -37,6 +39,14 @@ pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
pub const FS_WALK_METHOD: &str = "fs/walk";
pub const FS_REMOVE_METHOD: &str = "fs/remove";
pub const FS_COPY_METHOD: &str = "fs/copy";
/// Discovers capability manifests below selected roots using executor-local filesystem access.
pub const CAPABILITY_ROOTS_DISCOVER_METHOD: &str = "capabilityRoots/discoverV1";
/// Ordered plugin manifest paths recognized beneath a plugin root.
pub const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] = &[
".codex-plugin/plugin.json",
".claude-plugin/plugin.json",
".cursor-plugin/plugin.json",
];
/// JSON-RPC request method for executor-side HTTP requests.
pub const HTTP_REQUEST_METHOD: &str = "http/request";
/// JSON-RPC notification method for streamed executor HTTP response bodies.
@@ -431,6 +441,119 @@ pub struct FsCopyParams {
#[serde(rename_all = "camelCase")]
pub struct FsCopyResponse {}
/// Roots to inspect for plugin and skill capability manifests.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRootsDiscoverParams {
pub roots: Vec<CapabilityRootDiscoverRequest>,
}
/// One caller-selected capability root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRootDiscoverRequest {
/// Opaque caller identity returned unchanged in the response.
pub id: String,
/// Absolute root URI interpreted using the exec-server host's path rules.
pub path: PathUri,
}
/// Executor-local discovery results in request order.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRootsDiscoverResponse {
pub roots: Vec<CapabilityRootDiscovery>,
}
/// Recognized UTF-8 capability file materialized by the exec-server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityTextFile {
pub path: PathUri,
pub contents: String,
}
/// Plugin files declared directly by a selected root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveredPluginFiles {
pub manifest: CapabilityTextFile,
/// File-backed MCP declarations, including the conventional `.mcp.json` fallback.
#[serde(default)]
pub mcp_config: Option<CapabilityTextFile>,
/// File-backed connector declarations.
#[serde(default)]
pub apps_config: Option<CapabilityTextFile>,
}
/// A skill instructions file and its optional sibling metadata.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveredSkillFiles {
pub instructions: CapabilityTextFile,
#[serde(default)]
pub metadata: Option<CapabilityTextFile>,
}
/// Manifest bundle for one selected root.
///
/// Discovery failures are root-local so one broken package does not discard valid siblings.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRootDiscovery {
pub id: String,
pub path: PathUri,
#[serde(default)]
pub plugin: Option<DiscoveredPluginFiles>,
#[serde(default)]
pub skills: Vec<DiscoveredSkillFiles>,
/// Plugin manifests found while scanning the root, used to namespace nested skills.
#[serde(default)]
pub namespace_manifests: Vec<CapabilityTextFile>,
#[serde(default)]
pub warnings: Vec<String>,
#[serde(default)]
pub error: Option<String>,
}
/// Immutable results for the selected capability roots visible in one model step.
#[derive(Clone, Debug)]
pub struct ExecutorCapabilityDiscoverySnapshot {
roots: Arc<[ExecutorCapabilityDiscoverySnapshotEntry]>,
}
#[derive(Clone, Debug)]
pub struct ExecutorCapabilityDiscoverySnapshotEntry {
pub selected_root: SelectedCapabilityRoot,
pub result: Result<Arc<CapabilityRootDiscovery>, String>,
}
impl ExecutorCapabilityDiscoverySnapshot {
pub fn new(
selected_roots: &[SelectedCapabilityRoot],
discoveries: Vec<Result<Arc<CapabilityRootDiscovery>, String>>,
) -> Self {
debug_assert_eq!(selected_roots.len(), discoveries.len());
Self {
roots: selected_roots
.iter()
.cloned()
.zip(discoveries)
.map(
|(selected_root, result)| ExecutorCapabilityDiscoverySnapshotEntry {
selected_root,
result,
},
)
.collect(),
}
}
pub fn roots(&self) -> &[ExecutorCapabilityDiscoverySnapshotEntry] {
&self.roots
}
}
/// HTTP header represented in the executor protocol.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]