stage #4593 onto master (v0.51.550) — document extension sidecar CSP support + CSP test coverage

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 <santastabber@users.noreply.github.com>
This commit is contained in:
nesquena-hermes authored and nesquena-hermes committed 2026-06-21 07:04:31 +00:00
1 parent cf16ad4bb5
commit 8f3f6ea67a
4 files changed
+74 -1

No files matched your search

+1 -1
View File
@@ -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) |
+39
View File
@@ -25,12 +25,15 @@ Extensions can:
- inject configured same-origin scripts before `</body>`
- 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
+14
View File
@@ -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")
+20
View File
@@ -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