mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
The native-app (RFC 8252) login passed its unit tests but failed in real Electron runtime — the tests mocked the exact seams that were broken. Four runtime defects, each proven against a live gated gateway: 1. Lockfile drift: apps/desktop declared @assistant-ui/react + @assistant-ui/react-streamdown but package-lock.json didn't place them, so `npm ci` (CI + every fresh checkout) failed to install them → Vite "Failed to resolve @assistant-ui/react". Reconcile the lockfile. 2. Double JSON encoding: postJsonNoAuth pre-JSON.stringify'd the body before fetchJson (which stringifies again), so /auth/native/token received a JSON string, not an object → gateway 422 "Input should be a valid dictionary" → native login silently fell back to the embedded webview. 3. Cookie-only liveness gate: buildRemoteConnection (and the Settings connected indicator) treated "signed in" as "has OAuth cookie". The native flow stores a bearer and sets no cookie, so a completed native login looped the UI into needsOauthLogin. Accept native token OR cookie. 4. Cookie-only REST path: the hermes:api handler routed oauth-mode REST through the cookie partition only. A cookieless native session → 401 no_cookie on every API call. Prefer the native bearer (with transparent refresh), else cookie — mirroring mintGatewayWsTicket, which was already bearer-aware. The three decision points (2–4) are extracted into a pure native-auth-decisions.ts with regression tests, since the mocked flow tests could not catch them. Verified live: system-browser login → cookieless bearer → connected chat, no embedded webview.
64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
/**
|
|
* native-auth-decisions.ts
|
|
*
|
|
* Pure decision helpers extracted from main.ts for the RFC 8252 native-app
|
|
* auth flow. These encode three choices that were each the site of a real
|
|
* runtime bug — invisible to the mocked flow tests because the tests never
|
|
* exercised the real main.ts internals. Keeping them pure + unit-tested here
|
|
* prevents silent regressions:
|
|
*
|
|
* 1. resolveJsonBody — the token/refresh POST body must be the raw
|
|
* object (fetchJson owns JSON.stringify). Pre-stringifying double-encodes
|
|
* it into a JSON string, which the gateway's Pydantic model rejects with
|
|
* 422 "Input should be a valid dictionary".
|
|
*
|
|
* 2. oauthSessionIsLive — an OAuth gateway is "signed in" when EITHER a
|
|
* native bearer token OR a live cookie session exists. Gating on the
|
|
* cookie alone rejects a completed native login and loops the UI into
|
|
* "not signed in".
|
|
*
|
|
* 3. resolveOauthRestAuth — an oauth-mode REST call authenticates with the
|
|
* native bearer when present, else the cookie partition. Cookie-only
|
|
* routing returns 401 no_cookie for a cookieless native session.
|
|
*
|
|
* All three are trivial once named; the value is the test that pins the
|
|
* contract so the god-file call sites can't drift back to the buggy shape.
|
|
*/
|
|
|
|
/**
|
|
* Decide the request body to hand to fetchJson (which JSON.stringifies it).
|
|
* Returns the object UNCHANGED — callers must NOT pre-stringify. A string here
|
|
* would be double-encoded downstream; this function exists to document and
|
|
* pin that contract at the one seam that got it wrong.
|
|
*/
|
|
export function resolveJsonBody<T>(body: T): T {
|
|
return body
|
|
}
|
|
|
|
/**
|
|
* True when an oauth gateway should be treated as signed-in. `hasNativeToken`
|
|
* is whether a native bearer token is stored; `hasCookieSession` is whether a
|
|
* live AT-or-RT cookie exists in the OAuth partition. Either suffices.
|
|
*/
|
|
export function oauthSessionIsLive(hasNativeToken: boolean, hasCookieSession: boolean): boolean {
|
|
return hasNativeToken || hasCookieSession
|
|
}
|
|
|
|
export type OauthRestAuth =
|
|
| { kind: 'bearer'; token: string }
|
|
| { kind: 'cookie' }
|
|
|
|
/**
|
|
* Decide how an oauth-mode REST request authenticates: prefer the native
|
|
* bearer (cookieless RFC 8252 flow) when a non-empty access token is present,
|
|
* otherwise fall back to the cookie partition. `nativeAccessToken` is the
|
|
* result of ensureNativeAccessToken (null/empty when there is no native
|
|
* session or the refresh terminally failed).
|
|
*/
|
|
export function resolveOauthRestAuth(nativeAccessToken: string | null | undefined): OauthRestAuth {
|
|
if (nativeAccessToken) {
|
|
return { kind: 'bearer', token: nativeAccessToken }
|
|
}
|
|
|
|
return { kind: 'cookie' }
|
|
}
|