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
+160
View File
@@ -1885,6 +1885,153 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
}
}
#[tokio::test]
async fn install_plugin_materializes_default_command_skills() {
let codex_home = TempDir::new().unwrap();
let source_root = codex_home.path().join("source/sample");
write_file(
&source_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "sample",
"skills": "./custom-skills/"
}"#,
);
fs::create_dir_all(source_root.join("custom-skills")).unwrap();
write_file(
&source_root.join("custom-skills/source-command-pr-review/SKILL.md"),
"---\nname: source-command-pr-review\ndescription: Native review skill\n---\n",
);
write_file(
&source_root.join("commands/pr/review.md"),
"---\ndescription: Review a pull request\n---\nInspect the proposed changes.\n",
);
write_file(
&source_root.join("commands/summarize.md"),
"---\ndescription: Summarize a change\n---\nSummarize the proposed changes.\n",
);
write_file(
&source_root.join("commands/oversized.md"),
&format!("---\ndescription: Oversized\n---\n{}", "x".repeat(4_000)),
);
write_file(
&source_root.join(".codex-plugin/migrated-command-skills/undeclared-command/SKILL.md"),
"---\nname: undeclared-command\ndescription: undeclared command\n---\n",
);
let result = PluginStore::new(codex_home.path().to_path_buf())
.install(
source_root.abs(),
PluginId::parse("sample@test").expect("plugin id should parse"),
)
.unwrap();
let migrated_skill = result
.installed_path
.join(".codex-plugin/migrated-command-skills/source-command-pr-review/SKILL.md");
let expected_migrated_skill = "---\nname: \"source-command-pr-review\"\ndescription: \"Review a pull request\"\n---\n\n# source-command-pr-review\n\nUse this skill when the user asks to run the migrated source command `pr-review`.\n\n## Command Template\n\nInspect the proposed changes.\n";
assert_eq!(
fs::read_to_string(&migrated_skill).unwrap(),
expected_migrated_skill
);
assert!(
!result
.installed_path
.join(".codex-plugin/migrated-command-skills/undeclared-command")
.exists()
);
assert!(
!result
.installed_path
.join(".codex-plugin/migrated-command-skills/source-command-oversized")
.exists()
);
let manifest = crate::manifest::load_plugin_manifest(&result.installed_path).unwrap();
let resolved = load_plugin_skills(
&result.installed_path,
&result.plugin_id,
&manifest,
/*restriction_product*/ None,
&SkillConfigRules::default(),
/*plugin_skill_snapshots*/ None,
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
)
.await;
assert_eq!(
resolved
.skills
.iter()
.map(|skill| skill.path_to_skills_md.clone())
.collect::<Vec<_>>(),
vec![
AbsolutePathBuf::from_absolute_path_checked(
fs::canonicalize(
result
.installed_path
.join("custom-skills/source-command-pr-review/SKILL.md")
)
.unwrap()
)
.unwrap(),
AbsolutePathBuf::from_absolute_path_checked(
fs::canonicalize(result.installed_path.join(
".codex-plugin/migrated-command-skills/source-command-summarize/SKILL.md"
))
.unwrap()
)
.unwrap()
]
);
}
#[test]
fn install_plugin_ignores_invalid_commands_manifest_field() {
let codex_home = TempDir::new().unwrap();
let source_root = codex_home.path().join("source/sample");
write_file(
&source_root.join(".codex-plugin/plugin.json"),
r#"{"name":"sample","commands":{}}"#,
);
write_file(
&source_root.join("commands/review.md"),
"---\ndescription: Review\n---\nReview the current change.\n",
);
let result = PluginStore::new(codex_home.path().to_path_buf())
.install(
source_root.abs(),
PluginId::parse("sample@test").expect("plugin id should parse"),
)
.unwrap();
assert!(
!result
.installed_path
.join(".codex-plugin/migrated-command-skills")
.exists()
);
}
#[test]
fn install_plugin_ignores_command_migration_errors() {
let codex_home = TempDir::new().unwrap();
let source_root = codex_home.path().join("source/sample");
write_file(
&source_root.join(".codex-plugin/plugin.json"),
r#"{"name":"sample","commands":"./commands/review.md"}"#,
);
fs::create_dir_all(source_root.join("commands")).unwrap();
fs::write(source_root.join("commands/review.md"), [0xff]).unwrap();
let result = PluginStore::new(codex_home.path().to_path_buf())
.install(
source_root.abs(),
PluginId::parse("sample@test").expect("plugin id should parse"),
)
.unwrap();
assert!(result.installed_path.join("commands/review.md").is_file());
}
#[tokio::test]
async fn load_plugin_skills_dedupes_overlapping_manifest_roots() {
let codex_home = TempDir::new().unwrap();
@@ -2851,6 +2998,10 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
"review skill",
)
.unwrap();
write_file(
&plugin_root.join("commands/review.md"),
"---\ndescription: Review code\n---\nReview the current change.\n",
);
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
@@ -2866,6 +3017,7 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
"skills": [
"./skills/thermo-nuclear-code-quality-review"
],
"commands": ["./commands/review.md"],
"category": "code-review"
}
]
@@ -2931,6 +3083,14 @@ async fn install_plugin_writes_marketplace_manifest_fallback_when_missing_plugin
serde_json::json!({ "name": "Byron Grogan" })
);
assert_eq!(fallback_json["category"], "code-review");
assert_eq!(
fs::read_to_string(
installed_path
.join(".codex-plugin/migrated-command-skills/source-command-review/SKILL.md")
)
.unwrap(),
"---\nname: \"source-command-review\"\ndescription: \"Review code\"\n---\n\n# source-command-review\n\nUse this skill when the user asks to run the migrated source command `review`.\n\n## Command Template\n\nReview the current change.\n"
);
}
#[tokio::test]