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:
teknium1
2026-09-15 14:12:43 -07:00
committed by Teknium
parent b915405f92
commit e3a90079b4
4 changed files with 70 additions and 5 deletions
+4 -3
View File
@@ -62,12 +62,13 @@ def _get_project_plugins_dir() -> Optional[Path]:
def _is_memory_provider_dir(path: Path) -> bool:
"""Cheap text heuristic (no import): ``__init__.py`` mentions the memory provider contract."""
init_file = path / "__init__.py"
if not init_file.exists():
return False
try:
if not init_file.exists():
return False
source = init_file.read_text(errors="replace", encoding="utf-8")[:8192]
return "register_memory_provider" in source or "MemoryProvider" in source
except Exception:
except OSError as exc: # one mode-000 / ACL-denied child must not abort discovery
logger.warning("Skipping unreadable plugin directory %s: %s", path, exc)
return False
+12 -2
View File
@@ -13,6 +13,8 @@ import sys
from pathlib import Path
from typing import Any, Callable, List, Optional, Tuple
_log = logging.getLogger(__name__)
_PLUGINS_ROOT = Path(__file__).parent
@@ -39,8 +41,16 @@ def iter_plugin_dirs(root: Path) -> List[Path]:
"""Sorted child dirs of *root* that have an ``__init__.py`` (skips ``_``/``.`` names)."""
if not root.is_dir():
return []
return [child for child in sorted(root.iterdir())
if child.is_dir() and not child.name.startswith(("_", ".")) and (child / "__init__.py").exists()]
dirs: List[Path] = []
for child in sorted(root.iterdir()):
if child.name.startswith(("_", ".")):
continue
try:
if child.is_dir() and (child / "__init__.py").exists():
dirs.append(child)
except OSError as exc: # one mode-000 / ACL-denied child must not abort the listing
_log.warning("Skipping unreadable plugin directory %s: %s", child, exc)
return dirs
def read_plugin_description(plugin_dir: Path) -> str: