Migrate plugin commands into skills on install (#33411)

## What changed

- Convert supported plugin command Markdown into generated skills when a plugin is installed.
- Read command sources from the manifest's `commands` field, falling back to the plugin's `commands/` directory.
- Load generated command skills alongside native plugin skills while preferring native skills with the same name.
- Skip commands with unsupported templates, missing descriptions, name collisions, or generated skills larger than 4 KB, and keep migration failures from blocking plugin installation.

## Testing

- Cover default and manifest-configured command paths, stale generated skill cleanup, size limits, native-skill precedence, invalid manifest fields, and non-fatal migration errors.

GitOrigin-RevId: 67d910048544b160b191066569bca69b2b951ebc
This commit is contained in:
charlesgong-openai
2026-07-15 19:51:37 +00:00
committed by copyberry
parent 38b064c31b
commit 2cd6ed7509
14 changed files with 984 additions and 427 deletions
+30
View File
@@ -1,5 +1,6 @@
use crate::app_mcp_routing::apply_app_mcp_routing_policy;
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::PluginManifestHooks;
@@ -958,11 +959,36 @@ pub(crate) async fn load_plugin_skill_inventory(
.collect::<Vec<_>>();
let outcome = load_skills_from_roots(roots, plugin_skill_snapshots, root_scan_slots).await;
let had_errors = !outcome.errors.is_empty();
let migrated_command_skills = migrated_command_skills_root(plugin_root);
let migrated_command_skills = fs::canonicalize(migrated_command_skills.as_path())
.ok()
.and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok())
.unwrap_or(migrated_command_skills);
let skills = outcome
.skills
.into_iter()
.filter(|skill| skill.matches_product_restriction_for_product(restriction_product))
.collect::<Vec<_>>();
let native_skill_names = skills
.iter()
.filter(|skill| {
!skill
.path_to_skills_md
.as_path()
.starts_with(migrated_command_skills.as_path())
})
.map(|skill| skill.name.clone())
.collect::<HashSet<_>>();
let skills = skills
.into_iter()
.filter(|skill| {
!skill
.path_to_skills_md
.as_path()
.starts_with(migrated_command_skills.as_path())
|| !native_skill_names.contains(&skill.name)
})
.collect::<Vec<_>>();
PluginSkillInventory { skills, had_errors }
}
@@ -976,6 +1002,10 @@ fn plugin_skill_roots(
} 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);
}
paths.sort_unstable();
paths.dedup();
paths