{"record":{"id":"8da1cf1b2fa62915","repo":"NousResearch/hermes-agent","slug":"session-token-not-available-page-must-be-served","errorCode":null,"errorMessage":"Session token not available — page must be served by the Hermes dashboard server","messagePattern":"Session token not available — page must be served by the Hermes dashboard server","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"web/src/lib/gatewayClient.ts","lineNumber":50,"sourceCode":"      closedErrorMessage: \"WebSocket closed\",\n      connectErrorMessage: \"WebSocket connection failed\",\n      notConnectedErrorMessage: \"gateway not connected\",\n      onSocketClose: (event) => maybeReloadForLoopbackWsAuthFailure(event.code),\n      requestIdPrefix: \"w\",\n    });\n  }\n\n  async connect(token?: string): Promise<void> {\n    if (this.connectionState === \"open\" || this.connectionState === \"connecting\") {\n      return;\n    }\n\n    // Gated mode: legacy ``?token=`` is rejected by ``_ws_auth_ok``; the SPA\n    // must fetch a single-use ticket. Explicit ``token`` keeps the test-only\n    // override path.\n    const authParam = token ? ([\"token\", token] as const) : await buildWsAuthParam();\n    if (!authParam[1]) {\n      throw new Error(\n        \"Session token not available — page must be served by the Hermes dashboard server\",\n      );\n    }\n\n    await super.connect(\n      buildHermesWebSocketUrl({\n        authParam,\n        basePath: HERMES_BASE_PATH,\n        path: \"/api/ws\",\n      }),\n    );\n  }\n}\n","sourceCodeStart":32,"sourceCodeEnd":64,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/web/src/lib/gatewayClient.ts#L32-L64","documentation":"gatewayClient.connect() needs an auth query parameter for the WebSocket upgrade. It builds one via buildWsAuthParam(): a single-use ticket in gated mode, or the injected session token in loopback mode. If that value is empty, the page was not served by (or initialized by) the Hermes dashboard server, so no credential exists to authenticate the WS handshake.","triggerScenarios":"Opening the built SPA from file://, a static host, or a different origin that never injected window.__HERMES_SESSION_TOKEN__ and has no session cookie; also when the injected token is an empty string in loopback mode.","commonSituations":"Developing the SPA with `vite dev` against a separately served backend without the token bootstrap; copying the web/dist folder to a CDN; or a race where connect() runs before the server-injected bootstrap script executed.","solutions":["Serve the SPA from the Hermes dashboard server itself (`hermes dashboard`) so it injects the session token / sets the auth cookie.","If developing against a separate frontend server, proxy the app through the dashboard backend or pass an explicit token to connect(token) (test-only path).","Ensure connect() is only called after page bootstrap scripts have run (window.__HERMES_SESSION_TOKEN__ is populated)."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const wsCredential =\n  window.__HERMES_AUTH_REQUIRED__ || window.__HERMES_SESSION_TOKEN__\n  ? await buildWsAuthParam()\n  : null\nif (!wsCredential?.[1]) {\n  showFatal('Dashboard must be opened via the Hermes dashboard server (hermes dashboard)')\n  return\n}","typeGuard":"function hasInjectedCredential(): boolean {\n  return typeof window.__HERMES_SESSION_TOKEN__ === 'string'\n    && window.__HERMES_SESSION_TOKEN__.length > 0\n}","tryCatchPattern":"try {\n  await client.connect()\n} catch (err) {\n  if (String(err).includes('Session token not available')) {\n    window.location.reload() // re-bootstrap from the dashboard server\n    return\n  }\n  throw err\n}","preventionTips":["Always serve the SPA from the dashboard backend so credentials are injected.","Delay connect() until bootstrap globals are present.","In dev, proxy through the backend or pass an explicit test token."],"tags":["websocket","auth","spa","bootstrap"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}