Skip to content

Desktop: send the gateway's own Origin on gateway WebSocket upgrades - #65114

Open
Zeus-Deus wants to merge 1 commit into
NousResearch:mainfrom
Zeus-Deus:fix/desktop-gateway-ws-origin
Open

Desktop: send the gateway's own Origin on gateway WebSocket upgrades#65114
Zeus-Deus wants to merge 1 commit into
NousResearch:mainfrom
Zeus-Deus:fix/desktop-gateway-ws-origin

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Contributor

Problem

The dashboard applies a DNS-rebinding Host/Origin guard to WebSocket upgrades (_ws_host_origin_reason in web_server.py): when the handshake carries an Origin header, it must match the bound dashboard host, or the upgrade is refused with 403 before accept().

Chromium stamps the window's web origin on every WebSocket handshake and the page cannot override it. When the desktop app runs from a source checkout (npm run dev), the renderer is served by Vite at http://127.0.0.1:5174, so every gateway WS upgrade carries that as its Origin. Against a remote gateway bound to another host, the socket is closed pre-accept and the app surfaces the opaque "Could not connect to Hermes gateway" — while every REST call keeps working, because those route through the Electron main process, which sends no Origin at all. That combination (REST green, WS dead) makes this miserable to debug: the gateway auth log shows ws-tickets being minted, but no ws accepted line ever follows in tui_gateway logs.

I hit this connecting the desktop app on my laptop to the gateway on my home server over my tailnet. Isolating it down:

  • mint a ticket, attempt the WS upgrade with Origin: http://127.0.0.1:5174HTTP 403
  • same flow with no Origin header → 101 Switching Protocols, gateway.ready arrives

Packaged builds don't hit this because their window is file://, a non-web origin the server-side guard already exempts. So this only affects running from source — which is exactly how you iterate on desktop ↔ remote gateway setups.

Fix

The desktop shell owns its gateway connection, and the real auth boundary is the credential (the single-use ws-ticket minted over the authenticated cookie session). So the handshake now presents the gateway's own origin — the same trust statement the file:// exemption already makes for packaged builds:

  • electron/gateway-ws-origin.ts — pure, electron-free rewrite decision (same pattern as connection-config.ts): only ws:/wss: upgrades whose path ends in /api/ws, /api/pub, or /api/events get their Origin replaced with the target's own origin; everything else passes through untouched. Path prefixes (/hermes/api/ws) work.
  • main.ts wires it into session.defaultSession.webRequest.onBeforeSendHeaders, filtered to ws://*/* / wss://*/*.

Nothing changes for real browsers hitting the dashboard; the server-side guard stays fully meaningful for them.

Related: #62301 adds a server-side allowed-hosts opt-in whose allowlist also feeds the WS origin check, so it can work around this too — but a native client shouldn't need server configuration to talk to its own configured gateway. Both changes stand on their own.

Testing

  • npm run typecheck clean
  • 8 new unit tests in electron/gateway-ws-origin.test.ts; the full electron vitest project passes (422 tests)
  • Verified live: dev-mode desktop on my laptop ↔ hermes serve on my home server over Tailscale. Before the change the gateway logged ticket mints but never ws accepted; with it the upgrade is accepted and the app connects and streams normally.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) area/auth Authentication, OAuth, credential pools needs-decision Awaiting maintainer decision before any implementation labels Jul 15, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Desktop fix. The premise remains present on current main: hermes_cli/web_server.py:14717-14732 rejects a mismatched HTTP(S) Origin, while the source Desktop window loads DEV_SERVER at apps/desktop/electron/main.ts:7338-7341.

The proposed helper is appropriately narrow: it only rewrites ws:/wss: URLs ending in the known gateway endpoint paths, and derives the replacement Origin from that target URL. This preserves the server-side browser protection: current tests still reject cross-site HTTP(S) origins and allow same-host origins (tests/hermes_cli/test_dashboard_auth_ws_auth.py:536-545).

GitHub currently marks the branch conflicting; 75f45a069 reformatted the main.ts import block, so salvage should be a mechanical transplant of the import and startup registration alongside the two new helper/test files.

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 16, 2026
The dashboard's DNS-rebinding guard refuses WebSocket upgrades whose
Origin does not match the bound host, with 403 before accept(). Chromium
stamps the window's web origin on every WS handshake and the renderer
cannot override it, so a dev-mode renderer (Vite, 127.0.0.1:5174) can
never open the gateway WS against a remote host: REST keeps working (the
main process sends no Origin) while the WS dies pre-accept, surfacing as
the opaque "Could not connect to Hermes gateway".

Rewrite the Origin on gateway WS upgrades (/api/ws, /api/pub,
/api/events only) to the target's own origin in the main process. The
rewrite decision is electron-free and unit-tested; packaged builds were
already exempt server-side via their non-web file:// origin.
@Zeus-Deus
Zeus-Deus force-pushed the fix/desktop-gateway-ws-origin branch from 0540935 to 686e435 Compare July 19, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants