From 8f3f6ea67a9ae0f7447735f294917acd1ebd3b66 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 21 Jun 2026 06:56:12 +0000 Subject: [PATCH] =?UTF-8?q?stage=20#4593=20onto=20master=20(v0.51.550)=20?= =?UTF-8?q?=E2=80=94=20document=20extension=20sidecar=20CSP=20support=20+?= =?UTF-8?q?=20CSP=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit santastabber: docs (README + EXTENSIONS.md) clarifying extension sidecar CSP behavior + test_issue1909_csp_enforcement / test_issue2901_csp_connect_extra assertions. Docs+tests only, no prod code. Co-authored-by: santastabber --- README.md | 2 +- docs/EXTENSIONS.md | 39 +++++++++++++++++++++++ tests/test_issue1909_csp_enforcement.py | 14 ++++++++ tests/test_issue2901_csp_connect_extra.py | 20 ++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bcd7bde..f18f33dd 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ Full list of environment variables: | `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace | | `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default | | `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication | -| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the report-only CSP `connect-src` directive for reverse-proxy or tunnel deployments | +| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the enforced and report-only CSP `connect-src` directives for trusted reverse-proxy, tunnel, or extension sidecar deployments | | `HERMES_WEBUI_SSE_CHUNKED` | *(unset)* | Set truthy (`1`/`true`/`yes`/`on`) to send SSE with `Transfer-Encoding: chunked`. Needed behind buffering reverse proxies (e.g. `jupyter-server-proxy`) that otherwise buffer the whole stream; harmless but unnecessary for directly-served deployments | | `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled | | `HERMES_WEBUI_EXTENSION_MANIFEST` | *(unset)* | Optional relative JSON manifest inside `HERMES_WEBUI_EXTENSION_DIR` listing bundled scripts/styles to inject; see [WebUI Extensions](docs/EXTENSIONS.md) | diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md index 2594bbf1..4e6ad2c7 100644 --- a/docs/EXTENSIONS.md +++ b/docs/EXTENSIONS.md @@ -25,12 +25,15 @@ Extensions can: - inject configured same-origin scripts before `` - read a local JSON manifest that lists bundled scripts/styles to inject - call the normal WebUI APIs available to the browser session +- call trusted local loopback sidecars directly from extension JavaScript when + the browser Content Security Policy allows that origin Extensions cannot, by themselves: - bypass WebUI authentication - serve files outside the configured extension directory - load third-party scripts/styles through the built-in injection config +- register new WebUI backend routes or proxy arbitrary sidecar/backend traffic - change Hermes Agent permissions, models, memory, or tools unless they call existing authenticated APIs that already allow those changes @@ -125,6 +128,42 @@ These restrictions keep the existing Content Security Policy intact and avoid turning the extension hook into a third-party script loader. Invalid configured URLs are ignored rather than injected. +## Trusted local sidecars + +Manifest-bundled extensions may integrate with a trusted local sidecar process, +such as a desktop companion listening on `http://127.0.0.1:17787`. The injected +extension JavaScript talks to that sidecar directly from the browser; Hermes +WebUI does not proxy those requests and does not create extension-owned backend +routes. + +Loopback sidecar origins are already included in WebUI's enforced CSP +`connect-src` directive: + +```text +http://127.0.0.1:* +http://localhost:* +http://ipc.localhost +ws://127.0.0.1:* +ws://localhost:* +``` + +The wildcard ports above cover any loopback port, including +`http://127.0.0.1:17787`. For a trusted non-loopback origin that you explicitly +control, append the exact origin with `HERMES_WEBUI_CSP_CONNECT_EXTRA` before +starting WebUI: + +```bash +HERMES_WEBUI_CSP_CONNECT_EXTRA=https://companion.example.internal \ +HERMES_WEBUI_EXTENSION_DIR=/path/to/my-extension/static \ +HERMES_WEBUI_EXTENSION_MANIFEST=extensions.json \ +./start.sh +``` + +`HERMES_WEBUI_CSP_CONNECT_EXTRA` accepts space-separated `http(s)://` or +`ws(s)://` origins only. It rejects paths, directive injection, and invalid port +numbers. Avoid wildcard or remote origins unless you fully control the target; +extension JavaScript runs with the logged-in WebUI session's authority. + ## Static file serving When `HERMES_WEBUI_EXTENSION_DIR` points at an existing directory, files under diff --git a/tests/test_issue1909_csp_enforcement.py b/tests/test_issue1909_csp_enforcement.py index 70976ed9..4eb8ad19 100644 --- a/tests/test_issue1909_csp_enforcement.py +++ b/tests/test_issue1909_csp_enforcement.py @@ -69,6 +69,20 @@ def test_enforcing_csp_honors_valid_extra_connect_origins(monkeypatch): ) in policy +def test_enforcing_csp_allows_trusted_loopback_sidecars_by_default(monkeypatch): + monkeypatch.delenv("HERMES_WEBUI_CSP_CONNECT_EXTRA", raising=False) + + policy = _headers_from_security_helper()["Content-Security-Policy"] + + assert "connect-src" in policy + assert "http://127.0.0.1:*" in policy + assert "http://localhost:*" in policy + assert "http://ipc.localhost" in policy + assert "ws://127.0.0.1:*" in policy + assert "ws://localhost:*" in policy + assert "http://127.0.0.1:17787" not in policy + + def test_enforcing_and_report_only_csp_share_validated_connect_extra(monkeypatch): monkeypatch.setenv("HERMES_WEBUI_CSP_CONNECT_EXTRA", "https://metrics.example.com") diff --git a/tests/test_issue2901_csp_connect_extra.py b/tests/test_issue2901_csp_connect_extra.py index 7a441ce5..0039c29a 100644 --- a/tests/test_issue2901_csp_connect_extra.py +++ b/tests/test_issue2901_csp_connect_extra.py @@ -36,6 +36,26 @@ def test_csp_connect_src_includes_valid_extra_origins(monkeypatch): ) in policy +def test_csp_connect_src_includes_explicit_trusted_sidecar_origin(monkeypatch): + from server import Handler + + monkeypatch.setenv( + "HERMES_WEBUI_CSP_CONNECT_EXTRA", + "http://127.0.0.1:17787 ws://127.0.0.1:17787", + ) + + report_only = Handler.csp_report_only_policy() + + assert "http://127.0.0.1:17787" in report_only + assert "ws://127.0.0.1:17787" in report_only + + from api.helpers import _build_csp_enforced_policy + + enforced = _build_csp_enforced_policy() + assert "http://127.0.0.1:17787" in enforced + assert "ws://127.0.0.1:17787" in enforced + + def test_csp_connect_src_rejects_directive_injection(monkeypatch, caplog): from server import Handler