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
@@ -6,16 +6,19 @@ use std::sync::Arc;
|
||||
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_skills::parse_skill_frontmatter_metadata;
|
||||
use codex_skills::parse_skill_metadata;
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::CapabilityRootDiscoverRequest;
|
||||
use crate::CapabilityRootDiscovery;
|
||||
use crate::CapabilityTextFile;
|
||||
use crate::DiscoverV2CapabilitiesResponse;
|
||||
use crate::ExecutorPlugin;
|
||||
use crate::ExecutorSkill;
|
||||
use crate::LocalFileSystem;
|
||||
use crate::SkillDiscoveryRoot;
|
||||
use crate::capability_discovery::MAX_CONCURRENT_ROOTS;
|
||||
use crate::capability_discovery::discover_root;
|
||||
use crate::discover_v2::capability_locations::CapabilityLocation;
|
||||
@@ -39,6 +42,23 @@ struct CapabilityDiscovery {
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
// Only the metadata V2 returns or needs to materialize discovered configuration.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PluginManifestMetadata {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
interface: Option<PluginManifestInterface>,
|
||||
mcp_servers: Option<serde_json::Value>,
|
||||
apps: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PluginManifestInterface {
|
||||
display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for CapabilityDiscovery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -237,18 +257,87 @@ fn parse_root(
|
||||
}
|
||||
ancestor = root.parent();
|
||||
}
|
||||
let metadata = skill.metadata.as_ref().and_then(|metadata| {
|
||||
parse_skill_metadata(&metadata.contents).map_err(|error| {
|
||||
tracing::warn!(path = %metadata.path, %error, "ignoring invalid discovered skill metadata");
|
||||
}).ok()
|
||||
}).unwrap_or_default();
|
||||
skills.push(ExecutorSkill {
|
||||
name: parsed.name,
|
||||
namespace,
|
||||
name: namespace
|
||||
.map(|namespace| format!("{namespace}:{}", parsed.name))
|
||||
.unwrap_or(parsed.name),
|
||||
description: parsed.description,
|
||||
short_description: parsed.short_description,
|
||||
path,
|
||||
metadata: skill.metadata,
|
||||
discovery_root: location
|
||||
.standalone_root_id
|
||||
.as_ref()
|
||||
.map(|id| SkillDiscoveryRoot {
|
||||
id: id.clone(),
|
||||
path: location.root.clone(),
|
||||
}),
|
||||
dependencies: metadata.dependencies,
|
||||
policy: metadata.policy,
|
||||
});
|
||||
}
|
||||
let content = if let (Some(plugin), Some(files)) = (location.plugin, discovery.plugin) {
|
||||
let content = if let (Some(plugin), Some(mut files)) = (location.plugin, discovery.plugin) {
|
||||
let manifest =
|
||||
match serde_json::from_str::<PluginManifestMetadata>(&files.manifest.contents) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(_) => {
|
||||
warnings.push(format!(
|
||||
"invalid plugin manifest at {}",
|
||||
files.manifest.path
|
||||
));
|
||||
return CapabilityDiscovery {
|
||||
warnings,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
let name = if manifest.name.trim().is_empty() {
|
||||
location.root.basename().unwrap_or(manifest.name)
|
||||
} else {
|
||||
manifest.name
|
||||
};
|
||||
let display_name = manifest
|
||||
.interface
|
||||
.and_then(|interface| interface.display_name)
|
||||
.map(|display_name| display_name.trim().to_string())
|
||||
.filter(|display_name| !display_name.is_empty())
|
||||
.unwrap_or(name);
|
||||
// V2 materializes inline declarations; the path identifies their source manifest.
|
||||
if files.mcp_config.is_none() {
|
||||
if let Some(contents) = manifest
|
||||
.mcp_servers
|
||||
.as_ref()
|
||||
.filter(|value| value.is_object())
|
||||
{
|
||||
files.mcp_config = Some(CapabilityTextFile {
|
||||
path: files.manifest.path.clone(),
|
||||
contents: contents.to_string(),
|
||||
});
|
||||
} else if manifest
|
||||
.mcp_servers
|
||||
.as_ref()
|
||||
.is_some_and(serde_json::Value::is_string)
|
||||
{
|
||||
warnings.push(format!(
|
||||
"plugin `{}` at {} declares an MCP configuration file, but discovery returned no contents",
|
||||
plugin.id, location.root
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if files.apps_config.is_none() && manifest.apps.is_some() {
|
||||
warnings.push(format!(
|
||||
"plugin `{}` at {} declares an Apps configuration file, but discovery returned no contents",
|
||||
plugin.id, location.root
|
||||
));
|
||||
}
|
||||
DiscoveredSource::Plugin(Box::new(ExecutorPlugin {
|
||||
id: plugin.id,
|
||||
display_name,
|
||||
remote_plugin_id: plugin.remote_plugin_id,
|
||||
version: plugin.version,
|
||||
root: location.root,
|
||||
|
||||
@@ -39,6 +39,7 @@ const AGENTS_SKILLS_SUFFIX: &str = ".agents/skills";
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct CapabilityLocation {
|
||||
pub root: PathUri,
|
||||
pub standalone_root_id: Option<String>,
|
||||
pub plugin: Option<InstalledPlugin>,
|
||||
}
|
||||
|
||||
@@ -69,19 +70,22 @@ pub(crate) async fn prewarm_locations(
|
||||
.map(PathUri::to_abs_path)
|
||||
.transpose()?;
|
||||
// Fixed standalone roots extend Flora's V1 root; project skill discovery is deferred.
|
||||
let mut skill_roots = vec![codex_home.join("skills")];
|
||||
let mut skill_roots = vec![("codex-home-skills", codex_home.join("skills"))];
|
||||
if let Some(home) = user_home {
|
||||
skill_roots.push(home.join(AGENTS_SKILLS_SUFFIX));
|
||||
skill_roots.push(("user-agent-skills", home.join(AGENTS_SKILLS_SUFFIX)));
|
||||
}
|
||||
if let Some(system_root) = &request.system_skills_root {
|
||||
skill_roots.push(system_root.to_abs_path()?);
|
||||
skill_roots.push(("system-skills", system_root.to_abs_path()?));
|
||||
}
|
||||
CandidateCapabilityLocations::resolve(
|
||||
fs,
|
||||
Some(PathUri::from_abs_path(&codex_home.join(PLUGINS_CACHE_DIR))),
|
||||
skill_roots
|
||||
.into_iter()
|
||||
.map(|root| PathUri::from_abs_path(&root))
|
||||
.map(|(id, root)| crate::SkillDiscoveryRoot {
|
||||
id: id.to_string(),
|
||||
path: PathUri::from_abs_path(&root),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
@@ -171,6 +175,7 @@ async fn discover_cached_plugins(
|
||||
};
|
||||
locations.locations.push(CapabilityLocation {
|
||||
root: PathUri::from_abs_path(&package_root.join(&version)),
|
||||
standalone_root_id: None,
|
||||
plugin: Some(InstalledPlugin {
|
||||
id,
|
||||
version,
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::CapabilityLocations;
|
||||
use super::MAX_LOCATIONS;
|
||||
use super::discover_cached_plugins;
|
||||
use crate::LocalFileSystem;
|
||||
use crate::SkillDiscoveryRoot;
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_file_watcher::WatchPath;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -22,7 +23,7 @@ pub(super) struct CandidateCapabilityLocations {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum CandidateCapabilityLocation {
|
||||
Cache(PathUri),
|
||||
Standalone(PathUri),
|
||||
Standalone(SkillDiscoveryRoot),
|
||||
}
|
||||
|
||||
impl CandidateCapabilityLocations {
|
||||
@@ -30,7 +31,7 @@ impl CandidateCapabilityLocations {
|
||||
pub(super) async fn resolve(
|
||||
fs: &LocalFileSystem,
|
||||
cache_root: Option<PathUri>,
|
||||
skill_roots: Vec<PathUri>,
|
||||
skill_roots: Vec<SkillDiscoveryRoot>,
|
||||
) -> io::Result<CapabilityLocations> {
|
||||
let mut candidates = cache_root
|
||||
.into_iter()
|
||||
@@ -38,7 +39,7 @@ impl CandidateCapabilityLocations {
|
||||
.collect::<Vec<_>>();
|
||||
let mut standalone = skill_roots;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
standalone.retain(|root| seen.insert(root.clone()));
|
||||
standalone.retain(|root| seen.insert(root.path.clone()));
|
||||
candidates.extend(
|
||||
standalone
|
||||
.into_iter()
|
||||
@@ -49,8 +50,8 @@ impl CandidateCapabilityLocations {
|
||||
let mut retained_candidates = Vec::new();
|
||||
for candidate in candidates {
|
||||
let root = match &candidate {
|
||||
CandidateCapabilityLocation::Cache(root)
|
||||
| CandidateCapabilityLocation::Standalone(root) => root,
|
||||
CandidateCapabilityLocation::Cache(root) => root,
|
||||
CandidateCapabilityLocation::Standalone(root) => &root.path,
|
||||
};
|
||||
let exists = match fs
|
||||
.get_metadata(root, Default::default(), /*sandbox*/ None)
|
||||
@@ -90,7 +91,8 @@ impl CandidateCapabilityLocations {
|
||||
// Standalone roots need no manifest or version selection.
|
||||
CandidateCapabilityLocation::Standalone(root) => Ok(CapabilityLocations {
|
||||
locations: vec![CapabilityLocation {
|
||||
root: root.clone(),
|
||||
root: root.path.clone(),
|
||||
standalone_root_id: Some(root.id.clone()),
|
||||
plugin: None,
|
||||
}],
|
||||
..Default::default()
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::CapabilityRootDiscoverRequest;
|
||||
use crate::DiscoverV2CapabilitiesRequest;
|
||||
use crate::ExecutorSkill;
|
||||
use crate::LocalFileSystem;
|
||||
use crate::SkillDiscoveryRoot;
|
||||
use crate::capability_discovery::discover_root;
|
||||
use crate::discover_v2::capability_locations::tests::MANIFEST;
|
||||
use crate::discover_v2::capability_locations::tests::request;
|
||||
@@ -36,6 +37,24 @@ async fn discovery_preserves_inventory_and_v1_metadata() -> anyhow::Result<()> {
|
||||
MANIFEST,
|
||||
)?;
|
||||
}
|
||||
let inline_root = "codex/plugins/cache/test/unconfigured/local";
|
||||
write(
|
||||
temp.path(),
|
||||
"codex/plugins/cache/test/disabled/local/.codex-plugin/plugin.json",
|
||||
r#"{"name":" ","interface":{"displayName":" "},"mcpServers":"./missing-mcp.json","apps":"./missing-apps.json"}"#,
|
||||
)?;
|
||||
let inline_manifest = r#"{"name":"inline","interface":{"displayName":" Inline Plugin "},"mcpServers":{"inline":{"command":"node"}}}"#;
|
||||
write(
|
||||
temp.path(),
|
||||
&format!("{inline_root}/.codex-plugin/plugin.json"),
|
||||
inline_manifest,
|
||||
)?;
|
||||
// Inline declarations take precedence over a conventional MCP file.
|
||||
write(
|
||||
temp.path(),
|
||||
&format!("{inline_root}/.mcp.json"),
|
||||
r#"{"wrong":{"command":"wrong"}}"#,
|
||||
)?;
|
||||
let root = "codex/plugins/cache/test/demo/local";
|
||||
write(
|
||||
temp.path(),
|
||||
@@ -86,6 +105,17 @@ async fn discovery_preserves_inventory_and_v1_metadata() -> anyhow::Result<()> {
|
||||
.await;
|
||||
// V1 keeps undeclared/migrated skills and excludes the skill beyond its depth limit.
|
||||
assert_eq!(v1.skills.len(), 4);
|
||||
let inline_v1 = discover_root(
|
||||
&file_system,
|
||||
CapabilityRootDiscoverRequest {
|
||||
id: "unconfigured@test".to_string(),
|
||||
path: PathUri::from_host_native_path(temp.path().join(inline_root))?,
|
||||
sandbox: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let inline_v1_files = inline_v1.plugin.expect("inline plugin");
|
||||
assert_eq!(inline_v1_files.mcp_config, None);
|
||||
let manager = CapabilityManager::new(file_system);
|
||||
manager.seed_prewarmed_locations(request.clone()).await?;
|
||||
let response = manager
|
||||
@@ -102,6 +132,14 @@ async fn discovery_preserves_inventory_and_v1_metadata() -> anyhow::Result<()> {
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["demo@test", "disabled@test", "unconfigured@test"],
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| plugin.display_name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["demo", "local", "Inline Plugin"],
|
||||
);
|
||||
let plugin = response
|
||||
.plugins
|
||||
.first()
|
||||
@@ -122,38 +160,60 @@ async fn discovery_preserves_inventory_and_v1_metadata() -> anyhow::Result<()> {
|
||||
v1.skills
|
||||
.into_iter()
|
||||
.map(|skill| {
|
||||
let namespace = if skill
|
||||
let name = if skill
|
||||
.instructions
|
||||
.path
|
||||
.starts_with(&plugin.root.join("nested")?)
|
||||
{
|
||||
"team"
|
||||
"team:deploy"
|
||||
} else {
|
||||
"demo"
|
||||
"demo:deploy"
|
||||
};
|
||||
Ok(expected_skill(skill.instructions.path, Some(namespace)))
|
||||
Ok(expected_skill(skill.instructions.path, name))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?
|
||||
);
|
||||
assert_eq!(
|
||||
response.skills,
|
||||
vec![
|
||||
ExecutorSkill {
|
||||
name: "deploy skill".to_string(),
|
||||
..expected_skill(
|
||||
request.codex_home.join("skills/deploy skill/SKILL.md")?,
|
||||
/*namespace*/ None,
|
||||
)
|
||||
},
|
||||
expected_skill(
|
||||
request.codex_home.join("skills/deploy skill/SKILL.md")?,
|
||||
"deploy skill",
|
||||
),
|
||||
expected_skill(
|
||||
request
|
||||
.codex_home
|
||||
.join("skills/nested/skills/deploy/SKILL.md")?,
|
||||
Some("team")
|
||||
"team:deploy"
|
||||
),
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|mut skill| {
|
||||
skill.discovery_root = Some(SkillDiscoveryRoot {
|
||||
id: "codex-home-skills".to_string(),
|
||||
path: request.codex_home.join("skills").expect("skills root"),
|
||||
});
|
||||
skill
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let inline_plugin = response
|
||||
.plugins
|
||||
.iter()
|
||||
.find(|plugin| plugin.id == "unconfigured@test")
|
||||
.expect("inline plugin");
|
||||
assert_eq!(inline_plugin.files.manifest, inline_v1_files.manifest);
|
||||
assert_eq!(
|
||||
inline_plugin.files.mcp_config,
|
||||
Some(crate::CapabilityTextFile {
|
||||
path: inline_v1_files.manifest.path,
|
||||
contents: serde_json::json!({"inline": {"command": "node"}}).to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
response.warnings,
|
||||
vec!["some capability metadata could not be loaded; discovery may be incomplete"],
|
||||
);
|
||||
assert_eq!(response.warnings, v1.warnings);
|
||||
let json = serde_json::to_string(&response)?;
|
||||
assert!(
|
||||
response.plugins.len() + plugin.skills.len() + response.skills.len() <= MAX_CAPABILITIES
|
||||
@@ -164,14 +224,15 @@ async fn discovery_preserves_inventory_and_v1_metadata() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expected_skill(path: PathUri, namespace: Option<&str>) -> ExecutorSkill {
|
||||
fn expected_skill(path: PathUri, name: &str) -> ExecutorSkill {
|
||||
ExecutorSkill {
|
||||
name: "deploy".to_string(),
|
||||
namespace: namespace.map(str::to_string),
|
||||
name: name.to_string(),
|
||||
description: "Deploy the service.".to_string(),
|
||||
short_description: None,
|
||||
path,
|
||||
metadata: None,
|
||||
discovery_root: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,3 +275,133 @@ async fn discovery_reports_location_failures_without_paths() -> anyhow::Result<(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discovery_resolves_optional_skill_metadata() -> anyhow::Result<()> {
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_skills::SkillDependencies;
|
||||
use codex_skills::SkillPolicy;
|
||||
use codex_skills::SkillToolDependency;
|
||||
|
||||
let dependencies = Some(SkillDependencies {
|
||||
tools: vec![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),
|
||||
}],
|
||||
});
|
||||
let policy = Some(SkillPolicy {
|
||||
allow_implicit_invocation: Some(false),
|
||||
products: vec![Product::Codex, Product::Chatgpt],
|
||||
});
|
||||
for (yaml, dependencies, policy) in [
|
||||
(None, None, None),
|
||||
(Some("interface: [invalid]"), None, None),
|
||||
(Some("dependencies:\n tools: [{type: mcp}]"), None, None),
|
||||
(Some("dependencies: {tools: []}"), None, None),
|
||||
(
|
||||
Some(
|
||||
r#"dependencies:
|
||||
tools:
|
||||
- type: " mcp "
|
||||
value: " deploy "
|
||||
transport: stdio
|
||||
command: deploy-mcp
|
||||
oauth: {callback_port: 3118}
|
||||
- type: mcp
|
||||
value: " "
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
products: [codex, CHATGPT]
|
||||
"#,
|
||||
),
|
||||
dependencies,
|
||||
policy,
|
||||
),
|
||||
] {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let request = request(temp.path())?;
|
||||
let plugin_root = "codex/plugins/cache/test/demo/local";
|
||||
write(
|
||||
temp.path(),
|
||||
&format!("{plugin_root}/.codex-plugin/plugin.json"),
|
||||
MANIFEST,
|
||||
)?;
|
||||
for root in [
|
||||
"codex/skills/deploy".to_string(),
|
||||
format!("{plugin_root}/skills/deploy"),
|
||||
] {
|
||||
write(temp.path(), &format!("{root}/SKILL.md"), SKILL)?;
|
||||
if let Some(yaml) = yaml {
|
||||
write(temp.path(), &format!("{root}/agents/openai.yaml"), yaml)?;
|
||||
}
|
||||
}
|
||||
let manager = CapabilityManager::new(LocalFileSystem::unsandboxed());
|
||||
manager.seed_prewarmed_locations(request.clone()).await?;
|
||||
let response = manager
|
||||
.get_or_refresh_discovery(DiscoverV2CapabilitiesRequest {
|
||||
cwd: PathUri::from_host_native_path(temp.path().join("project"))?,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
let mut expected =
|
||||
expected_skill(request.codex_home.join("skills/deploy/SKILL.md")?, "deploy");
|
||||
expected.dependencies = dependencies;
|
||||
expected.policy = policy;
|
||||
expected.discovery_root = Some(SkillDiscoveryRoot {
|
||||
id: "codex-home-skills".to_string(),
|
||||
path: request.codex_home.join("skills")?,
|
||||
});
|
||||
assert_eq!(response.skills, vec![expected.clone()]);
|
||||
expected.path = request
|
||||
.codex_home
|
||||
.join("plugins/cache/test/demo/local/skills/deploy/SKILL.md")?;
|
||||
expected.name = "demo:deploy".into();
|
||||
expected.discovery_root = None;
|
||||
assert_eq!(response.plugins[0].skills, vec![expected]);
|
||||
assert_eq!(response.warnings, Vec::<String>::new());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_skills_retain_their_search_roots() -> anyhow::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let request = request(temp.path())?;
|
||||
let roots = [
|
||||
("codex-home-skills", "codex/skills"),
|
||||
("user-agent-skills", "home/.agents/skills"),
|
||||
("system-skills", "etc/codex/skills"),
|
||||
];
|
||||
let mut expected = Vec::new();
|
||||
for (id, root) in roots {
|
||||
for relative in ["deploy/SKILL.md", "nested/deploy/SKILL.md"] {
|
||||
let path = format!("{root}/{relative}");
|
||||
write(temp.path(), &path, SKILL)?;
|
||||
let mut skill = expected_skill(
|
||||
PathUri::from_host_native_path(temp.path().join(path))?,
|
||||
"deploy",
|
||||
);
|
||||
skill.discovery_root = Some(SkillDiscoveryRoot {
|
||||
id: id.to_string(),
|
||||
path: PathUri::from_host_native_path(temp.path().join(root))?,
|
||||
});
|
||||
expected.push(skill);
|
||||
}
|
||||
}
|
||||
let manager = CapabilityManager::new(LocalFileSystem::unsandboxed());
|
||||
manager.seed_prewarmed_locations(request.clone()).await?;
|
||||
let query = DiscoverV2CapabilitiesRequest {
|
||||
cwd: PathUri::from_host_native_path(temp.path().join("project"))?,
|
||||
sandbox: None,
|
||||
};
|
||||
let response = manager.get_or_refresh_discovery(query.clone()).await?;
|
||||
assert_eq!(response.skills, expected);
|
||||
// Cached discovery must retain the same root metadata.
|
||||
assert_eq!(manager.get_or_refresh_discovery(query).await?, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -67,15 +67,21 @@ async fn prewarming_resolves_global_locations_without_cwd_or_config() -> anyhow:
|
||||
actual.sort_unstable_by_key(|location| location.root.to_string());
|
||||
let mut expected = vec![CapabilityLocation {
|
||||
root: request.codex_home.join("plugins/cache/team/demo/1.10.0")?,
|
||||
standalone_root_id: None,
|
||||
plugin: Some(InstalledPlugin {
|
||||
id: "demo@team".to_string(),
|
||||
version: "1.10.0".to_string(),
|
||||
remote_plugin_id: Some("plugins~demo".to_string()),
|
||||
}),
|
||||
}];
|
||||
for root in ["codex/skills", "home/.agents/skills", "etc/codex/skills"] {
|
||||
for (id, root) in [
|
||||
("codex-home-skills", "codex/skills"),
|
||||
("user-agent-skills", "home/.agents/skills"),
|
||||
("system-skills", "etc/codex/skills"),
|
||||
] {
|
||||
expected.push(CapabilityLocation {
|
||||
root: PathUri::from_host_native_path(temp.path().join(root))?,
|
||||
standalone_root_id: Some(id.to_string()),
|
||||
plugin: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ pub use protocol::ShellInfo;
|
||||
pub use protocol::ShellSnapshotRequest;
|
||||
pub use protocol::SignalParams;
|
||||
pub use protocol::SignalResponse;
|
||||
pub use protocol::SkillDiscoveryRoot;
|
||||
pub use protocol::TerminateParams;
|
||||
pub use protocol::TerminateResponse;
|
||||
pub use protocol::WriteParams;
|
||||
|
||||
Reference in New Issue
Block a user