feat: Handle alternate plugin manifest paths (#18182)

Load plugin manifests through a shared discoverable-path helper so
manifest reads, installs, and skill names all see the same alternate
manifest location.
This commit is contained in:
xl-openai
2026-04-16 19:43:19 -07:00
committed by GitHub
parent a803790a10
commit 37161bc76e
13 changed files with 581 additions and 222 deletions

View File

@@ -5,5 +5,5 @@ pub mod mcp_connector;
pub mod mention_syntax;
pub mod plugin_namespace;
pub use plugin_namespace::PLUGIN_MANIFEST_PATH;
pub use plugin_namespace::find_plugin_manifest_path;
pub use plugin_namespace::plugin_namespace_for_skill_path;

View File

@@ -2,9 +2,18 @@
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use std::path::PathBuf;
/// Relative path from a plugin root to its manifest file.
pub const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json";
const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] =
&[".codex-plugin/plugin.json", ".claude-plugin/plugin.json"];
pub fn find_plugin_manifest_path(plugin_root: &Path) -> Option<PathBuf> {
DISCOVERABLE_PLUGIN_MANIFEST_PATHS
.iter()
.map(|relative_path| plugin_root.join(relative_path))
.find(|manifest_path| manifest_path.is_file())
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -17,11 +26,18 @@ async fn plugin_manifest_name(
fs: &dyn ExecutorFileSystem,
plugin_root: &AbsolutePathBuf,
) -> Option<String> {
let manifest_path = plugin_root.join(PLUGIN_MANIFEST_PATH);
match fs.get_metadata(&manifest_path, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_file => {}
Ok(_) | Err(_) => return None,
let mut manifest_path = None;
for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS {
let candidate = plugin_root.join(relative_path);
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_file => {
manifest_path = Some(candidate);
break;
}
Ok(_) | Err(_) => {}
}
}
let manifest_path = manifest_path?;
let contents = fs
.read_file_text(&manifest_path, /*sandbox*/ None)
.await
@@ -53,12 +69,15 @@ pub async fn plugin_namespace_for_skill_path(
#[cfg(test)]
mod tests {
use super::find_plugin_manifest_path;
use super::plugin_namespace_for_skill_path;
use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::test_support::PathBufExt;
use std::fs;
use tempfile::tempdir;
const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
#[tokio::test]
async fn uses_manifest_name() {
let tmp = tempdir().expect("tempdir");
@@ -79,4 +98,24 @@ mod tests {
Some("sample".to_string())
);
}
#[tokio::test]
async fn uses_name_from_alternate_discoverable_manifest_path() {
let tmp = tempdir().expect("tempdir");
let plugin_root = tmp.path().join("plugins/sample");
let skill_path = plugin_root.join("skills/search/SKILL.md");
let manifest_path = plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH);
fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir");
fs::create_dir_all(manifest_path.parent().expect("manifest parent"))
.expect("mkdir manifest");
fs::write(&manifest_path, r#"{"name":"sample"}"#).expect("write manifest");
fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill");
assert_eq!(
plugin_namespace_for_skill_path(LOCAL_FS.as_ref(), &skill_path.abs()).await,
Some("sample".to_string())
);
assert_eq!(find_plugin_manifest_path(&plugin_root), Some(manifest_path));
}
}