mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
Node >=18 / Electron 40 ship fetch; the hand-rolled http/https.request plumbing buys nothing. AbortSignal.timeout replaces the socket timeout, protocol guard and >=400 rejection semantics preserved. 13/13 unit tests and the live web_server.py repro both green over the new transport.
99 lines
3.5 KiB
JavaScript
99 lines
3.5 KiB
JavaScript
/**
|
|
* Helpers for local dashboard session-token discovery.
|
|
*
|
|
* The desktop main process can pass HERMES_DASHBOARD_SESSION_TOKEN when it
|
|
* spawns the local dashboard, but the dashboard is the source of truth for the
|
|
* token it actually serves to the renderer. If those drift, HTTP readiness
|
|
* probes still pass while /api/ws rejects the renderer's token.
|
|
*/
|
|
|
|
const DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 3_000
|
|
|
|
async function fetchPublicText(url, options = {}) {
|
|
const { protocol } = new URL(url)
|
|
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
throw new Error(`Unsupported Hermes backend URL protocol: ${protocol}`)
|
|
}
|
|
|
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }).catch(error => {
|
|
if (error.name === 'TimeoutError') {
|
|
throw new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`)
|
|
}
|
|
throw error
|
|
})
|
|
const text = await res.text()
|
|
|
|
if (!res.ok) throw new Error(`${res.status}: ${text || res.statusText}`)
|
|
|
|
return text
|
|
}
|
|
|
|
function extractInjectedDashboardToken(html) {
|
|
const match = /window\.__HERMES_SESSION_TOKEN__\s*=\s*("(?:\\.|[^"\\])*")/.exec(String(html || ''))
|
|
if (!match) return null
|
|
try {
|
|
return JSON.parse(match[1])
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function dashboardIndexUrl(baseUrl) {
|
|
return `${String(baseUrl || '').replace(/\/+$/, '')}/`
|
|
}
|
|
|
|
async function resolveServedDashboardToken(baseUrl, fallbackToken, options = {}) {
|
|
const fetchText = options.fetchText || fetchPublicText
|
|
const html = await fetchText(dashboardIndexUrl(baseUrl), {
|
|
timeoutMs: options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
|
|
})
|
|
const servedToken = extractInjectedDashboardToken(html)
|
|
|
|
if (servedToken && servedToken !== fallbackToken && typeof options.rememberLog === 'function') {
|
|
options.rememberLog('[boot] dashboard served a different session token; using served token for WebSocket auth')
|
|
}
|
|
|
|
return servedToken || fallbackToken
|
|
}
|
|
|
|
/**
|
|
* A served token that differs from our spawn token while our child is DEAD
|
|
* came from a process we did not spawn (orphan/port squatter that satisfied
|
|
* the public /api/status readiness probe). With a live child the mismatch is
|
|
* benign: our own backend regenerated the token because the env pin did not
|
|
* survive the spawn.
|
|
*/
|
|
function isForeignBackendToken({ servedToken, spawnToken, childAlive }) {
|
|
return Boolean(servedToken) && servedToken !== spawnToken && !childAlive
|
|
}
|
|
|
|
/**
|
|
* Resolve the token the backend actually serves, adopting benign drift and
|
|
* failing loudly on a foreign backend. `childAlive` is a thunk so liveness is
|
|
* sampled after the fetch, not before.
|
|
*/
|
|
async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, label = 'Hermes backend', ...options }) {
|
|
const servedToken = await resolveServedDashboardToken(baseUrl, spawnToken, options).catch(error => {
|
|
options.rememberLog?.(`[boot] could not read served dashboard token (${label}): ${error.message}`)
|
|
return spawnToken
|
|
})
|
|
|
|
if (isForeignBackendToken({ servedToken, spawnToken, childAlive: childAlive() })) {
|
|
throw new Error(
|
|
`${label} exited and ${dashboardIndexUrl(baseUrl)} is served by a process we did not spawn; refusing its session token.`
|
|
)
|
|
}
|
|
|
|
return servedToken
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_TOKEN_FETCH_TIMEOUT_MS,
|
|
adoptServedDashboardToken,
|
|
dashboardIndexUrl,
|
|
extractInjectedDashboardToken,
|
|
fetchPublicText,
|
|
isForeignBackendToken,
|
|
resolveServedDashboardToken
|
|
}
|