fix(plugins): skip dunder dirs and contain plugin-scan OSErrors

Walking __pycache__ and other dunder children let an unreadable
directory raise out of plugin discovery and fail unrelated tool
calls. Skip those names and treat iterdir/is_dir OSError as an
empty scan for that node.

Fixes #86996
This commit is contained in:
Christopher
2026-08-15 15:23:25 +02:00
committed by Teknium
parent 9778f00970
commit f8dfa68fb3
2 changed files with 67 additions and 1 deletions

View File

@@ -109,7 +109,17 @@ def scan_directory(
manifests: List[PluginManifest] = []
if not path.is_dir():
return manifests
for child in sorted(path.iterdir()):
try:
children = sorted(path.iterdir())
except OSError as exc:
logger.warning("Failed to scan plugin directory %s: %s", path, exc)
return manifests
for child in children:
# Cache/dunder dirs (__pycache__, __MACOSX__, …) are never
# plugins. Walking them can raise PermissionError and take
# down every subsequent tool call (#86996).
if child.name.startswith("__") and child.name.endswith("__"):
continue
try:
if not child.is_dir() or (depth == 0 and skip_names and child.name in skip_names):
continue

View File

@@ -0,0 +1,56 @@
"""Plugin directory scans must skip dunder dirs and survive OSError (#86996)."""
from __future__ import annotations
from pathlib import Path
from hermes_cli.plugins import PluginManager
def test_scan_skips_dunder_dirs_and_still_loads_real_plugin(tmp_path: Path):
root = tmp_path / "plugins"
demo = root / "demo"
demo.mkdir(parents=True)
(demo / "plugin.yaml").write_text("name: demo\nversion: 0.1.0\ndescription: x\n")
(demo / "__pycache__").mkdir()
pycache = root / "__pycache__"
pycache.mkdir()
pycache.chmod(0o000)
try:
found = PluginManager()._scan_directory(root, "user")
finally:
pycache.chmod(0o700)
assert [manifest.name for manifest in found] == ["demo"]
def test_scan_survives_iterdir_oserror(tmp_path: Path, monkeypatch):
root = tmp_path / "plugins"
root.mkdir()
def _boom(_self):
raise PermissionError("unreadable plugin root")
monkeypatch.setattr(Path, "iterdir", _boom, raising=False)
found = PluginManager()._scan_directory(root, "user")
assert found == []
def test_scan_skips_child_whose_is_dir_raises(tmp_path: Path, monkeypatch):
root = tmp_path / "plugins"
demo = root / "demo"
demo.mkdir(parents=True)
(demo / "plugin.yaml").write_text("name: demo\nversion: 0.1.0\ndescription: x\n")
spooky = root / "spooky"
spooky.mkdir()
original_is_dir = Path.is_dir
def _is_dir(self):
if self.name == "spooky":
raise PermissionError("stat failed")
return original_is_dir(self)
monkeypatch.setattr(Path, "is_dir", _is_dir)
found = PluginManager()._scan_directory(root, "user")
assert [manifest.name for manifest in found] == ["demo"]