{"record":{"id":"98df971f7438bc60","repo":"NousResearch/hermes-agent","slug":"hermes-desktop-bridge-unavailable","errorCode":null,"errorMessage":"Hermes desktop bridge unavailable","messagePattern":"Hermes desktop bridge unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/desktop/src/hermes.ts","lineNumber":299,"sourceCode":"function pluginPathSuffix(caller: string, path: string): string {\n  const suffix = path.startsWith('/') ? path : `/${path}`\n\n  if (suffix.split(/[?#]/, 1)[0].split('/').includes('..')) {\n    throw new Error(`${caller}: illegal path traversal in \"${path}\"`)\n  }\n\n  return suffix\n}\n\n/** The plugin REST door. Every call is scoped BY CONSTRUCTION to the plugin's\n *  own backend namespace — `path` is relative to `/api/plugins/<pluginId>`\n *  ('/board' → `/api/plugins/kanban/board`), so a plugin can't address another\n *  plugin's API or a core route through it. Profile-aware like every desktop\n *  REST call. Broader reach (core endpoints, another namespace) is the future\n *  declared-capability seam; today the namespace IS the boundary. */\nexport async function pluginRest<T>(pluginId: string, path: string, opts: PluginRestOptions = {}): Promise<T> {\n  if (!window.hermesDesktop?.api) {\n    throw new Error('Hermes desktop bridge unavailable')\n  }\n\n  const suffix = pluginPathSuffix('pluginRest', path)\n\n  return window.hermesDesktop.api<T>({\n    path: `/api/plugins/${pluginId}${suffix}`,\n    method: opts.method,\n    body: opts.body,\n    upload: opts.upload,\n    timeoutMs: opts.timeoutMs,\n    ...profileScoped()\n  })\n}\n\n/** The plugin WebSocket door — the live twin of `pluginRest`, scoped the same\n *  way: `path` is relative to `/api/plugins/<pluginId>` ('/events' → the\n *  plugin's own event stream). Token-mode backends auth via the same query\n *  credential the app's own sockets use; OAuth remotes resolve null (callers","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/src/hermes.ts#L281-L317","documentation":"Thrown by pluginRest() in apps/desktop/src/hermes.ts:299 when `window.hermesDesktop.api` is not present — i.e. the code is not running inside the Hermes desktop shell (or the preload bridge has not been injected yet). All desktop REST traffic goes through the Electron preload bridge; in a plain browser tab, a web preview, or before bridge initialization there is no `api` function to route through.","triggerScenarios":"Calling pluginRest() from a component rendered in the web dashboard or a normal browser (no hermesDesktop preload); calling during app startup before the preload script exposes the bridge; running plugin code in a context where the sandboxed renderer did not receive contextBridge injection.","commonSituations":"A runtime plugin or shared module executed in both desktop and web builds; a unit test (jsdom) with no window.hermesDesktop stub; opening the renderer's index.html directly outside Electron; a corrupted or disabled preload bundle after an update.","solutions":["Guard the call: `if (!window.hermesDesktop?.api) { /* hide/disable the plugin REST feature */ }`.","In tests, stub `window.hermesDesktop = { api: vi.fn() }` before invoking plugin code.","If code must run in both environments, branch to the web app's HTTP client when the bridge is absent.","Verify you are actually inside the desktop shell (window.hermesDesktop defined) before mounting plugin surfaces that need REST."],"exampleFix":"// before\nconst board = await pluginRest('kanban', '/board')\n\n// after\nif (!window.hermesDesktop?.api) {\n  setUnavailable('Plugin API requires the Hermes desktop shell')\n  return\n}\nconst board = await pluginRest('kanban', '/board')","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function hasDesktopApi(w: Window): w is Window & { hermesDesktop: { api: <T>(r: unknown) => Promise<T> } } {\n  return typeof (w as any).hermesDesktop?.api === 'function'\n}","tryCatchPattern":"if (!hasDesktopApi(window)) { setFeatureUnavailable('requires the Hermes desktop shell'); return }\ntry { await pluginRest(id, '/board') } catch (e) { notifyError(e) }","preventionTips":["Check window.hermesDesktop?.api once at feature mount, not per call in a loop","Stub the bridge in unit tests","Keep pluginRest-touching components out of web/dashboard bundles","Don't catch-and-ignore: absence of the bridge is an environment fact, model it as a disabled state"],"tags":["desktop","electron","bridge","plugins","environment"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}