From e31ecc38153aa41a8aa8403a9ed623e11bd7448d Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Mon, 7 Sep 2026 00:08:15 +0800 Subject: [PATCH] fix(plugins): accept str skill paths in register_skill Plugin register() helpers commonly pass the SKILL.md location as a filesystem string (PluginManifest.path itself is stored as str), but register_skill() called path.exists() directly, so a valid string path aborted the whole plugin load with "'str' object has no attribute 'exists'" instead of registering. Coerce to Path up front so the registry entry and find_plugin_skill() keep their Path contract, and a missing location still fails with FileNotFoundError. Fixes #104404 --- hermes_cli/plugins.py | 4 ++++ tests/test_plugin_skills.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index e6c3956b87f7..ec1e62703c9d 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -984,6 +984,10 @@ def register_skill( f"plugin name '{self.manifest.name}' automatically).") if not name or not _NAMESPACE_RE.match(name): raise ValueError(f"Invalid skill name '{name}'. Must match [a-zA-Z0-9_-]+.") + # Plugin register() helpers commonly pass the SKILL.md location as str + # (PluginManifest.path is stored as str); the registry and find_plugin_skill() + # promise a Path downstream. + path = Path(path) if not path.exists(): raise FileNotFoundError(f"SKILL.md not found at {path}") namespace = self.manifest.skill_namespace or self.manifest.name diff --git a/tests/test_plugin_skills.py b/tests/test_plugin_skills.py index b2f49061fa3d..5a58b3ad6c3b 100644 --- a/tests/test_plugin_skills.py +++ b/tests/test_plugin_skills.py @@ -139,6 +139,26 @@ def test_rejects_missing_file(self, ctx, tmp_path): with pytest.raises(FileNotFoundError): ctx.register_skill("foo", tmp_path / "nonexistent.md") + def test_accepts_str_path(self, ctx, tmp_path): + # Plugin register() helpers commonly pass the SKILL.md location as str + # (#104404); this used to abort the whole plugin load with + # "'str' object has no attribute 'exists'" instead of registering. + from pathlib import Path + + skill_md = tmp_path / "skills" / "my-skill" / "SKILL.md" + skill_md.parent.mkdir(parents=True) + skill_md.write_text("---\nname: my-skill\n---\nContent.\n") + + ctx.register_skill("my-skill", str(skill_md), "A test skill") + + found = ctx._manager.find_plugin_skill("testplugin:my-skill") + assert found == skill_md + assert isinstance(found, Path) + + def test_missing_str_path_raises_filenotfound(self, ctx, tmp_path): + with pytest.raises(FileNotFoundError): + ctx.register_skill("foo", str(tmp_path / "nonexistent.md")) + def test_duplicate_qualified_name_is_rejected(self, ctx, tmp_path): ctx.manifest.portable = True first = tmp_path / "first" / "SKILL.md"