NousResearch/hermes-agent

Dashboard WebSocket fixes for reverse-proxy and insecure modes

Cluster 34227 · dashboard. Data as of

Nothing open on GitHub. Fix merged in #35141 on 30 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #35141. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #35141. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address regressions in dashboard WebSocket connectivity and action-status polling that affected reverse-proxy deployments and insecure public bind configurations. The work involves opt-in auth handling and restoring access broken around v0.14.0.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #35141 fix(dashboard): allow chat websockets on insecure public bind (salvage #33278)

    merged to main

    files: hermes_cli/web_server.py, scripts/release.py, tests/hermes_cli/test_dashboard_auth_ws_auth.py

    Show the diff
    • hermes_cli/web_server.py
      @@ -3371,10 +3371,15 @@ class PtyUnavailableError(RuntimeError):  # type: ignore[no-redef]
       def _ws_client_is_allowed(ws: "WebSocket") -> bool:
           """Check if the WebSocket client IP is acceptable.
       
      -    Loopback mode: only loopback clients allowed — the legacy
      +    Loopback bind: only loopback clients allowed — the legacy
           ``?token=[redacted:secret-assignment] path is the only auth we have, so we
           don't want LAN hosts guessing tokens.
       
      +    All-interfaces insecure bind (``--host 0.0.0.0 --insecure`` or
      +    ``--host :: --insecure``): allow any peer. The operator explicitly
      +    opted into LAN/public exposure in this mode, so the loopback-only peer
      +    restriction should not apply.
      +
           Gated mode: any peer is allowed — uvicorn's ``proxy_headers=True``
           (enabled when the OAuth gate is active so cookies can pick up
           ``X-Forwarded-Proto``) rewrites ``ws.client.host`` to the
      @@ -3385,6 +3390,9 @@ def _ws_client_is_allowed(ws: "WebSocket") -> bool:
           """
           if getattr(app.state, "auth_required", False):
               return True
      +    bound_host = getattr(app.state, "bound_host", "")
      +    if bound_host in {"0.0.0.0", "::"}:
      +        return True
           client_host = ws.client.host if ws.client else ""
           if not client_host:
               return True
    • scripts/release.py
      @@ -45,6 +45,7 @@
       
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
      +    "[redacted:email]": "SeaXen",
           "[redacted:email]": "HashClawAI",
           "[redacted:email]": "TonyPepeBear",
           "[redacted:email]": "nielskaspers",
    • tests/hermes_cli/test_dashboard_auth_ws_auth.py
      @@ -80,6 +80,25 @@ def loopback_app():
           web_server.app.state.auth_required = prev_required
       
       
      [redacted:email]
      +def insecure_public_app():
      +    """web_server.app configured for all-interfaces insecure mode."""
      +    _reset_for_tests()
      +    clear_providers()
      +    prev_host = getattr(web_server.app.state, "bound_host", None)
      +    prev_port = getattr(web_server.app.state, "bound_port", None)
      +    prev_required = getattr(web_server.app.state, "auth_required", None)
      +    web_server.app.state.bound_host = "0.0.0.0"
      +    web_server.app.state.bound_port = 9120
      +    web_server.app.state.auth_required = False
      +    client = TestClient(web_server.app, base_url="http://192.168.0.222:9120")
      +    yield client
      +    _reset_for_tests()
      +    web_server.app.state.bound_host = prev_host
      +    web_server.app.state.bound_port = prev_port
      +    web_server.app.state.auth_required = prev_required
      +
      +
       def _logged_in(client: TestClient) -> None:
           """Drive the stub OAuth round trip so the client holds session cookies."""
           r1 = client.get("/auth/login?provider=stub", follow_redirects=False)
      @@ -281,6 +300,21 @@ def test_loopback_peer_allowed_in_loopback_mode(self, loopback_app):
               ws.headers = {"host": "127.0.0.1:8080"}
               assert web_server._ws_request_is_allowed(ws) is True
       
      +    def test_non_loopback_peer_allowed_in_insecure_public_mode(self, insecure_public_app):
      +        """`--host 0.0.0.0 --insecure` is an explicit LAN/public opt-in.
      +
      +        Regression coverage for the dashboard `/chat` breakage where the
      +        HTML shell loaded on 9120 but every WebSocket upgrade was rejected
      +        with 403 because the loopback-only peer guard still ran even though
      +        the operator intentionally exposed the dashboard on all interfaces.
      +        """
      +        ws = _fake_ws(query={}, client_host="192.168.0.55")
      +        ws.headers = {
      +            "host": "192.168.0.222:9120",
      +            "origin": "http://192.168.0.222:9120",
      +        }
      +        assert web_server._ws_request_is_allowed(ws) is True
      +
           def test_host_origin_guard_still_runs_in_gated_mode(self, gated_app):
               """Bypassing the peer-IP check must not bypass the DNS-rebinding
               Host header guard — that one still protects against attacker

2 PRs closed without merging

From the close comments: done elsewhere 2

  • #34307 fix(dashboard): restore reverse-proxy WS access and action-status polling in --insecure mode · done elsewhere
  • #34370 fix(dashboard): WS auth opt-in for reverse-proxy deployments (#34227) · done elsewhere → #34307, #34227, #34370
Closed issues · 1
  • Closed#34227v0.14.0 dashboard breaks behind reverse proxies — two regressions
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
dashboard
Merged PRs
#35141
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
30 May 2026

For your agent

The same facts, typed.