Enforce Agent Plugin runtime boundaries (#37027)

## What changed

- Track Agent Plugin manifests through plugin, skill, and MCP loading so their capabilities use format-specific behavior without changing legacy plugins.
- Discover only direct-child skills, exclude app and hook capabilities, isolate MCP data, and reject MCP configuration files that are non-regular or resolve outside the plugin root.
- Bound model-visible skill instructions, plugin instructions, MCP descriptions, schemas, individual tools, and the aggregate Agent Plugin MCP tool set.
- Stop MCP and OAuth redirects when Agent Plugins send configured or authorization headers, while retaining existing redirect behavior for legacy MCP servers.

## Testing

- Add coverage for capability filtering, skill discovery, isolated MCP data and reserved-path expansion, unsafe MCP configuration files, context limits, and redirect handling.

GitOrigin-RevId: c9af66b051269f3226628ca280a58d32c808c38f
This commit is contained in:
jacobzhou-oai
2026-08-05 04:54:33 +00:00
committed by copyberry
parent f21dc46388
commit 56b82e676c
63 changed files with 1830 additions and 106 deletions
+190 -35
View File
@@ -3,10 +3,11 @@ use crate::app_mcp_routing::apps_route_available;
use crate::command_migration::migrated_command_skills_root;
use crate::is_openai_curated_marketplace_name;
use crate::manifest::PluginManifest;
use crate::manifest::PluginManifestFormat;
use crate::manifest::PluginManifestHooks;
use crate::manifest::PluginManifestMcpServers;
use crate::manifest::PluginManifestPaths;
use crate::manifest::load_plugin_manifest;
use crate::manifest::load_plugin_manifest_with_format;
use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::find_marketplace_plugin;
use crate::marketplace::list_marketplaces_with_home;
@@ -23,6 +24,7 @@ use crate::store::plugin_version_for_source_with_fallback_manifest;
use codex_config::ConfigLayerStack;
use codex_config::HooksFile;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_config::types::PluginConfig;
use codex_config::types::PluginMcpServerConfig;
use codex_connectors::parse_plugin_app_config;
@@ -33,6 +35,7 @@ use codex_core_skills::config_rules::skill_config_rules_from_stack;
use codex_core_skills::loader::SkillRoot;
use codex_core_skills::loader::load_skills_from_roots;
use codex_exec_server::LOCAL_FS;
use codex_mcp::parse_agent_plugin_mcp_config;
use codex_mcp::parse_plugin_mcp_config;
use codex_plugin::AppDeclaration;
use codex_plugin::LoadedPlugin;
@@ -831,6 +834,7 @@ async fn load_plugin(
root,
enabled: plugin.enabled,
skill_roots: Vec::new(),
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
@@ -873,12 +877,19 @@ async fn load_plugin(
return loaded_plugin;
}
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root.as_path()) else {
loaded_plugin.error = Some("missing or invalid plugin.json".to_string());
return loaded_plugin;
};
loaded_plugin.skill_discovery_mode = match loaded_manifest.format {
PluginManifestFormat::Legacy => SkillDiscoveryMode::Recursive,
PluginManifestFormat::AgentPlugin => SkillDiscoveryMode::DirectChildren,
};
let manifest = loaded_manifest.manifest;
let manifest_paths = &manifest.paths;
let plugin_data_root = store.plugin_data_root(&loaded_plugin_id);
let mcp_plugin_data_root = store.mcp_data_root(&loaded_plugin_id, loaded_manifest.format);
loaded_plugin.plugin_namespace = Some(manifest.name.clone());
match scope {
PluginLoadScope::AllCapabilities {
@@ -890,40 +901,51 @@ async fn load_plugin(
} => {
loaded_plugin.manifest_name = Some(manifest.display_name().to_string());
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
loaded_plugin.skill_roots =
plugin_skill_roots(&plugin_root, manifest_paths, loaded_manifest.format);
let plugin_identity = PluginIdentity {
plugin_id: loaded_plugin_id.as_key(),
remote_plugin_id: loaded_plugin.remote_plugin_id.clone(),
};
let resolved_skills = load_plugin_skills_with_identity(
let resolved_skills = load_plugin_skill_inventory(
&plugin_root,
&plugin_identity,
&manifest,
loaded_manifest.format,
*restriction_product,
skill_config_rules,
*plugin_skill_snapshots,
Arc::clone(root_scan_slots),
)
.await;
.await
.resolve(skill_config_rules);
let has_enabled_skills = resolved_skills.has_enabled_skills();
loaded_plugin.disabled_skill_paths = resolved_skills.disabled_skill_paths;
loaded_plugin.has_enabled_skills = has_enabled_skills;
loaded_plugin.mcp_servers = load_plugin_mcp_servers_from_manifest(
loaded_plugin.mcp_servers = load_plugin_mcp_servers_from_manifest_with_format(
plugin_root.as_path(),
manifest_paths,
Some(&plugin.mcp_servers),
Some(mcp_plugin_data_root.as_path()),
loaded_manifest.format,
)
.await;
loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await;
if loaded_manifest.format == PluginManifestFormat::Legacy {
loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await;
}
}
PluginLoadScope::HooksOnly => {}
}
let (hook_sources, hook_load_warnings) = load_plugin_hooks(
&plugin_root,
&loaded_plugin_id,
&store.plugin_data_root(&loaded_plugin_id),
manifest_paths,
);
let (hook_sources, hook_load_warnings) =
if loaded_manifest.format == PluginManifestFormat::AgentPlugin {
(Vec::new(), Vec::new())
} else {
load_plugin_hooks(
&plugin_root,
&loaded_plugin_id,
&plugin_data_root,
manifest_paths,
)
};
loaded_plugin.hook_sources = hook_sources;
loaded_plugin.hook_load_warnings = hook_load_warnings;
loaded_plugin
@@ -961,7 +983,7 @@ impl PluginSkillInventory {
)
}
fn resolve(self, skill_config_rules: &SkillConfigRules) -> ResolvedPluginSkills {
pub(crate) fn resolve(self, skill_config_rules: &SkillConfigRules) -> ResolvedPluginSkills {
let disabled_skill_paths = resolve_disabled_skill_paths(&self.skills, skill_config_rules);
ResolvedPluginSkills {
skills: self.skills,
@@ -1031,6 +1053,7 @@ pub(crate) async fn load_plugin_skills_with_identity(
plugin_root,
plugin_identity,
manifest,
PluginManifestFormat::Legacy,
restriction_product,
plugin_skill_snapshots,
root_scan_slots,
@@ -1043,11 +1066,16 @@ pub(crate) async fn load_plugin_skill_inventory(
plugin_root: &AbsolutePathBuf,
plugin_identity: &PluginIdentity,
manifest: &PluginManifest,
manifest_format: PluginManifestFormat,
restriction_product: Option<Product>,
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
root_scan_slots: Arc<Semaphore>,
) -> PluginSkillInventory {
let roots = plugin_skill_roots(plugin_root, &manifest.paths)
let discovery_mode = match manifest_format {
PluginManifestFormat::Legacy => SkillDiscoveryMode::Recursive,
PluginManifestFormat::AgentPlugin => SkillDiscoveryMode::DirectChildren,
};
let roots = plugin_skill_roots(plugin_root, &manifest.paths, manifest_format)
.into_iter()
.map(|path| SkillRoot {
path,
@@ -1056,7 +1084,7 @@ pub(crate) async fn load_plugin_skill_inventory(
plugin_identity: Some(plugin_identity.clone()),
plugin_namespace: Some(manifest.name.clone()),
plugin_root: Some(plugin_root.clone()),
discovery_mode: SkillDiscoveryMode::Recursive,
discovery_mode,
})
.collect::<Vec<_>>();
let outcome = load_skills_from_roots(roots, plugin_skill_snapshots, root_scan_slots).await;
@@ -1098,15 +1126,18 @@ pub(crate) async fn load_plugin_skill_inventory(
fn plugin_skill_roots(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
manifest_format: PluginManifestFormat,
) -> Vec<AbsolutePathBuf> {
let mut paths = if manifest_paths.skills.is_empty() {
default_skill_roots(plugin_root)
} else {
manifest_paths.skills.clone()
};
let migrated_command_skills = migrated_command_skills_root(plugin_root);
if migrated_command_skills.is_dir() {
paths.push(migrated_command_skills);
if manifest_format == PluginManifestFormat::Legacy {
let migrated_command_skills = migrated_command_skills_root(plugin_root);
if migrated_command_skills.is_dir() {
paths.push(migrated_command_skills);
}
}
paths.sort_unstable();
paths.dedup();
@@ -1146,8 +1177,11 @@ fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
}
pub async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppDeclaration> {
if let Some(manifest) = load_plugin_manifest(plugin_root) {
return load_plugin_apps_from_manifest(plugin_root, &manifest.paths).await;
if let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root) {
if loaded_manifest.format == PluginManifestFormat::AgentPlugin {
return Vec::new();
}
return load_plugin_apps_from_manifest(plugin_root, &loaded_manifest.manifest.paths).await;
}
load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await
}
@@ -1345,14 +1379,40 @@ pub async fn plugin_capability_summary_from_root(
plugin_id: &PluginId,
plugin_root: &AbsolutePathBuf,
) -> Option<PluginCapabilitySummary> {
let manifest = load_plugin_manifest(plugin_root.as_path())?;
let loaded_manifest = load_plugin_manifest_with_format(plugin_root.as_path())?;
let manifest_format = loaded_manifest.format;
let manifest = loaded_manifest.manifest;
let plugin_identity = PluginIdentity {
plugin_id: plugin_id.as_key(),
remote_plugin_id: None,
};
let manifest_paths = &manifest.paths;
let has_skills = !plugin_skill_roots(plugin_root, manifest_paths).is_empty();
let mut mcp_server_names = load_plugin_mcp_servers_from_manifest(
let has_skills = match manifest_format {
PluginManifestFormat::Legacy => {
!plugin_skill_roots(plugin_root, manifest_paths, manifest_format).is_empty()
}
PluginManifestFormat::AgentPlugin => {
!load_plugin_skill_inventory(
plugin_root,
&plugin_identity,
&manifest,
manifest_format,
/*restriction_product*/ None,
/*plugin_skill_snapshots*/ None,
Arc::new(Semaphore::new(1)),
)
.await
.skills
.is_empty()
}
};
let mut mcp_server_names = load_plugin_mcp_servers_from_manifest_with_format(
plugin_root.as_path(),
manifest_paths,
/*plugin_policy*/ None,
/*plugin_data_root*/ None,
manifest_format,
)
.await
.into_keys()
@@ -1360,16 +1420,17 @@ pub async fn plugin_capability_summary_from_root(
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
let app_declarations = load_apps_from_paths(
plugin_root.as_path(),
plugin_app_config_paths(plugin_root.as_path(), manifest_paths),
)
.await;
let app_declarations = if manifest_format == PluginManifestFormat::AgentPlugin {
Vec::new()
} else {
load_plugin_apps_from_manifest(plugin_root.as_path(), manifest_paths).await
};
let app_connector_ids = app_connector_ids_from_declarations(&app_declarations);
Some(PluginCapabilitySummary {
config_name: plugin_id.as_key(),
display_name: plugin_id.plugin_name.clone(),
plugin_namespace: Some(manifest.name.clone()),
description: None,
has_skills,
mcp_server_names,
@@ -1426,17 +1487,26 @@ async fn load_declared_plugin_mcp_servers(
plugin_root: &Path,
plugin_policy: Option<&HashMap<String, PluginMcpServerConfig>>,
) -> HashMap<String, McpServerConfig> {
let Some(manifest) = load_plugin_manifest(plugin_root) else {
let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root) else {
return HashMap::new();
};
load_plugin_mcp_servers_from_manifest(plugin_root, &manifest.paths, plugin_policy).await
load_plugin_mcp_servers_from_manifest_with_format(
plugin_root,
&loaded_manifest.manifest.paths,
plugin_policy,
/*plugin_data_root*/ None,
loaded_manifest.format,
)
.await
}
pub(crate) async fn load_plugin_mcp_servers_from_manifest(
pub(crate) async fn load_plugin_mcp_servers_from_manifest_with_format(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
plugin_policy: Option<&HashMap<String, PluginMcpServerConfig>>,
plugin_data_root: Option<&Path>,
manifest_format: PluginManifestFormat,
) -> HashMap<String, McpServerConfig> {
let mut mcp_servers = HashMap::new();
match &manifest_paths.mcp_servers {
@@ -1457,7 +1527,13 @@ pub(crate) async fn load_plugin_mcp_servers_from_manifest(
}
Some(PluginManifestMcpServers::Path(_)) | None => {
for mcp_config_path in plugin_mcp_config_paths(plugin_root, manifest_paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await;
let plugin_mcp = load_mcp_servers_from_file(
plugin_root,
plugin_data_root,
manifest_format,
&mcp_config_path,
)
.await;
for (name, mut config) in plugin_mcp.mcp_servers {
if let Some(policy) = plugin_policy.and_then(|policy| policy.get(&name)) {
apply_plugin_mcp_server_policy(&mut config, policy);
@@ -1480,12 +1556,74 @@ pub(crate) async fn load_plugin_mcp_servers_from_manifest(
async fn load_mcp_servers_from_file(
plugin_root: &Path,
plugin_data_root: Option<&Path>,
manifest_format: PluginManifestFormat,
mcp_config_path: &AbsolutePathBuf,
) -> PluginMcpDiscovery {
let is_agent_plugin_mcp = manifest_format == PluginManifestFormat::AgentPlugin;
if is_agent_plugin_mcp {
match tokio::fs::symlink_metadata(mcp_config_path.as_path()).await {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
warn!(
path = %mcp_config_path.display(),
"Agent Plugins MCP config is not a regular file; disabling MCP"
);
return PluginMcpDiscovery::default();
}
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return PluginMcpDiscovery::default();
}
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to inspect Agent Plugins MCP config; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
}
let resolved_root = match tokio::fs::canonicalize(plugin_root).await {
Ok(path) => path,
Err(err) => {
warn!(
plugin = %plugin_root.display(),
"failed to resolve Agent Plugins root; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
};
let resolved_config = match tokio::fs::canonicalize(mcp_config_path.as_path()).await {
Ok(path) => path,
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to resolve Agent Plugins MCP config; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
};
if !resolved_config.starts_with(&resolved_root) {
warn!(
plugin = %plugin_root.display(),
path = %mcp_config_path.display(),
"Agent Plugins MCP config resolves outside the plugin root; disabling MCP"
);
return PluginMcpDiscovery::default();
}
}
let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else {
return PluginMcpDiscovery::default();
};
let parsed = match parse_plugin_mcp_config(plugin_root, &contents) {
let fallback_data_root = plugin_root.join(".plugin-data");
let mut parsed = match if is_agent_plugin_mcp {
parse_agent_plugin_mcp_config(
plugin_root,
plugin_data_root.unwrap_or(&fallback_data_root),
&contents,
)
} else {
parse_plugin_mcp_config(plugin_root, &contents)
} {
Ok(parsed) => parsed,
Err(err) => {
warn!(
@@ -1495,6 +1633,23 @@ async fn load_mcp_servers_from_file(
return PluginMcpDiscovery::default();
}
};
if is_agent_plugin_mcp
&& let Some(plugin_data_root) = plugin_data_root
&& parsed
.servers
.values()
.any(|server| matches!(&server.transport, McpServerTransportConfig::Stdio { .. }))
&& let Err(err) = tokio::fs::create_dir_all(plugin_data_root).await
{
warn!(
plugin = %plugin_root.display(),
path = %plugin_data_root.display(),
"failed to create Agent Plugins data directory; disabling stdio MCP servers: {err}"
);
parsed.servers.retain(|_, server| {
!matches!(&server.transport, McpServerTransportConfig::Stdio { .. })
});
}
for error in parsed.errors {
warn!(
plugin = %plugin_root.display(),
+176 -2
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::manifest::load_plugin_manifest;
use crate::manifest::load_plugin_manifest_with_format;
use crate::test_support::write_file;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
@@ -7,6 +8,7 @@ use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS;
use codex_plugin::PluginId;
use codex_utils_plugins::AGENT_PLUGIN_SCHEMA_URI;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -25,6 +27,177 @@ fn user_layer(path: AbsolutePathBuf, config: &str) -> ConfigLayerEntry {
)
}
#[tokio::test]
async fn agent_plugin_overlay_apps_are_not_runtime_active() {
let temp_dir = TempDir::new().expect("tempdir");
let plugin_root = temp_dir.path().join("plugin");
write_file(
&plugin_root.join("plugin.json"),
&format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin"}}"#),
);
write_file(
&plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"plugin","apps":"./.app.json"}"#,
);
write_file(
&plugin_root.join(".app.json"),
r#"{"apps":{"example":{"id":"connector_example"}}}"#,
);
assert!(load_plugin_apps(&plugin_root).await.is_empty());
}
#[cfg(unix)]
#[tokio::test]
async fn agent_plugin_mcp_rejects_config_symlink_outside_plugin_root() {
let temp_dir = TempDir::new().expect("tempdir");
let plugin_root = temp_dir.path().join("plugin");
let outside_config = temp_dir.path().join("outside-mcp.json");
fs::create_dir_all(&plugin_root).expect("create plugin root");
fs::write(
plugin_root.join("plugin.json"),
format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin"}}"#),
)
.expect("write Agent Plugins manifest");
fs::write(
&outside_config,
r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"outside":{"type":"stdio","command":"echo"}}}"#,
)
.expect("write outside MCP config");
std::os::unix::fs::symlink(&outside_config, plugin_root.join("mcp.json"))
.expect("create MCP symlink");
let config_path = AbsolutePathBuf::from_absolute_path(plugin_root.join("mcp.json"))
.expect("absolute MCP path");
let discovered = load_mcp_servers_from_file(
&plugin_root,
/*plugin_data_root*/ None,
PluginManifestFormat::AgentPlugin,
&config_path,
)
.await;
assert!(discovered.mcp_servers.is_empty());
}
#[tokio::test]
async fn agent_plugin_mcp_rejects_present_nonregular_config() {
let temp_dir = TempDir::new().expect("tempdir");
let plugin_root = temp_dir.path().join("plugin");
let config_path = plugin_root.join("mcp.json");
fs::create_dir_all(&config_path).expect("create nonregular MCP config");
let discovered = load_mcp_servers_from_file(
&plugin_root,
/*plugin_data_root*/ None,
PluginManifestFormat::AgentPlugin,
&AbsolutePathBuf::from_absolute_path(config_path).expect("absolute MCP path"),
)
.await;
assert!(discovered.mcp_servers.is_empty());
}
#[tokio::test]
async fn legacy_manifest_can_point_at_root_mcp_json() {
let temp_dir = TempDir::new().expect("tempdir");
let plugin_root = temp_dir.path().join("plugin");
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest directory");
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"plugin","mcpServers":"./mcp.json"}"#,
)
.expect("write legacy manifest");
fs::write(
plugin_root.join("mcp.json"),
r#"{"mcpServers":{"legacy":{"command":"echo"}}}"#,
)
.expect("write legacy MCP config");
let manifest = load_plugin_manifest(&plugin_root).expect("load legacy manifest");
let discovered = load_plugin_mcp_servers_from_manifest_with_format(
&plugin_root,
&manifest.paths,
/*plugin_policy*/ None,
/*plugin_data_root*/ None,
PluginManifestFormat::Legacy,
)
.await;
assert_eq!(
discovered.keys().collect::<Vec<_>>(),
vec![&"legacy".to_string()]
);
}
#[tokio::test]
async fn installed_agent_plugin_uses_isolated_data_root_for_stdio_mcp() {
let temp_dir = TempDir::new().expect("tempdir");
let plugin_root = temp_dir.path().join("plugins/cache/c/a-b/local");
write_file(
&plugin_root.join("plugin.json"),
&format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"a-b"}}"#),
);
write_file(
&plugin_root.join("mcp.json"),
r#"{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"example": {
"type": "stdio",
"command": "echo"
}
}
}"#,
);
let stack = ConfigLayerStack::new(
vec![user_layer(
user_config_path(&temp_dir, "config.toml"),
"[plugins.\"a-b@c\"]\nenabled = true\n",
)],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("valid config layer stack");
let store = PluginStore::new(temp_dir.path().to_path_buf());
let plugins = load_plugins_from_layer_stack(
&stack,
RemoteInstalledPluginsSnapshot::default(),
&store,
/*plugin_skill_snapshots*/ None,
Some(Product::Codex),
/*remote_global_catalog_active*/ false,
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
)
.await;
let expected_data_root = temp_dir
.path()
.join("plugins")
.join("data")
.join("agent-plugins")
.join("6920dd17774030852d11d1b94758fcaae4f894c7b2f36301ed174bc3b33e0743");
let expected_data_root = AbsolutePathBuf::from_absolute_path(expected_data_root)
.expect("absolute Agent Plugin data root")
.canonicalize()
.expect("canonical Agent Plugin data root");
let server = plugins
.first()
.and_then(|plugin| plugin.mcp_servers.get("example"))
.expect("Agent plugin stdio MCP server");
let McpServerTransportConfig::Stdio { env, .. } = &server.transport else {
panic!("expected stdio MCP server");
};
assert_eq!(
env.as_ref()
.and_then(|env| env.get("PLUGIN_DATA"))
.map(String::as_str),
expected_data_root.as_path().to_str()
);
assert!(expected_data_root.as_path().is_dir());
}
#[test]
fn configured_plugins_from_stack_merges_user_layers() {
let temp_dir = TempDir::new().expect("tempdir");
@@ -265,7 +438,8 @@ fn write_hook_file(plugin_root: &AbsolutePathBuf, relative_path: &str, event: &s
}
fn load_sources(plugin_root: &AbsolutePathBuf) -> (Vec<PluginHookSource>, Vec<String>) {
let manifest = load_plugin_manifest(plugin_root.as_path()).expect("manifest");
let loaded_manifest =
load_plugin_manifest_with_format(plugin_root.as_path()).expect("manifest");
let plugin_data_root = AbsolutePathBuf::try_from(
plugin_root
.as_path()
@@ -278,7 +452,7 @@ fn load_sources(plugin_root: &AbsolutePathBuf) -> (Vec<PluginHookSource>, Vec<St
plugin_root,
&plugin_id(),
&plugin_data_root,
&manifest.paths,
&loaded_manifest.manifest.paths,
)
}
+37 -15
View File
@@ -10,8 +10,8 @@ use crate::loader::curated_plugin_cache_version;
use crate::loader::load_plugin_apps_from_manifest;
use crate::loader::load_plugin_hooks;
use crate::loader::load_plugin_hooks_from_layer_stack;
use crate::loader::load_plugin_mcp_servers_from_manifest;
use crate::loader::load_plugin_skills_with_identity;
use crate::loader::load_plugin_mcp_servers_from_manifest_with_format;
use crate::loader::load_plugin_skill_inventory;
use crate::loader::load_plugins_from_layer_stack;
use crate::loader::log_plugin_load_errors;
use crate::loader::materialize_marketplace_plugin_source;
@@ -21,8 +21,10 @@ use crate::loader::refresh_curated_plugin_cache;
use crate::loader::refresh_non_curated_plugin_cache_detailed;
use crate::loader::refresh_non_curated_plugin_cache_force_reinstall_detailed;
use crate::loader::remote_installed_plugins_to_config;
use crate::manifest::PluginManifestFormat;
use crate::manifest::PluginManifestInterface;
use crate::manifest::load_plugin_manifest;
use crate::manifest::load_plugin_manifest_with_format;
use crate::marketplace::MarketplaceError;
use crate::marketplace::MarketplaceInterface;
use crate::marketplace::MarketplaceListError;
@@ -416,6 +418,7 @@ impl From<PluginDetail> for PluginCapabilitySummary {
Self {
config_name: value.id,
display_name: value.name,
plugin_namespace: None,
description: prompt_safe_plugin_description(value.description.as_deref()),
has_skills,
mcp_server_names: value.mcp_server_names,
@@ -1997,18 +2000,24 @@ impl PluginsManager {
"path does not exist or is not a directory".to_string(),
));
}
let manifest =
let loaded_manifest =
if codex_utils_plugins::find_plugin_manifest_path(source_path.as_path()).is_some() {
load_plugin_manifest(source_path.as_path())
load_plugin_manifest_with_format(source_path.as_path())
} else {
plugin
.manifest_fallback
.as_ref()
.and_then(|fallback| fallback.parse_for_plugin_root(source_path.as_path()))
.map(|manifest| crate::manifest::LoadedPluginManifest {
manifest,
format: PluginManifestFormat::Legacy,
})
}
.ok_or_else(|| {
MarketplaceError::InvalidPlugin("missing or invalid plugin.json".to_string())
})?;
let manifest_format = loaded_manifest.format;
let manifest = loaded_manifest.manifest;
let description = manifest.description.clone();
let marketplace_category = plugin
.interface
@@ -2022,21 +2031,27 @@ impl PluginsManager {
plugin_id: plugin_id.as_key(),
remote_plugin_id: self.remote_plugin_id_for(&plugin_id),
};
let resolved_skills = load_plugin_skills_with_identity(
let skill_config_rules = codex_core_skills::config_rules::skill_config_rules_from_stack(
&config.config_layer_stack,
);
let resolved_skills = load_plugin_skill_inventory(
&source_path,
&plugin_identity,
&manifest,
manifest_format,
self.restriction_product,
&codex_core_skills::config_rules::skill_config_rules_from_stack(
&config.config_layer_stack,
),
/*plugin_skill_snapshots*/ None,
Arc::clone(&self.skill_root_scan_slots),
)
.await;
.await
.resolve(&skill_config_rules);
let plugin_data_root = self.store.plugin_data_root(&plugin_id);
let (hook_sources, _hook_load_warnings) =
load_plugin_hooks(&source_path, &plugin_id, &plugin_data_root, &manifest.paths);
let (hook_sources, _hook_load_warnings) = if manifest_format == PluginManifestFormat::Legacy
{
load_plugin_hooks(&source_path, &plugin_id, &plugin_data_root, &manifest.paths)
} else {
(Vec::new(), Vec::new())
};
let hooks = plugin_hook_declarations(&hook_sources)
.into_iter()
.map(|hook| PluginHookSummary {
@@ -2045,15 +2060,22 @@ impl PluginsManager {
})
.collect();
let auth_mode = self.auth_mode();
let mut app_declarations =
load_plugin_apps_from_manifest(source_path.as_path(), &manifest.paths).await;
let mut mcp_servers = load_plugin_mcp_servers_from_manifest(
let mut app_declarations = if manifest_format == PluginManifestFormat::Legacy {
load_plugin_apps_from_manifest(source_path.as_path(), &manifest.paths).await
} else {
Vec::new()
};
let mcp_data_root = (manifest_format == PluginManifestFormat::AgentPlugin)
.then(|| self.store.mcp_data_root(&plugin_id, manifest_format));
let mut mcp_servers = load_plugin_mcp_servers_from_manifest_with_format(
source_path.as_path(),
&manifest.paths,
/*plugin_policy*/ None,
mcp_data_root.as_deref(),
manifest_format,
)
.await;
if auth_mode.is_some() {
if manifest_format == PluginManifestFormat::Legacy && auth_mode.is_some() {
apply_app_mcp_routing_policy(
&mut app_declarations,
&mut mcp_servers,
+118 -2
View File
@@ -52,6 +52,7 @@ use codex_skills_extension::HostSkillsLoadInput;
use codex_skills_extension::HostSkillsService;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_plugins::SkillDiscoveryMode;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
@@ -675,6 +676,7 @@ async fn plugin_auth_projection_reprojects_cached_plugins_when_auth_changes() {
PluginCapabilitySummary {
config_name: "docs@test".to_string(),
display_name: "docs".to_string(),
plugin_namespace: Some("docs".to_string()),
description: None,
has_skills: false,
mcp_server_names: vec!["docs".to_string()],
@@ -683,6 +685,7 @@ async fn plugin_auth_projection_reprojects_cached_plugins_when_auth_changes() {
PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: None,
has_skills: false,
mcp_server_names: Vec::new(),
@@ -705,6 +708,7 @@ async fn plugin_auth_projection_reprojects_cached_plugins_when_auth_changes() {
PluginCapabilitySummary {
config_name: "docs@test".to_string(),
display_name: "docs".to_string(),
plugin_namespace: Some("docs".to_string()),
description: None,
has_skills: false,
mcp_server_names: vec!["docs".to_string()],
@@ -713,6 +717,7 @@ async fn plugin_auth_projection_reprojects_cached_plugins_when_auth_changes() {
PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: None,
has_skills: false,
mcp_server_names: vec!["sample".to_string()],
@@ -739,7 +744,11 @@ fn write_plugin_with_version(
format!(r#"{{"name":"{manifest_name}"{version}}}"#),
)
.unwrap();
fs::write(plugin_root.join("skills/SKILL.md"), "skill").unwrap();
fs::write(
plugin_root.join("skills/SKILL.md"),
format!("---\nname: {manifest_name}-skill\ndescription: test skill\n---\n\n# Test skill\n"),
)
.unwrap();
fs::write(plugin_root.join(".mcp.json"), r#"{"mcpServers":{}}"#).unwrap();
}
@@ -931,6 +940,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(),
enabled: true,
skill_roots: vec![plugin_root.join("skills").abs()],
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: true,
mcp_servers: HashMap::from([(
@@ -973,6 +983,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
&[PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: Some("Plugin that includes the sample MCP server and Skills".to_string(),),
has_skills: true,
mcp_server_names: vec!["sample".to_string()],
@@ -1232,6 +1243,7 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
capability_summary: Some(PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
@@ -1264,6 +1276,7 @@ async fn installed_plugin_telemetry_metadata_resolves_persisted_remote_identity(
capability_summary: Some(PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "linear".to_string(),
plugin_namespace: Some("linear".to_string()),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
@@ -1317,6 +1330,7 @@ async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity()
capability_summary: Some(PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "linear".to_string(),
plugin_namespace: Some("linear".to_string()),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
@@ -1354,6 +1368,7 @@ fn capability_summary_telemetry_metadata_uses_local_identity() {
let summary = PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "Linear".to_string(),
plugin_namespace: Some("linear".to_string()),
description: Some("Track work".to_string()),
has_skills: true,
mcp_server_names: vec!["linear".to_string()],
@@ -1387,6 +1402,7 @@ fn capability_summary_telemetry_metadata_resolves_persisted_remote_identity() {
let summary = PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "Linear".to_string(),
plugin_namespace: Some("linear".to_string()),
description: Some("Track work".to_string()),
has_skills: true,
mcp_server_names: vec!["linear".to_string()],
@@ -1772,6 +1788,7 @@ enabled = true
&[PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
@@ -1817,6 +1834,7 @@ async fn plugin_telemetry_metadata_uses_default_mcp_config_path() {
Some(PluginCapabilitySummary {
config_name: "sample@test".to_string(),
display_name: "sample".to_string(),
plugin_namespace: Some("sample".to_string()),
description: None,
has_skills: false,
mcp_server_names: vec!["sample".to_string()],
@@ -1858,6 +1876,7 @@ async fn plugin_capability_summary_uses_manifest_mcp_server_objects() {
Some(PluginCapabilitySummary {
config_name: "counter-sample@test".to_string(),
display_name: "counter-sample".to_string(),
plugin_namespace: Some("counter-sample".to_string()),
description: None,
has_skills: false,
mcp_server_names: vec!["counter".to_string()],
@@ -2273,7 +2292,6 @@ async fn load_plugin_skills_dedupes_overlapping_manifest_roots() {
interface: None,
};
let plugin_id = PluginId::parse("sample@test").expect("plugin id should parse");
let resolved = load_plugin_skills(
&plugin_root,
&plugin_id,
@@ -2502,6 +2520,7 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
root: AbsolutePathBuf::try_from(plugin_root).unwrap(),
enabled: false,
skill_roots: Vec::new(),
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
@@ -2679,6 +2698,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
enabled: true,
skill_roots: Vec::new(),
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
@@ -2690,6 +2710,12 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
let summary = |config_name: &str, display_name: &str| PluginCapabilitySummary {
config_name: config_name.to_string(),
display_name: display_name.to_string(),
plugin_namespace: Some(
config_name
.split_once('@')
.map_or(config_name, |(name, _)| name)
.to_string(),
),
description: None,
..PluginCapabilitySummary::default()
};
@@ -3985,6 +4011,96 @@ plugins = true
);
}
#[tokio::test]
async fn agent_plugin_read_and_tool_suggestions_use_portable_capabilities_only() {
let tmp = tempfile::tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let plugin_root = repo_root.join("agent-plugin");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
write_file(
&repo_root.join(".agents/plugins/marketplace.json"),
r#"{"name":"debug","plugins":[{"name":"agent-plugin","source":"./agent-plugin"}]}"#,
);
write_file(
&plugin_root.join("plugin.json"),
r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"agent.tools"}"#,
);
write_file(
&plugin_root.join("skills/direct/SKILL.md"),
"---\nname: direct\ndescription: Direct skill\n---\n",
);
write_file(
&plugin_root.join("skills/group/nested/SKILL.md"),
"---\nname: nested\ndescription: Nested skill\n---\n",
);
write_file(
&plugin_root.join("mcp.json"),
r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"portable":{"type":"stdio","command":"echo"}}}"#,
);
write_file(
&plugin_root.join(".codex-plugin/plugin.json"),
r#"{"apps":"./.app.json","hooks":"./hooks/hooks.json"}"#,
);
write_file(
&plugin_root.join(".app.json"),
r#"{"apps":{"legacy":{"id":"connector_legacy"}}}"#,
);
write_file(
&plugin_root.join("hooks/hooks.json"),
r#"{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo legacy"}]}]}}"#,
);
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
"[features]\nplugins = true\n",
);
let config = load_config(tmp.path(), &repo_root).await;
let manager = PluginsManager::new(tmp.path().to_path_buf());
let plugin = manager
.list_marketplaces_for_config(
&config,
&[AbsolutePathBuf::try_from(repo_root).unwrap()],
/*include_openai_curated*/ false,
)
.unwrap()
.marketplaces
.into_iter()
.find(|marketplace| marketplace.name == "debug")
.unwrap()
.plugins
.into_iter()
.find(|plugin| plugin.name == "agent-plugin")
.unwrap();
let detail = manager
.read_plugin_detail_for_marketplace_plugin(&config, "debug", plugin.clone())
.await
.unwrap();
let suggestion = manager
.tool_suggest_metadata_for_marketplace_plugin(
"debug",
&plugin,
&SkillConfigRules::default(),
)
.await
.unwrap();
assert_eq!(
detail
.skills
.iter()
.map(|skill| skill.name.as_str())
.collect::<Vec<_>>(),
vec!["agent.tools:direct"]
);
assert_eq!(detail.mcp_server_names, vec!["portable"]);
assert!(detail.apps.is_empty());
assert!(detail.hooks.is_empty());
assert!(suggestion.has_skills);
assert_eq!(suggestion.mcp_server_names, vec!["portable"]);
assert!(suggestion.app_connector_ids.is_empty());
}
#[tokio::test]
async fn read_plugin_for_config_does_not_fallback_from_invalid_plugin_manifest() {
let tmp = tempfile::tempdir().unwrap();
+28 -1
View File
@@ -31,6 +31,17 @@ pub type PluginManifestPaths = codex_plugin::manifest::PluginManifestPaths<Absol
pub type UriPluginManifest = codex_plugin::manifest::PluginManifest<PathUri>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PluginManifestFormat {
Legacy,
AgentPlugin,
}
pub(crate) struct LoadedPluginManifest {
pub manifest: PluginManifest,
pub format: PluginManifestFormat,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawPluginManifest {
@@ -143,6 +154,15 @@ enum RawPluginManifestHooks {
/// Loads a plugin manifest from the local host filesystem.
pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
load_plugin_manifest_with_format(plugin_root).map(|loaded| loaded.manifest)
}
pub fn is_agent_plugin_manifest(plugin_root: &Path) -> bool {
load_plugin_manifest_with_format(plugin_root)
.is_some_and(|loaded| loaded.format == PluginManifestFormat::AgentPlugin)
}
pub(crate) fn load_plugin_manifest_with_format(plugin_root: &Path) -> Option<LoadedPluginManifest> {
let manifest_path = find_plugin_manifest_path(plugin_root)?;
let contents = fs::read_to_string(&manifest_path).ok()?;
let is_agent_plugin = manifest_path == plugin_root.join(AGENT_PLUGIN_MANIFEST_RELATIVE_PATH);
@@ -162,7 +182,14 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
.as_ref()
.map(|(path, contents)| (path.as_path(), contents.as_str())),
) {
Ok(manifest) => Some(manifest),
Ok(manifest) => Some(LoadedPluginManifest {
manifest,
format: if is_agent_plugin {
PluginManifestFormat::AgentPlugin
} else {
PluginManifestFormat::Legacy
},
}),
Err(err) => {
tracing::warn!(
path = %manifest_path.display(),
+1
View File
@@ -1524,6 +1524,7 @@ pub async fn resolve_remote_plugin_uninstall_target(
let fallback_capability_summary = PluginCapabilitySummary {
config_name: plugin_id.as_key(),
display_name: plugin.release.display_name,
plugin_namespace: Some(plugin_id.plugin_name.clone()),
description: prompt_safe_plugin_description(Some(&plugin.release.description)),
has_skills: !plugin.release.skills.is_empty(),
mcp_server_names,
@@ -10,6 +10,7 @@ use crate::test_support::write_curated_plugin_sha_with;
use crate::test_support::write_openai_api_curated_marketplace;
use crate::test_support::write_openai_curated_marketplace;
use codex_plugin::PluginLoadOutcome;
use codex_utils_plugins::SkillDiscoveryMode;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -30,6 +31,7 @@ fn loaded_plugin(config_name: &str, root: &Path, enabled: bool) -> LoadedPlugin
root: path(root),
enabled,
skill_roots: Vec::new(),
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
+23
View File
@@ -1,5 +1,6 @@
use crate::command_migration::migrate_plugin_commands;
use crate::manifest::PluginManifest;
use crate::manifest::PluginManifestFormat;
use crate::manifest::load_plugin_manifest;
use crate::manifest::parse_plugin_manifest;
use codex_plugin::PluginId;
@@ -24,6 +25,7 @@ use std::path::PathBuf;
pub const DEFAULT_PLUGIN_VERSION: &str = "local";
pub const PLUGINS_CACHE_DIR: &str = "plugins/cache";
pub const PLUGINS_DATA_DIR: &str = "plugins/data";
const AGENT_PLUGINS_DATA_DIR: &str = "agent-plugins";
const REMOTE_PLUGIN_INSTALL_METADATA_FILE: &str = ".codex-remote-plugin-install.json";
const REMOTE_PLUGIN_INSTALL_METADATA_SCHEMA_VERSION: u8 = 1;
const DEFAULT_AGENT_PLUGIN_VERSION: &str = "1.0.0";
@@ -143,6 +145,27 @@ impl PluginStore {
))
}
pub(crate) fn agent_plugin_data_root(&self, plugin_id: &PluginId) -> AbsolutePathBuf {
let mut digest = Sha256::new();
digest.update(plugin_id.marketplace_name.as_bytes());
digest.update([0]);
digest.update(plugin_id.plugin_name.as_bytes());
self.data_root
.join(AGENT_PLUGINS_DATA_DIR)
.join(hex_prefix(&digest.finalize(), /*count*/ 32))
}
pub(crate) fn mcp_data_root(
&self,
plugin_id: &PluginId,
manifest_format: PluginManifestFormat,
) -> AbsolutePathBuf {
match manifest_format {
PluginManifestFormat::AgentPlugin => self.agent_plugin_data_root(plugin_id),
PluginManifestFormat::Legacy => self.plugin_data_root(plugin_id),
}
}
pub fn active_plugin_version(&self, plugin_id: &PluginId) -> Option<String> {
let mut discovered_versions = fs::read_dir(self.plugin_base_root(plugin_id).as_path())
.ok()?
+22
View File
@@ -162,6 +162,28 @@ fn plugin_data_root_derives_path_from_key() {
);
}
#[test]
fn agent_plugin_data_root_is_stable_and_unambiguous() {
let tmp = tempdir().unwrap();
let store = PluginStore::new(tmp.path().to_path_buf());
let first = PluginId::new("a-b".to_string(), "c".to_string()).unwrap();
let second = PluginId::new("a".to_string(), "b-c".to_string()).unwrap();
let first_root = store.agent_plugin_data_root(&first);
let second_root = store.agent_plugin_data_root(&second);
let expected_parent = tmp.path().join("plugins/data/agent-plugins");
assert_ne!(first_root, second_root);
assert_eq!(
first_root.as_path(),
expected_parent.join("6920dd17774030852d11d1b94758fcaae4f894c7b2f36301ed174bc3b33e0743")
);
assert_eq!(
second_root.as_path(),
expected_parent.join("fa89b988ebbe54a68fdcbeb87fb913a5238d482084a3cee49a86288c2d45fa90")
);
}
#[test]
fn install_with_version_uses_requested_cache_version() {
let tmp = tempdir().unwrap();
@@ -21,7 +21,8 @@ use crate::loader::load_plugin_mcp_servers;
use crate::loader::load_plugin_skill_inventory;
use crate::manager::ConfiguredMarketplacePlugin;
use crate::manager::remote_plugin_install_required_description;
use crate::manifest::load_plugin_manifest;
use crate::manifest::PluginManifestFormat;
use crate::manifest::load_plugin_manifest_with_format;
use crate::marketplace::MarketplaceError;
use crate::marketplace::MarketplacePluginSource;
@@ -53,6 +54,7 @@ struct PluginArtifactIdentity {
pub(crate) struct ToolSuggestMetadataFragment {
config_name: String,
display_name: String,
plugin_namespace: Option<String>,
description: Option<String>,
mcp_server_names: Vec<String>,
app_declarations: Vec<AppDeclaration>,
@@ -86,6 +88,7 @@ impl ToolSuggestMetadataFragment {
PluginCapabilitySummary {
config_name: self.config_name.clone(),
display_name: self.display_name.clone(),
plugin_namespace: self.plugin_namespace.clone(),
description: self.description.clone(),
has_skills: self
.skill_inventory
@@ -206,6 +209,7 @@ async fn load_plugin_metadata(
return Ok(Arc::new(ToolSuggestMetadataFragment {
config_name: plugin.id.clone(),
display_name: plugin.name.clone(),
plugin_namespace: Some(plugin.name.clone()),
description: prompt_safe_plugin_description(Some(
&remote_plugin_install_required_description(&plugin.source),
)),
@@ -217,16 +221,18 @@ async fn load_plugin_metadata(
if !plugin_root.as_path().is_dir() {
return Err("path does not exist or is not a directory".to_string());
}
let manifest = load_plugin_manifest(plugin_root.as_path())
let loaded_manifest = load_plugin_manifest_with_format(plugin_root.as_path())
.ok_or_else(|| "missing or invalid plugin.json".to_string())?;
let plugin_identity = PluginIdentity {
plugin_id: plugin_id.as_key(),
remote_plugin_id: None,
};
let manifest = loaded_manifest.manifest;
let skill_inventory = load_plugin_skill_inventory(
plugin_root,
&plugin_identity,
&manifest,
loaded_manifest.format,
restriction_product,
/*plugin_skill_snapshots*/ None,
root_scan_slots,
@@ -239,11 +245,16 @@ async fn load_plugin_metadata(
.collect::<Vec<_>>();
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
let app_declarations = load_plugin_apps(plugin_root.as_path()).await;
let app_declarations = if loaded_manifest.format == PluginManifestFormat::AgentPlugin {
Vec::new()
} else {
load_plugin_apps(plugin_root.as_path()).await
};
Ok(Arc::new(ToolSuggestMetadataFragment {
config_name: plugin.id.clone(),
display_name: plugin.name.clone(),
plugin_namespace: Some(manifest.name.clone()),
description: prompt_safe_plugin_description(manifest.description.as_deref()),
mcp_server_names,
app_declarations,