Merge origin/main into bb/gui

This commit is contained in:
Brooklyn Nicholson
2026-05-27 21:22:14 -05:00
918 changed files with 153852 additions and 7510 deletions
+75 -6
View File
@@ -520,12 +520,6 @@ class TestBuildSchemaFromConfig:
assert entry["type"] == "select"
assert "options" in entry
assert "local" in entry["options"]
assert "vercel_sandbox" in entry["options"]
runtime_entry = CONFIG_SCHEMA["terminal.vercel_runtime"]
assert runtime_entry["type"] == "select"
assert "node24" in runtime_entry["options"]
assert "python3.13" in runtime_entry["options"]
assert len(runtime_entry["options"]) >= 3
def test_empty_prefix_produces_correct_keys(self):
from hermes_cli.web_server import _build_schema_from_config
@@ -2538,3 +2532,78 @@ def test_resolve_chat_argv_injects_gateway_ws_url(monkeypatch):
gateway_url = env.get("HERMES_TUI_GATEWAY_URL", "")
assert gateway_url.startswith("ws://127.0.0.1:9119/api/ws?")
assert "token=" in gateway_url
class TestDashboardPluginStaticAssetAllowlist:
"""``/dashboard-plugins/<name>/<path>`` is unauthenticated by design —
the SPA loads plugin JS via ``<script src>`` and CSS via
``<link href>``, neither of which can attach a custom auth header.
Instead the route restricts file types to the browser-asset
allowlist (JS/CSS/JSON/images/fonts) so that user-installed
plugins shipping a ``plugin_api.py`` backend module don't leak
their Python source to anyone reachable on the loopback port.
Regression test for the dashboard pentest finding filed alongside
the ``web-pentest`` skill (PR #32265 / issue #32267).
"""
@pytest.fixture(autouse=True)
def _setup_test_client(self, monkeypatch, _isolate_hermes_home):
try:
from starlette.testclient import TestClient
except ImportError:
pytest.skip("fastapi/starlette not installed")
from hermes_cli.web_server import app
self.client = TestClient(app)
def test_python_source_is_404(self):
"""The example plugin's ``plugin_api.py`` must NOT be served as
a static asset, even though the file exists under the plugin's
dashboard directory. Suffix not in the allowlist → 404."""
resp = self.client.get("/dashboard-plugins/example/plugin_api.py")
assert resp.status_code == 404
def test_pycache_is_404(self):
"""Same protection for compiled Python (``.pyc``) inside the
plugin's ``__pycache__/``. Real plugins ship these as a
side-effect of running tests / dashboard once."""
# __pycache__ files are only generated after the api file has
# been imported once. Use the path the example plugin actually
# generates during the dashboard test boot.
resp = self.client.get(
"/dashboard-plugins/example/__pycache__/plugin_api.cpython-311.pyc"
)
# 404 either way (file may not exist on this CI Python version);
# what matters is we never get a 200 with the bytes.
assert resp.status_code == 404
def test_manifest_json_still_served(self):
"""JSON files remain browser-fetchable — manifests, localized
data, source maps, etc. all sit in this bucket."""
resp = self.client.get("/dashboard-plugins/example/manifest.json")
assert resp.status_code == 200
assert resp.headers["content-type"].startswith("application/json")
# And the body is actually the manifest, not the SPA fallback.
body = resp.json()
assert body.get("name") == "example"
def test_unknown_plugin_is_404(self):
"""Existing behaviour preserved: nonexistent plugin name → 404."""
resp = self.client.get(
"/dashboard-plugins/_definitely_not_a_plugin_/manifest.json"
)
assert resp.status_code == 404
def test_path_traversal_still_blocked(self):
"""The allowlist is on top of the existing ``.resolve()`` /
``is_relative_to()`` check — a ``.js`` named file at an
out-of-base path is still rejected as traversal, not served."""
resp = self.client.get(
"/dashboard-plugins/example/..%2Fplugin_api.py"
)
# 403 traversal-blocked OR 404 (depending on URL decode order)
# — never 200.
assert resp.status_code in (403, 404)