feat(desktop): support connecting to a remote Hermes backend

Add HERMES_DESKTOP_REMOTE_URL and HERMES_DESKTOP_REMOTE_TOKEN env
vars that, when set, short-circuit the local-child spawn in
startHermes() and connect the Electron renderer to an already-
running 'hermes dashboard' server reachable over the network.

Motivating use case: WSL2 users who want to run the Hermes core
(agent loop, tools, filesystem access) inside their WSL
distribution while rendering the Electron GUI on native Windows.
Before this change, the desktop app always spawned a local Python
child on the same host as the renderer, which doesn't cross the
WSL/Windows boundary.

The remote path reuses waitForHermes() as a liveness probe
(/api/status is in the backend's public endpoint allowlist), so
the connection is only returned once the backend is actually
ready. WebSocket URL derivation picks ws:// or wss:// based on
the input scheme. URL validation rejects non-http(s) schemes and
requires both env vars together to avoid a half-configured
connection that would silently fall through to the spawn path.

No behaviour change when the env vars are unset — the default
local-spawn flow is untouched.

Typical usage:

  # in WSL2
  hermes dashboard --tui --no-open --host 0.0.0.0 --port 9119 --insecure

  # on Windows
  set HERMES_DESKTOP_REMOTE_URL=http://localhost:9119
  set HERMES_DESKTOP_REMOTE_TOKEN=<session token>
  set HERMES_DESKTOP_IGNORE_EXISTING=1
  (launch Hermes desktop)
This commit is contained in:
emozilla 2026-05-05 02:08:32 -04:00
parent 2964f25534
commit 3aabae20eb

View file

@ -1068,9 +1068,56 @@ function installMediaPermissions() {
})
}
function resolveRemoteBackend() {
const rawUrl = process.env.HERMES_DESKTOP_REMOTE_URL
const rawToken = process.env.HERMES_DESKTOP_REMOTE_TOKEN
if (!rawUrl) return null
if (!rawToken) {
throw new Error(
'HERMES_DESKTOP_REMOTE_URL is set but HERMES_DESKTOP_REMOTE_TOKEN is not. ' +
'Both must be provided to connect to a remote Hermes backend.'
)
}
let parsed
try {
parsed = new URL(rawUrl)
} catch (error) {
throw new Error(`HERMES_DESKTOP_REMOTE_URL is not a valid URL: ${error.message}`)
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`HERMES_DESKTOP_REMOTE_URL must be http:// or https://, got ${parsed.protocol}`)
}
const baseUrl = `${parsed.protocol}//${parsed.host}`
const wsScheme = parsed.protocol === 'https:' ? 'wss' : 'ws'
const wsUrl = `${wsScheme}://${parsed.host}/api/ws?token=${encodeURIComponent(rawToken)}`
return { baseUrl, token: rawToken, wsUrl }
}
async function startHermes() {
if (connectionPromise) return connectionPromise
const remote = resolveRemoteBackend()
if (remote) {
connectionPromise = (async () => {
rememberLog(`Using remote Hermes backend at ${remote.baseUrl}`)
await waitForHermes(remote.baseUrl, remote.token)
return {
baseUrl: remote.baseUrl,
token: remote.token,
wsUrl: remote.wsUrl,
logs: hermesLog.slice(-80),
windowButtonPosition: getWindowButtonPosition()
}
})().catch(error => {
connectionPromise = null
throw error
})
return connectionPromise
}
connectionPromise = (async () => {
const port = await pickPort()
const token = crypto.randomBytes(32).toString('base64url')