Skip to content

Fix remote gateway fallback caused by auth-less liveness and registry drift - #92274

Closed
Allecpu wants to merge 2 commits into
NousResearch:mainfrom
Allecpu:fix/windows-desktop-remote-auth-liveness
Closed

Allecpu wants to merge 2 commits into
NousResearch:mainfrom
Allecpu:fix/windows-desktop-remote-auth-liveness

Conversation

@Allecpu

@Allecpu Allecpu commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Problem

Hermes Desktop could initially connect to an authenticated remote gateway and load its sessions, then later drop the connection and fall back to This device.

A separate issue affected Save and reconnect: the remote backend could become reachable without the corresponding registered gateway becoming Current or Primary.

Root cause

Two independent issues contributed to the behavior.

1. Remote liveness probes dropped authentication

Primary and pooled remote liveness checks queried the status endpoint through an anonymous fetch path.

For authenticated self-hosted gateways, this could return an authorization failure even though the active OAuth session, native bearer token, or static token was valid.

Those failures accumulated in the liveness tracker until the cached remote connection was treated as stale and replaced.

2. Save and reconnect updated only the legacy connection config

The apply path wrote connection.json, while the Connections UI and active connection identity are resolved through the v2 connections.json registry.

Migration only runs when the registry does not yet exist. If the registry already contained only the local connection, applying a remote gateway left the two stores divergent.

The backend could therefore connect successfully without having a matching registry connectionId, preventing the remote connection from becoming Current or Primary.

Solution

  • Pass the resolved connection descriptor into primary and pooled liveness probes.
  • Run remote liveness probes through the existing authenticated backend fetch path, preserving OAuth cookies, native bearer tokens, static tokens, and configured headers.
  • Authenticate protected status checks used by connection testing.
  • Reconcile successfully applied global connections into the v2 registry.
  • Match remote gateways by normalized URL to preserve existing IDs and labels without creating duplicates.
  • Set the applied connection as Primary and last-used/current.
  • Keep local, remote, cloud, SSH, and profile-scoped behavior isolated.
  • Treat legacy config and registry persistence as one recoverable apply boundary, restoring both snapshots if persistence or activation fails.
  • Preflight authenticated REST and WebSocket connectivity before committing a global remote apply.

Tests

Added regression coverage for:

  • authenticated primary and pooled remote liveness probes;
  • applying a remote gateway to a local-only registry;
  • preserving an existing remote ID and label;
  • avoiding duplicate normalized URLs;
  • switching remote → local;
  • switching between two remotes;
  • rolling back both stores after activation or registry-write failure;
  • authenticated OAuth connection status checks;
  • preflight ordering before persistence.

Focused tests, ESLint, and TypeScript typechecking pass.

The full Desktop suite was also run on Windows. Remaining failures were unrelated existing platform/environment issues involving POSIX permission and SSH assumptions, locale-dependent formatting, jsdom canvas support, and UI timing.

Manual verification

A Windows x64 unpacked build was tested end-to-end against a real self-hosted Hermes Agent v0.20.5 exposed over public HTTPS with OAuth authentication.

Verified behavior:

  • Save and reconnect registers the remote gateway;
  • the remote becomes Current and Primary;
  • remote sessions load;
  • chat works;
  • the remote connection remains active;
  • Desktop no longer falls back automatically to This device.

No server-side changes are required, and no changes are required to Tailscale, Funnel, DNS, reverse proxy configuration, or other network infrastructure.

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 22, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Both root causes are plausibly the real story for "connected once, then silently fell back to This device": anonymous liveness probes against an authenticated gateway accumulate failures until the tracker condemns a healthy connection, and a reachable backend without registry promotion leaves Save-and-reconnect in limbo. The fix direction — fetchConnectionStatus resolves credentials per authMode (native bearer via ensureNativeAccessToken, OAuth session fetch, or static token) instead of a bare fetch — is correct: liveness must ask the same question with the same credentials as real traffic.

Two notes:

  1. hardening.test.ts regex-asserts helper source (assert.match(helper, /ensureNativeAccessToken\(baseUrl\)/) etc.). That pins the shape of the code rather than behavior — a rename or refactor breaks it while a genuine auth regression that keeps those substrings passes. The behavioral assertions elsewhere in remote-liveness.test.ts (mocked fetch receiving the bearer) are the ones doing the real work; consider deleting the source-matching ones or converting them to call-level checks.

  2. Failure-path asymmetry to pin: when token refresh fails (.catch(() => null)), the probe proceeds unauthenticated — which reproduces exactly the condemned-liveness failure this PR fixes. If intentional (a probe that can't authenticate should count as unhealthy), document it at the catch site; if not, an unauthenticated success should perhaps still count as alive-but-degraded so a transient refresh failure doesn't trigger fallback.

@OutThisLife

Copy link
Copy Markdown
Contributor

Superseded by #93408.

Both root causes here were right, and both are in the consolidated PR: reconcileAppliedGlobalConnection and the authenticated liveness probes are cherry-picked with your authorship, and you're credited as a co-author.

Two changes on top. The hardening.test.ts assertions that matched main.ts source text are replaced with behavior tests — preflight is now a real option on applyConnectionConfigAtomically, so its ordering is observable through the seam rather than inferred from source layout. And the OAuth fallthrough in fetchConnectionStatus is documented at the catch site: the cookie path is still an authenticated request, so a refresh failure is not a silent downgrade to an anonymous probe.

The residual gap this PR didn't cover is the one that keeps users stuck: reconciling on apply stops new drift, but an already-drifted registry never heals, so affected users still had to hand-edit connections.json. #93408 adds reconciliation on read for that, and folds in #93279 plus the other two symptoms of the same force-switch (#91564, #93197).

Thanks — the diagnosis did the hard part.

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

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants