Skip to content

fix(desktop): make remote gateway 'Test remote' validate the live WebSocket - #39098

Closed
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/desktop-test-remote-validate-ws
Closed

fix(desktop): make remote gateway 'Test remote' validate the live WebSocket#39098
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/desktop-test-remote-validate-ws

Conversation

@xxxigm

@xxxigm xxxigm commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Context / report

Discord report (multiple users hitting the same thing):
https://discord.com/channels/1053877538025386074/1511993870957674587/1511993870957674587

Users connecting the Desktop app to a remote Hermes backend see:

  • boot logs print Remote Hermes backend is ready
  • Test remote reports success (green / "reachable")
  • …but the app still fails with the opaque overlay "Could not connect to Hermes gateway"

Quotes from the thread:

  • "connection test is ok, but it still wont work… Windows side logs also show successful connection, but the desktop app still fails."
  • Nous team: "The test button only does a partial check, it doesn't validate the entire connection."

Root cause

Desktop boot touches a remote gateway over two independent paths:

  1. HTTP — the Electron main process calls GET /api/status (token in a header) to confirm the backend is up. This is what the boot logs print as Remote Hermes backend is ready, and what Test remote (testDesktopConnectionConfig) checked.
  2. WebSocket — the renderer then opens a live socket to /api/ws (credential in a query param) via gateway.connect(). The chat surface only works once this succeeds; on failure it throws connectErrorMessage = "Could not connect to Hermes gateway" (apps/shared/src/json-rpc-gateway.ts).

These two paths use different processes, transports, and credentials, and the server applies extra guards to the WS upgrade that the HTTP status route never sees — see web_server.py:

  • _ws_host_origin_reason (Host/Origin guard, mirrored from HTTP middleware because FastAPI middleware doesn't run for WS routes)
  • _ws_auth_reason (ws-ticket / internal / legacy ?token= validation, with ?token= rejected outright once the OAuth gate is engaged)
  • _ws_client_reason (peer-IP guard)

So a remote gateway can pass /api/status yet reject the WebSocket (blocked by a reverse proxy that doesn't forward Upgrade, a firewall, or one of the WS guards). The old test only exercised path #1, giving a false-positive "reachable" while the real boot failed on path #2.

Fix

Make the connection test exercise the same transport the app actually uses.

After the /api/status check, testDesktopConnectionConfig now opens the gateway WebSocket and confirms the upgrade is accepted:

  • token / localws(s)://…/api/ws?token=…
  • OAuth → freshly minted single-use ?ticket=… (POST /api/auth/ws-ticket); skipped (no false failure) when there's no live session

The probe (electron/gateway-ws-probe.cjs) classifies the handshake:

  • opens and stays open (or receives a frame) → ok
  • error / close before open → fail (unreachable, proxy not forwarding Upgrade, …)
  • opens then closes within a short grace window → fail (upgrade accepted but credential rejected post-handshake)
  • never opens → fail (timeout)

On failure the test throws an actionable message instead of reporting success:

Reached the gateway over HTTP, but the live WebSocket (/api/ws) connection failed: . The HTTP check can pass while the WebSocket is blocked by a proxy, firewall, or gateway auth/origin guard.

The WebSocket implementation is injected (Electron's global WebSocket; skipped if a runtime lacks one, so no spurious failures on older builds).

Tests

electron/gateway-ws-probe.test.cjs (wired into test:desktop:platforms) drives a fake socket through every outcome: open-stays-open, frame, error-before-open, close-before-open, accept-then-early-close, timeout, constructor throw, and missing impl.

npm run test:desktop:platforms   # 57 passing

Test plan

  • npm run test:desktop:platforms green
  • Remote gateway behind a proxy that doesn't forward Upgrade: Test remote now fails with the WS message instead of a false "reachable"
  • Healthy remote (token & OAuth): Test remote still passes

@xxxigm
xxxigm requested a review from a team June 4, 2026 13:57
xxxigm added 2 commits June 4, 2026 21:02
Adds electron/gateway-ws-probe.cjs: a small helper that opens a gateway
WebSocket URL and classifies the handshake (open/frame → ok; error or close
before open → fail; open-then-early-close → credential rejected; never-opens →
timeout). The WebSocket implementation is injected so it can be unit-tested
without a real socket.

Wires gateway-ws-probe.test.cjs into test:desktop:platforms, covering every
handshake outcome plus constructor-throw and missing-impl.
The "Test remote" button only checked HTTP GET /api/status, but the chat
surface depends on the renderer opening a live WebSocket to /api/ws — a
separate transport with separate server-side guards (Host/Origin checks,
ws-ticket/token auth, peer-IP checks). A gateway could pass the HTTP check yet
reject the WebSocket, so the test reported "reachable" while boot still failed
with the opaque "Could not connect to Hermes gateway".

testDesktopConnectionConfig now mirrors the renderer's connect: after the
status check it opens the WS URL (token/local) or a freshly minted ws-ticket
(OAuth) and confirms the upgrade is accepted and not immediately torn down by
a post-handshake auth rejection. Failures surface an actionable message instead
of a false-positive. The WS leg is skipped when the runtime lacks a global
WebSocket so it never fails spuriously.
@xxxigm
xxxigm force-pushed the fix/desktop-test-remote-validate-ws branch from 0cdc64f to 286ccbe Compare June 4, 2026 14:02

@YoussefEttamimi YoussefEttamimi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening the remote gateway test path. I found a blocking false-positive in the OAuth branch.

resolveTestWsUrl() currently catches ticket-mint failures and returns null, and testDesktopConnectionConfig() only runs the live WS probe when a wsUrl exists. In token mode that is fine because a URL is always constructed, but in OAuth mode a stale/invalid session can fail mintGatewayWsTicket(baseUrl), return null, skip the WebSocket validation entirely, and still report the remote test as successful after GET /api/status passes.

That is exactly the class of problem this PR is meant to catch: HTTP status can succeed while the renderer cannot authenticate /api/ws. The normal connection flow treats failure to mint the WS ticket as an auth/session failure, so the test button should do the same (or at least surface an actionable ticket-mint error) instead of silently skipping the probe.

Suggested fix: distinguish “cannot test because this runtime has no WebSocket” from “OAuth ticket mint failed”. For OAuth, propagate/report the ticket-mint failure and add coverage for:

  • token mode + WS failure
  • OAuth ticket mint success + WS failure
  • OAuth ticket mint failure should fail the remote test, not skip WS validation

Checks I ran/verified during review:

  • node --test electron/gateway-ws-probe.test.cjs — passed
  • npm run test:desktop:platforms — passed
  • node --check electron/gateway-ws-probe.cjs electron/gateway-ws-probe.test.cjs — passed

The helper itself looks solid; the issue is in the integration path for OAuth ticket mint failures.

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery labels Jun 4, 2026
@teknium1

teknium1 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Merged via #39511. Your two commits were cherry-picked onto current main with your authorship preserved in the git log (10c78bf, 500cf53).

Thanks for the fix — the live WebSocket probe is exactly the right approach. The only addition was resolving @YoussefEttamimi's review: an OAuth ticket-mint failure now fails the test (with an actionable "sign in again" message) instead of silently skipping the probe, mirroring the real boot path. resolveTestWsUrl was extracted into the electron-free connection-config.cjs so all three of his cases are unit-tested.

62/62 desktop platform tests passing.

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants