fix(plugins): one unreadable plugin child no longer aborts iter_plugin_dirs or memory-provider discovery
iter_plugin_dirs stat'd <child>/__init__.py inside every plugin dir, so a single mode-000 / ACL-denied $HERMES_HOME/plugins/<x> still raised PermissionError out of the loader, memory-provider discovery (dashboard memory settings, hermes memory setup, plugins memory picker) and the user cron-provider scan. Catch OSError per child and log the same 'Skipping unreadable plugin directory' warning the list path already emits. Part of #111804
This commit is contained in:
@@ -219,3 +219,27 @@ def test_activation_is_not_gated_on_plugins_enabled(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
assert memory_plugins.load_memory_provider("gatedmem") is not None
|
||||
|
||||
|
||||
def test_unreadable_user_plugin_does_not_abort_memory_discovery(tmp_path, monkeypatch):
|
||||
"""One mode-000 / ACL-denied ``$HERMES_HOME/plugins/<x>`` must not hide the bundled
|
||||
providers or its readable siblings from the dashboard / ``hermes memory`` pickers (#111804)."""
|
||||
plugins_root = tmp_path / "plugins"
|
||||
_write_provider_dir(plugins_root, "goodmem")
|
||||
denied = plugins_root / "denied"
|
||||
denied.mkdir()
|
||||
(denied / "__init__.py").write_text("", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
real_stat = Path.stat # chmod 000 does not bite as root; fail the child's stat instead
|
||||
|
||||
def stat(self, *args, **kwargs):
|
||||
if self.parent == denied:
|
||||
raise PermissionError(13, "Permission denied", str(self))
|
||||
return real_stat(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(Path, "stat", stat)
|
||||
|
||||
names = memory_plugins.list_memory_provider_names()
|
||||
assert "goodmem" in names
|
||||
assert "denied" not in names
|
||||
assert memory_plugins.find_provider_dir("denied") is None
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""One mode-000 / ACL-denied child under a plugin root must not abort the listing (#111804)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from plugins import plugin_loader
|
||||
|
||||
|
||||
def _deny(monkeypatch, denied: Path) -> None:
|
||||
"""chmod 000 does not bite as root, so fail the stat of the denied child's ``__init__.py``."""
|
||||
real_stat = Path.stat
|
||||
|
||||
def stat(self, *args, **kwargs):
|
||||
if self.parent == denied:
|
||||
raise PermissionError(13, "Permission denied", str(self))
|
||||
return real_stat(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(Path, "stat", stat)
|
||||
|
||||
|
||||
def test_iter_plugin_dirs_skips_unreadable_child(tmp_path, monkeypatch, caplog):
|
||||
for name in ("denied", "good"):
|
||||
(tmp_path / name).mkdir()
|
||||
(tmp_path / name / "__init__.py").write_text("", encoding="utf-8")
|
||||
_deny(monkeypatch, tmp_path / "denied")
|
||||
|
||||
with caplog.at_level("WARNING", logger="plugins.plugin_loader"):
|
||||
assert plugin_loader.iter_plugin_dirs(tmp_path) == [tmp_path / "good"]
|
||||
assert "Skipping unreadable plugin directory" in caplog.text
|
||||
Reference in New Issue
Block a user