mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Enrich V2 capability discovery with resolved plugin and skill metadata (#53763)
## What changed - Return qualified skill names such as `team:deploy` and parsed `dependencies` and `policy` instead of a separate namespace and raw metadata file. Share `agents/openai.yaml` parsing and validation between skill loaders and executor discovery. - Include each standalone skill's search-root ID and path in `discoveryRoot`, while accepting responses from older executors that omit it. - Add plugin `displayName`, falling back to the manifest name or installation directory. - Materialize inline MCP declarations in `mcpConfig` with the manifest as their source path, and report incomplete discovery when declared MCP or Apps configuration files have no returned contents. ## Testing Add coverage for JSON round trips, omitted `discoveryRoot`, optional and invalid skill metadata, qualified names, display-name fallbacks, and inline MCP configuration. Verify standalone search roots survive cached discovery and sandboxed RPC responses. GitOrigin-RevId: 4124972444ad7c2fb8c6418acbf9a589d797fad2
This commit is contained in:
committed by
copyberry
parent
21c1616119
commit
20b4ff2f35
@@ -1,13 +1,14 @@
|
||||
//! Executor capability inventory; callers apply identity and enablement.
|
||||
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_skills::SkillDependencies;
|
||||
use codex_skills::SkillPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::CapabilityTextFile;
|
||||
use crate::DiscoveredPluginFiles;
|
||||
|
||||
pub const CAPABILITIES_DISCOVER_V2_METHOD: &str = "capabilities/discoverV2";
|
||||
@@ -44,6 +45,8 @@ pub struct DiscoverV2CapabilitiesResponse {
|
||||
pub struct ExecutorPlugin {
|
||||
/// Environment-local ID: `<name>@<marketplace>`.
|
||||
pub id: String,
|
||||
/// Manifest display name, falling back to its name or the installation directory.
|
||||
pub display_name: String,
|
||||
pub remote_plugin_id: Option<String>,
|
||||
pub version: String,
|
||||
pub root: PathUri,
|
||||
@@ -51,20 +54,30 @@ pub struct ExecutorPlugin {
|
||||
pub skills: Vec<ExecutorSkill>,
|
||||
}
|
||||
|
||||
/// Executor-owned search root for a standalone skill.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SkillDiscoveryRoot {
|
||||
pub id: String,
|
||||
pub path: PathUri,
|
||||
}
|
||||
|
||||
/// Skill metadata, excluding instructions and resources.
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecutorSkill {
|
||||
/// Final skill name, qualified by the nearest enclosing plugin namespace when present.
|
||||
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>,
|
||||
/// Absent for plugin skills and responses from older executors.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub discovery_root: Option<SkillDiscoveryRoot>,
|
||||
pub dependencies: Option<SkillDependencies>,
|
||||
pub policy: Option<SkillPolicy>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ExecutorPlugin {
|
||||
@@ -95,12 +108,7 @@ impl fmt::Debug for ExecutorSkill {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! V2 discovery requests and responses round-trip through JSON.
|
||||
|
||||
use super::*;
|
||||
use crate::CapabilityTextFile;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -19,16 +20,37 @@ fn discovery_contract_round_trips_json() -> Result<(), Box<dyn std::error::Error
|
||||
contents: "{}".to_string(),
|
||||
};
|
||||
let skill = ExecutorSkill {
|
||||
name: "example".to_string(),
|
||||
namespace: Some("nested".to_string()),
|
||||
name: "nested:example".to_string(),
|
||||
description: "Example skill".to_string(),
|
||||
short_description: None,
|
||||
path: PathUri::parse("file:///plugin/skills/example/SKILL.md")?,
|
||||
metadata: Some(text_file.clone()),
|
||||
discovery_root: None,
|
||||
dependencies: Some(codex_skills::SkillDependencies {
|
||||
tools: vec![codex_skills::SkillToolDependency {
|
||||
r#type: "mcp".into(),
|
||||
value: "deploy".into(),
|
||||
description: None,
|
||||
transport: Some("stdio".into()),
|
||||
command: Some("deploy-mcp".into()),
|
||||
url: None,
|
||||
oauth_callback_port: Some(3118),
|
||||
}],
|
||||
}),
|
||||
policy: Some(codex_skills::SkillPolicy {
|
||||
allow_implicit_invocation: Some(false),
|
||||
products: vec![],
|
||||
}),
|
||||
};
|
||||
let mut standalone = skill.clone();
|
||||
standalone.path = PathUri::parse("file:///home/skills/nested/example/SKILL.md")?;
|
||||
standalone.discovery_root = Some(SkillDiscoveryRoot {
|
||||
id: "codex-home-skills".to_string(),
|
||||
path: PathUri::parse("file:///home/skills")?,
|
||||
});
|
||||
let response = DiscoverV2CapabilitiesResponse {
|
||||
plugins: vec![ExecutorPlugin {
|
||||
id: "example@company".to_string(),
|
||||
display_name: "Example".to_string(),
|
||||
remote_plugin_id: None,
|
||||
version: "local".to_string(),
|
||||
root: PathUri::parse("file:///plugin")?,
|
||||
@@ -37,15 +59,34 @@ fn discovery_contract_round_trips_json() -> Result<(), Box<dyn std::error::Error
|
||||
mcp_config: Some(text_file.clone()),
|
||||
apps_config: Some(text_file),
|
||||
},
|
||||
skills: vec![skill.clone()],
|
||||
skills: vec![skill],
|
||||
}],
|
||||
skills: vec![skill],
|
||||
skills: vec![standalone],
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
let wire = serde_json::to_value(&response)?;
|
||||
let mut wire = serde_json::to_value(&response)?;
|
||||
assert_eq!(
|
||||
wire["skills"][0]["discoveryRoot"],
|
||||
serde_json::json!({
|
||||
"id": "codex-home-skills",
|
||||
"path": "file:///home/skills",
|
||||
})
|
||||
);
|
||||
assert_eq!(wire["plugins"][0]["skills"][0].get("discoveryRoot"), None);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<DiscoverV2CapabilitiesResponse>(wire.clone())?,
|
||||
response
|
||||
);
|
||||
// Older executors omit the root metadata.
|
||||
wire["skills"][0]
|
||||
.as_object_mut()
|
||||
.expect("skill object")
|
||||
.remove("discoveryRoot");
|
||||
let mut legacy_response = response;
|
||||
legacy_response.skills[0].discovery_root = None;
|
||||
assert_eq!(
|
||||
serde_json::from_value::<DiscoverV2CapabilitiesResponse>(wire)?,
|
||||
response
|
||||
legacy_response
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -966,7 +966,8 @@ pub struct CapabilityRootsDiscoverResponse {
|
||||
pub roots: Vec<CapabilityRootDiscovery>,
|
||||
}
|
||||
|
||||
/// Recognized UTF-8 capability file materialized by the exec-server.
|
||||
/// UTF-8 capability contents materialized by the exec-server.
|
||||
/// `path` identifies the source file; V2 inline MCP contents are extracted from its manifest.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilityTextFile {
|
||||
@@ -979,7 +980,9 @@ pub struct CapabilityTextFile {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DiscoveredPluginFiles {
|
||||
pub manifest: CapabilityTextFile,
|
||||
/// File-backed MCP declarations, including the conventional `.mcp.json` fallback.
|
||||
/// MCP declarations from a selected file, including the conventional `.mcp.json` fallback.
|
||||
/// V2 also materializes inline declarations, using the manifest as the source path.
|
||||
/// V1 leaves inline declarations only in `manifest`.
|
||||
#[serde(default)]
|
||||
pub mcp_config: Option<CapabilityTextFile>,
|
||||
/// File-backed connector declarations.
|
||||
|
||||
Reference in New Issue
Block a user