fix(desktop): refuse a foreign backend's session token after readiness

The served-token fallback adopts whatever token the dashboard HTML
injects. That is correct when our own child regenerated the token (env
pin lost across a shell-wrapped spawn), but wrong when the readiness
probe answered from a process we did not spawn: /api/status is public,
so an orphaned dashboard squatting the port passes waitForHermes while
our child dies on the bind conflict. Silently adopting that process's
token would authenticate the renderer against a foreign backend,
possibly on the wrong profile.

Discriminate on child liveness: the desktop pins
HERMES_DASHBOARD_SESSION_TOKEN on every spawn, so a live child always
serves our token. Served-token mismatch + dead child = foreign backend;
fail the boot loudly instead of connecting. Mismatch + live child keeps
the adopt-served-token salvage from #43720.
This commit is contained in:
Brooklyn Nicholson
2026-06-11 18:18:22 -05:00
parent 7a2d498b9d
commit e3ed7722b5
3 changed files with 73 additions and 1 deletions
+30 -1
View File
@@ -29,7 +29,7 @@ const { runBootstrap } = require('./bootstrap-runner.cjs')
const { buildSessionWindowUrl, createSessionWindowRegistry } = require('./session-windows.cjs')
const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')
const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
const { resolveServedDashboardToken } = require('./dashboard-token.cjs')
const { isForeignBackendToken, resolveServedDashboardToken } = require('./dashboard-token.cjs')
const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
const { readDirForIpc } = require('./fs-read-dir.cjs')
@@ -4599,6 +4599,21 @@ async function spawnPoolBackend(profile, entry) {
rememberLog(`[boot] could not read served dashboard token for profile "${profile}": ${error.message}`)
return token
})
if (
isForeignBackendToken({
servedToken: authToken,
spawnToken: token,
childAlive: child.exitCode === null && !child.killed
})
) {
// Our child is dead and the port answers with someone else's token:
// /api/status readiness was a false positive from a process we did not
// spawn. Fail loudly rather than authenticate against a foreign backend.
backendPool.delete(profile)
throw new Error(
`Hermes backend for profile "${profile}" exited and port ${port} is served by a different process; refusing its session token.`
)
}
entry.token = authToken
return {
@@ -4831,6 +4846,20 @@ async function startHermes() {
rememberLog(`[boot] could not read served dashboard token: ${error.message}`)
return token
})
if (
isForeignBackendToken({
servedToken: authToken,
spawnToken: token,
childAlive: hermesProcess.exitCode === null && !hermesProcess.killed
})
) {
// Our child is dead and the port answers with someone else's token:
// /api/status readiness was a false positive from a process we did not
// spawn. Fail loudly rather than authenticate against a foreign backend.
throw new Error(
`Hermes backend exited and port ${port} is served by a different process; refusing its session token.`
)
}
updateBootProgress({
phase: 'backend.ready',
message: 'Hermes backend is ready. Finalizing desktop startup',