mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
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:
committed by
copyberry
parent
6fa3bcaae8
commit
ab7439231c
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user