# NousResearch/hermes-agent · Dashboard WebSocket fixes for reverse-proxy and insecure modes

## Open now

- 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.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #35141. Verify it addresses your case.
- **Recommended:** read PR #35141 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-35141`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #34227: v0.14.0 dashboard breaks behind reverse proxies — two regressions

## Prior & competing PRs

- **#34307: fix(dashboard): restore reverse-proxy WS access and action-status polling in --insecure mode**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#35141: fix(dashboard): allow chat websockets on insecure public bind (salvage #33278)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/web_server.py`, `scripts/release.py`, `tests/hermes_cli/test_dashboard_auth_ws_auth.py`
  - fix hunks:
  - `hermes_cli/web_server.py`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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
    ```
- **#34370: fix(dashboard): WS auth opt-in for reverse-proxy deployments (#34227)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#34307`, `#34227`, `#34370`

## Fix status

- Merged to main in PR #35141 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-30`.
- Subsystem: `dashboard`
