Skip to content

[Bug]: Dashboard plugin pins the relay host to 127.0.0.1, so every panel 502s when the relay isn't on the dashboard's loopback #550

Open

Description

@FiveBoroughs

Affected area

Dashboard plugin

What happened?

Observed: with the relay healthy and reachable, every panel in the Hermes-Relay dashboard tab (Service status, Paired devices, Last Bridge event, Recent Bridge activity) renders 502 {"detail":"relay unreachable at 127.0.0.1:8767: All connection attempts failed"}. Pairing, the WebSocket, and the Android clients work normally. Only the dashboard tab is affected.

Expected: the dashboard backend honors an env override for the relay host, like the rest of the plugin does.

dashboard/plugin_api.py parameterizes the port but hardcodes the host:

RELAY_PORT: int = int(os.environ.get("HERMES_RELAY_PORT", "8767"))
_RELAY_BASE: str = f"http://127.0.0.1:{RELAY_PORT}"

There's no HERMES_RELAY_HOST, so no configuration can redirect it. This is inconsistent with pair.py's read_relay_config(), which already honors RELAY_HOST/RELAY_PORT. The convention exists; the dashboard backend doesn't follow it.

Functional occurrences: line 90 (_RELAY_BASE), 453 (ws:// proxy target), 586 (error detail). Line 937's 127.0.0.1 is an unrelated API-host fallback and should stay. mobile_plugin_api.py and git_api.py have none.

Reproduction steps

  1. Run the relay server so it is not on the same loopback as the Hermes dashboard process (separate container, separate host, or any remote address).
  2. Confirm the relay is healthy: curl http://192.0.2.10:8767/health returns 200.
  3. Confirm the dashboard process reaches it on that address but not on loopback.
  4. Open the dashboard, then the Hermes-Relay tab.
  5. Every panel shows 502 relay unreachable at 127.0.0.1:8767.

Environment

  • Hermes-Relay version/tag: plugin 1.11.1
  • Install surface: plugin
  • hermes-agent version or commit: 0.21.0 (v2026.8.31)
  • Connection mode: LAN

Sanitized logs, screenshots, or traces

# from the process that runs the dashboard
$ curl -o /dev/null -w '%{http_code}' http://127.0.0.1:8767/health
000
$ curl -o /dev/null -w '%{http_code}' http://192.0.2.10:8767/health
200

# dashboard tab
502: {"detail":"relay unreachable at 127.0.0.1:8767: All connection attempts failed"}

Upstream or standard-path notes

Not an upstream hermes-agent issue. The relay dashboard plugin is relay-only. Standard chat, Manage, and dashboard voice are unaffected; the gateway and API server behave normally.

Suggested fix

Default preserves current behavior:

RELAY_HOST: str = os.environ.get("HERMES_RELAY_HOST", "127.0.0.1")
_RELAY_BASE: str = f"http://{RELAY_HOST}:{RELAY_PORT}"

plus the ws:// target at 453 and the error string at 586.

Activity

  1. hermes-relay-triage commented on Sep 7, 2026

    @hermes-relay-triage

    🤖 Hermes-Relay automated triage

    Thanks for the detailed split-host reproduction! This is confirmed in both the public Plugin v1.11.1 release and current dev: the dashboard backend fixes its HTTP and WebSocket relay hops to 127.0.0.1, and its tests explicitly enforce that loopback destination. The documented trust model also relies on the co-hosted hop for several management routes that allow loopback access without a paired-device bearer; RELAY_HOST configures the relay listener/advertised endpoint, not this dashboard upstream.

    That makes the 502 expected for a split-host or split-network-namespace deployment under the current design, while support for that topology is a valid enhancement. A hostname substitution alone would move these calls outside the loopback trust boundary and leave protected routes unauthorized, so this needs an authenticated proxy design rather than only an environment override. A maintainer will follow up to confirm whether split-host dashboard-to-relay operation should be supported and which authenticated transport contract should cover it.

  2. FiveBoroughs commented on Sep 7, 2026

    @FiveBoroughs
    Author

    Checked this against the tree. The triage is right, and I'm withdrawing the env-override suggestion from the description.

    plugin/relay/server.py:130 describes the trusted loopback hop, _require_loopback() at 1058 gates /pairing/register, /pairing/approve and /pairing/mint, and plugin/dashboard/test_plugin_api.py asserts the outbound host directly: assertEqual(request.url.host, "127.0.0.1") at 244 and 566, plus the error-detail assertion at 695. A host override would fail those tests and turn the gated routes into 403s instead of connection-refused. I hit that already: /relay/security returns 403: loopback only from a non-loopback peer.

    The gates behind the failing panels are not uniform, which might narrow the design question:

    • Service status uses /health: no auth gate
    • Paired devices uses /sessions (handle_sessions_list): _require_bearer_session
    • Recent Bridge activity uses /bridge/activity: _require_loopback

    Only the Bridge panels depend on loopback trust. /health is open and /sessions already has a non-loopback answer in the paired-device bearer. If the status and devices surface accepted a bearer the way a paired client does, that part works without a new transport contract, and the Bridge surface stays behind whatever authenticated proxy design lands.

    Not arguing the topology has to be supported. If it should not be, the useful outcome is a docs line saying the dashboard plugin requires a co-hosted relay, since today it surfaces as a generic 502 with nothing pointing at the topology.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions