Handle invalid plugin skills manifest field (#25717)

## Summary
- Treat invalid `plugin.json` `skills` shapes as a field-level warning
instead of rejecting the whole manifest
- Keep valid string path behavior unchanged and continue falling back to
the default `skills/` root
- Add regression coverage for array-shaped `skills`

## Tests
- `just fmt`
- `cargo test -p codex-core-plugins`
This commit is contained in:
xli-oai
2026-06-01 17:19:34 -07:00
committed by GitHub
Unverified
parent 0b3a6f7185
commit 1ad0d7aa4b
2 changed files with 63 additions and 2 deletions
@@ -959,6 +959,43 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
);
}
#[tokio::test]
async fn load_plugins_ignores_invalid_manifest_skills_shape() {
let codex_home = TempDir::new().unwrap();
let plugin_root = codex_home
.path()
.join("plugins/cache")
.join("test/sample/local");
write_file(
&plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "sample",
"skills": ["./custom-skills/"]
}"#,
);
write_file(
&plugin_root.join("skills/default-skill/SKILL.md"),
"---\nname: default-skill\ndescription: default skill\n---\n",
);
write_file(
&plugin_root.join("custom-skills/custom-skill/SKILL.md"),
"---\nname: custom-skill\ndescription: custom skill\n---\n",
);
let outcome = load_plugins_from_config(
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
codex_home.path(),
)
.await;
assert_eq!(outcome.plugins()[0].error, None);
assert_eq!(
outcome.plugins()[0].skill_roots,
vec![plugin_root.join("skills").abs()]
);
}
#[tokio::test]
async fn load_plugins_preserves_disabled_plugins_without_effective_contributions() {
let codex_home = TempDir::new().unwrap();
+26 -2
View File
@@ -23,7 +23,7 @@ struct RawPluginManifest {
// Keep manifest paths as raw strings so we can validate the required `./...` syntax before
// resolving them under the plugin root.
#[serde(default)]
skills: Option<String>,
skills: Option<RawPluginManifestPath>,
#[serde(default)]
mcp_servers: Option<String>,
#[serde(default)]
@@ -127,6 +127,13 @@ enum RawPluginManifestDefaultPromptEntry {
Invalid(JsonValue),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RawPluginManifestPath {
Path(String),
Invalid(JsonValue),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RawPluginManifestHooks {
@@ -238,7 +245,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
description,
keywords,
paths: PluginManifestPaths {
skills: resolve_manifest_path(plugin_root, "skills", skills.as_deref()),
skills: resolve_manifest_path_value(plugin_root, "skills", skills.as_ref()),
mcp_servers: resolve_manifest_path(
plugin_root,
"mcpServers",
@@ -394,6 +401,23 @@ fn json_value_type(value: &JsonValue) -> &'static str {
}
}
fn resolve_manifest_path_value(
plugin_root: &Path,
field: &'static str,
path: Option<&RawPluginManifestPath>,
) -> Option<AbsolutePathBuf> {
match path? {
RawPluginManifestPath::Path(path) => resolve_manifest_path(plugin_root, field, Some(path)),
RawPluginManifestPath::Invalid(value) => {
tracing::warn!(
"ignoring {field}: expected a string; found {}",
json_value_type(value)
);
None
}
}
}
fn resolve_manifest_path(
plugin_root: &Path,
field: &'static str,