fix(dashboard): skip unreadable plugin manifests

This commit is contained in:
KoNit-K
2026-09-15 18:14:36 +08:00
committed by Teknium
parent 0b48ae8d96
commit 9af62e7ef0
2 changed files with 57 additions and 5 deletions
+12 -5
View File
@@ -540,21 +540,28 @@ def _discover_dashboard_plugins() -> list:
plugins = []
seen_names: set = set()
for plugins_root, source in _dashboard_plugin_search_dirs():
if not plugins_root.is_dir():
try:
if not plugins_root.is_dir():
continue
with os.scandir(plugins_root) as scan:
children = sorted((Path(e.path) for e in scan), key=lambda p: p.name)
except OSError as exc:
_log.warning("Skipping unreadable dashboard plugin root %s: %s", plugins_root, exc)
continue
with os.scandir(plugins_root) as scan:
children = sorted((Path(e.path) for e in scan), key=lambda p: p.name)
for child in children:
manifest_file = child / "dashboard" / "manifest.json"
if not child.is_dir() or not manifest_file.exists():
continue
try:
if not child.is_dir() or not manifest_file.exists():
continue
data = json.loads(manifest_file.read_text(encoding="utf-8"))
name = data.get("name", child.name)
if name in seen_names:
continue
seen_names.add(name)
plugins.append(_dashboard_plugin_entry(data, name, child / "dashboard", source))
except OSError as exc:
_log.warning("Skipping unreadable dashboard plugin %s: %s", manifest_file, exc)
continue
except Exception as exc:
_log.warning("Bad dashboard plugin manifest %s: %s", manifest_file, exc)
continue
+45
View File
@@ -4540,6 +4540,51 @@ class TestDashboardPluginManifestExtensions:
assert len(entries) == 1
assert entries[0]["tab"]["path"] == "/from-profile"
def test_unreadable_plugin_paths_do_not_block_discovery(self, tmp_path, monkeypatch, caplog):
"""A denied plugin directory or manifest must not prevent valid plugins loading."""
from pathlib import Path
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
self._write_plugin(tmp_path, "valid", {
"name": "valid",
"label": "Valid Plugin",
"entry": "dist/index.js",
})
denied_root = tmp_path / "denied-root"
denied_root.mkdir()
denied_plugin = tmp_path / "plugins" / "denied"
(denied_plugin / "dashboard").mkdir(parents=True)
(denied_plugin / "dashboard" / "manifest.json").write_text("{}", encoding="utf-8")
from hermes_cli import web_server_dashboard
original_search_dirs = web_server_dashboard._dashboard_plugin_search_dirs
original_scandir = web_server_dashboard.os.scandir
original_exists = Path.exists
def search_dirs():
return [(denied_root, "user"), *original_search_dirs()]
def guarded_scandir(path):
if Path(path) == denied_root:
raise PermissionError("[WinError 5] Access is denied")
return original_scandir(path)
def guarded_exists(path):
if path == denied_plugin / "dashboard" / "manifest.json":
raise PermissionError("[WinError 5] Access is denied")
return original_exists(path)
monkeypatch.setattr(web_server_dashboard, "_dashboard_plugin_search_dirs", search_dirs)
monkeypatch.setattr(web_server_dashboard.os, "scandir", guarded_scandir)
monkeypatch.setattr(Path, "exists", guarded_exists)
plugins = web_server_dashboard._discover_dashboard_plugins()
assert "valid" in {plugin["name"] for plugin in plugins}
assert "denied" not in {plugin["name"] for plugin in plugins}
assert "Skipping unreadable dashboard plugin root" in caplog.text
assert "Skipping unreadable dashboard plugin" in caplog.text