Add executor capability discovery V2 infrastructure (#47683)

## What changed

- Define `capabilities/discoverV2` request and inventory types and advertise `capabilityDiscoveryV2` in executor metadata.
- Prewarm installed plugin and global skill locations at server startup, with nonfatal scan failures.
- Add sandbox-scoped discovery caching with file watcher invalidation and request-time reloads when watching is unavailable.
- Batch metadata reads through the filesystem sandbox helper and bound discovery responses to 4 MiB and 2,048 capabilities.
- Share plugin identity and installed-version selection in `codex-core-plugin-common`, and fall back to `.app.json` when a plugin does not declare an apps configuration.

The V2 discovery manager is not yet wired into RPC request dispatch.

## Testing

Add coverage for inventory metadata, sandbox cache isolation, watcher invalidation, startup scan failures, response limits, and compatibility with older executor metadata.

GitOrigin-RevId: a223a5bc15fdf251023c0c71f468fd92c3b2d847
This commit is contained in:
Anthony Tafoya
2026-09-23 22:20:34 +00:00
committed by copyberry
parent 6fa3bcaae8
commit ab7439231c
36 changed files with 2655 additions and 83 deletions
@@ -0,0 +1,102 @@
//! Executor capability inventory; callers apply identity and enablement.
use codex_file_system::FileSystemSandboxContext;
use codex_utils_path_uri::PathUri;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use crate::CapabilityTextFile;
use crate::DiscoveredPluginFiles;
pub const CAPABILITIES_DISCOVER_V2_METHOD: &str = "capabilities/discoverV2";
/// Discovers installed capabilities on the executor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverV2CapabilitiesRequest {
pub cwd: PathUri,
#[serde(default)]
pub sandbox: Option<FileSystemSandboxContext>,
}
/// Discovered metadata before caller policy is applied.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverV2CapabilitiesResponse {
pub plugins: Vec<ExecutorPlugin>,
/// Skills outside installed packages.
pub skills: Vec<ExecutorSkill>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutorPlugin {
/// Environment-local ID: `<name>@<marketplace>`.
pub id: String,
pub remote_plugin_id: Option<String>,
pub version: String,
pub root: PathUri,
pub files: DiscoveredPluginFiles,
pub skills: Vec<ExecutorSkill>,
}
/// Skill metadata, excluding instructions and resources.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutorSkill {
pub name: String,
/// Nearest enclosing legacy plugin's name, following V1 precedence.
#[serde(default)]
pub namespace: Option<String>,
pub description: String,
#[serde(default)]
pub short_description: Option<String>,
/// Path to SKILL.md, not its containing directory.
pub path: PathUri,
pub metadata: Option<CapabilityTextFile>,
}
impl fmt::Debug for ExecutorPlugin {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ExecutorPlugin")
.field("id", &self.id)
.field("remote_plugin_id", &self.remote_plugin_id)
.field("version", &self.version)
.field("root", &self.root)
.field("manifest_path", &self.files.manifest.path)
.field(
"mcp_config_path",
&self.files.mcp_config.as_ref().map(|file| &file.path),
)
.field(
"apps_config_path",
&self.files.apps_config.as_ref().map(|file| &file.path),
)
.field("skills", &self.skills)
.finish()
}
}
impl fmt::Debug for ExecutorSkill {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
// Omit potentially sensitive descriptions and file contents.
formatter
.debug_struct("ExecutorSkill")
.field("name", &self.name)
.field("namespace", &self.namespace)
.field("path", &self.path)
.field(
"metadata_path",
&self.metadata.as_ref().map(|file| &file.path),
)
.finish()
}
}
#[cfg(test)]
#[path = "capabilities_tests.rs"]
mod tests;
@@ -0,0 +1,51 @@
//! V2 discovery requests and responses round-trip through JSON.
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn discovery_contract_round_trips_json() -> Result<(), Box<dyn std::error::Error>> {
let request = DiscoverV2CapabilitiesRequest {
cwd: PathUri::parse("file:///workspace")?,
sandbox: None,
};
let wire = serde_json::to_value(&request)?;
assert_eq!(
serde_json::from_value::<DiscoverV2CapabilitiesRequest>(wire)?,
request
);
let text_file = CapabilityTextFile {
path: PathUri::parse("file:///plugin/config.json")?,
contents: "{}".to_string(),
};
let skill = ExecutorSkill {
name: "example".to_string(),
namespace: Some("nested".to_string()),
description: "Example skill".to_string(),
short_description: None,
path: PathUri::parse("file:///plugin/skills/example/SKILL.md")?,
metadata: Some(text_file.clone()),
};
let response = DiscoverV2CapabilitiesResponse {
plugins: vec![ExecutorPlugin {
id: "example@company".to_string(),
remote_plugin_id: None,
version: "local".to_string(),
root: PathUri::parse("file:///plugin")?,
files: DiscoveredPluginFiles {
manifest: text_file.clone(),
mcp_config: Some(text_file.clone()),
apps_config: Some(text_file),
},
skills: vec![skill.clone()],
}],
skills: vec![skill],
warnings: Vec::new(),
};
let wire = serde_json::to_value(&response)?;
assert_eq!(
serde_json::from_value::<DiscoverV2CapabilitiesResponse>(wire)?,
response
);
Ok(())
}
+2
View File
@@ -1,9 +1,11 @@
mod capabilities;
mod environment_config;
mod network_policy;
mod process_id;
mod protocol;
pub mod rpc;
pub use capabilities::*;
pub use environment_config::*;
pub use network_policy::*;
pub use process_id::ProcessId;
@@ -142,6 +142,9 @@ pub struct EnvironmentCapabilities {
/// Whether capability discovery applies the filesystem sandbox sent with each root.
#[serde(default)]
pub capability_discovery_sandbox: bool,
/// Whether this executor supports V2 capability discovery.
#[serde(default)]
pub capability_discovery_v2: bool,
/// Whether this executor supports the `environmentConfig/read` request.
#[serde(default)]
pub environment_config_read: bool,
@@ -248,6 +251,7 @@ impl EnvironmentInfo {
capabilities: EnvironmentCapabilities {
network_proxy_launch: true,
capability_discovery_sandbox: true,
capability_discovery_v2: true,
environment_config_read: true,
http_header_env_vars: true,
sandboxed_file_streaming: true,
@@ -1158,6 +1162,20 @@ mod base64_bytes {
#[cfg(test)]
mod tests {
use super::CapabilityRootDiscoverRequest;
#[test]
fn discovery_v2_support_defaults_off_for_older_executors() -> serde_json::Result<()> {
let legacy: super::EnvironmentCapabilities = serde_json::from_value(serde_json::json!({}))?;
assert!(!legacy.capability_discovery_v2);
let capabilities = super::EnvironmentInfo::local().capabilities;
assert_eq!(
serde_json::from_value::<super::EnvironmentCapabilities>(serde_json::to_value(
&capabilities
)?)?,
capabilities
);
Ok(())
}
use super::EnvironmentCapabilities;
use super::EnvironmentInfo;
use super::ExecExitedNotification;
@@ -1366,6 +1384,7 @@ mod tests {
EnvironmentCapabilities {
network_proxy_launch: true,
capability_discovery_sandbox: true,
capability_discovery_v2: false,
environment_config_read: false,
http_header_env_vars: false,
sandboxed_file_streaming: false,
@@ -1388,6 +1407,7 @@ mod tests {
"capabilities": {
"networkProxyLaunch": false,
"capabilityDiscoverySandbox": false,
"capabilityDiscoveryV2": false,
"environmentConfigRead": false,
"httpHeaderEnvVars": false,
"sandboxedFileStreaming": false,