Files
hermes-agent/apps/desktop/electron/remote-lifecycle.test.ts
T
Casey beb212dcc5 desktop: fix two managed-SSH-spawn bugs that break every fresh remote backend
1. Quoting: the spawn payload wrapped expandRemotePath() output -- already
   a shell-quoted fragment like "$HOME"'/...' -- in shq() again, so the
   reservation/lock/owner_file variables hold the quote characters
   literally and every mkdir "$reservation" fails forever (~5 min per
   attempt spinning in the reservation loop while holding the box-global
   update mutex; queued spawns starve behind it). The same double quoting
   sits in the stale-reaper identity guards, making every reap REFUSE.
   The lockfile-reuse path masks the bug for existing backends, so it
   only bites on fresh spawns.
2. Bashism: lockfile publication used ${var//__PID__/$child} -- bash-only
   substitution in a payload run under plain sh (dash on Ubuntu), which
   aborts the script AFTER the serve was spawned. The client then saw an
   unknown failure, ran its error cleanup (deleting the token file), and
   the just-booted serve died on the missing token -- orphaning one serve
   per attempt. Replaced with a POSIX sed substitution.

Adds two regression tests: payload variables must keep $HOME expandable
(no re-quoting), and the pid substitution must be POSIX sh. Both fail
against the previous code; all 89 remote-lifecycle tests pass with the
fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 02:48:21 -07:00

1852 lines
61 KiB
TypeScript

import assert from 'node:assert/strict'
import { exec as execCallback, spawn } from 'node:child_process'
import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { promisify } from 'node:util'
import { test } from 'vitest'
import { profileSshOverride } from './connection-config'
import {
assertRemoteInstallUpdateClear,
buildSpawnCommand,
classifySshReuseProof,
cleanupStale,
connect,
disconnect,
expandRemotePath,
fingerprintToken,
isForwardBindCollision,
isLockfileSkew,
listRemoteHermesProfiles,
locateHermes,
LOCKFILE_SCHEMA_VERSION,
lockfilePath,
openForward,
ownershipDirectory,
pidIsOurDashboard,
probeRemotePlatform,
PROTOCOL_VERSION,
readLockfile,
READY_RE,
remotePidAlive,
remoteSupportsSshOwnership,
scrapeReadyPort,
spawnLogPath,
spawnRemoteDashboard,
spawnTokenPath,
terminateOwnedDashboardForUpdate,
validateRemotePath,
writeLockfile
} from './remote-lifecycle'
const OWNERSHIP_ID = '0123456789abcdef0123456789abcdef'
const SPAWN_NONCE = '0123456789abcdef'
const exec = promisify(execCallback)
test('SSH reuse proof rejects a backend whose runtime was replaced', () => {
assert.equal(
classifySshReuseProof(
{ ok: true, sshOwnerNonce: SPAWN_NONCE, protocolVersion: 1, runtimeIntact: false },
SPAWN_NONCE
),
'authenticated-stale'
)
})
test('SSH reuse proof remains compatible when runtime state is absent', () => {
assert.equal(
classifySshReuseProof({ ok: true, sshOwnerNonce: SPAWN_NONCE, protocolVersion: 1 }, SPAWN_NONCE),
'authenticated-ok'
)
})
function ownedLock(over: any = {}) {
return {
schemaVersion: LOCKFILE_SCHEMA_VERSION,
protocolVersion: PROTOCOL_VERSION,
ownershipId: OWNERSHIP_ID,
spawnNonce: SPAWN_NONCE,
pid: 333,
port: 40000,
profile: '',
hermesPath: '~/.local/bin/hermes',
hermesHome: '~/.hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
tokenFingerprint: fingerprintToken('stored-token'),
startedAt: '2026-07-14T00:00:00.000Z',
creationTime: 'linux:123456',
...over
}
}
// A fake SshConnection whose exec() is matched against an ordered list of
// [regex|fn, response|fn] rules. First match wins; unmatched commands return ''.
function fakeSsh(rules: any[] = []) {
const calls: string[] = []
return {
calls,
async exec(cmd) {
calls.push(cmd)
// Existing lifecycle fixtures predate the install-wide relaunch gate.
// Their default remote has no update marker; focused marker tests below
// use explicit SSH doubles to exercise live/uncertain transitions.
if (cmd.includes('.hermes-update-in-progress') && !cmd.includes('marker_clear()') && !/setsid|nohup/.test(cmd)) {
return 'CLEAR'
}
const mutexWrapped = cmd.includes('fcntl.flock(fd,fcntl.LOCK_EX)')
const applicableRules = rules.filter(([matcher]) => {
if (cmd.includes('marker_clear()') && matcher instanceof RegExp && /kill -0/.test(matcher.source)) {
return false
}
return !(mutexWrapped && matcher instanceof RegExp && /python3 -c/.test(matcher.source))
})
if ((cmd.includes('os.kill(pid') && !cmd.includes('pidfd_open')) || cmd.includes('printf TERMINATED')) {
return 'TERMINATED'
}
for (const [matcher, resp] of applicableRules) {
const hit = typeof matcher === 'function' ? matcher(cmd) : matcher.test(cmd)
if (hit) {
const out = typeof resp === 'function' ? resp(cmd) : resp
if (out instanceof Error) {
throw out
}
return out
}
}
return ''
}
}
}
test('POSIX relaunch gate refuses live and uncertain install markers without executing Hermes', async () => {
for (const observation of ['LIVE:4242', 'UNCERTAIN']) {
const calls: string[] = []
const ssh = {
async exec(command) {
calls.push(command)
if (command === 'uname -s; uname -m') {
return 'Linux\nx86_64\n'
}
if (command.includes('HERMES_HOME')) {
return '/home/alice/.hermes\n'
}
if (command.includes('.hermes-update-in-progress')) {
return observation
}
throw new Error(`unexpected command after update gate: ${command}`)
}
}
await assert.rejects(
() => connect(connectDeps(ssh)),
(error: any) => error.kind === 'update-in-progress'
)
assert.equal(
calls.some(command => /\[ -x |--version|lock\.json|serve --help|setsid/.test(command)),
false
)
}
})
test('POSIX relaunch gate permits absent/dead markers and normalizes named-profile homes install-wide', async () => {
const commands: string[] = []
const ssh = {
async exec(command) {
commands.push(command)
return 'CLEAR'
}
}
await assertRemoteInstallUpdateClear(ssh, '/home/alice/.hermes/profiles/research')
assert.match(commands[0], /home\.parent\.name/)
assert.match(commands[0], /profiles/)
assert.match(commands[0], /\.hermes-update-in-progress/)
})
test('POSIX relaunch gate rechecks after token upload immediately before process creation', async () => {
const calls: string[] = []
let markerChecks = 0
const ssh = {
async exec(command) {
calls.push(command)
if (command === 'uname -s; uname -m') {
return 'Linux\nx86_64\n'
}
if (command.includes('HERMES_HOME')) {
return '/home/alice/.hermes\n'
}
if (command.includes('.hermes-update-in-progress')) {
markerChecks += 1
return markerChecks >= 3 ? 'LIVE:4242' : 'CLEAR'
}
if (/\[ -x /.test(command)) {
return 'OK'
}
if (command.includes('serve --help')) {
return 'YES\n'
}
if (command.includes('python3 -c')) {
return ''
}
if (command.includes('lock.json')) {
return ''
}
return ''
}
}
await assert.rejects(
() => connect(connectDeps(ssh)),
(error: any) => error.kind === 'update-in-progress'
)
assert.equal(markerChecks, 3)
assert.equal(
calls.some(command => /setsid|nohup/.test(command)),
false
)
})
test('listRemoteHermesProfiles inventories Mini-style profile dirs without spawning a dashboard', async () => {
const ssh = fakeSsh([
[/HERMES_HOME/, '/Users/zillajr/.hermes\n'],
[/ls -1/, 'bob\ndixie\ngoose\nrambo\nbob.rollback-old\n']
])
assert.deepEqual(await listRemoteHermesProfiles(ssh), ['default', 'bob', 'dixie', 'goose', 'rambo'])
assert.equal(
ssh.calls.some(cmd => cmd.includes('serve') || cmd.includes('dashboard')),
false
)
})
test('listRemoteHermesProfiles rejects a hostile HERMES_HOME', async () => {
const ssh = fakeSsh([[/HERMES_HOME/, '/tmp/x; echo pwned\n']])
await assert.rejects(
() => listRemoteHermesProfiles(ssh),
(err: any) => {
assert.equal(err.kind, 'unsafe-path')
return true
}
)
assert.equal(
ssh.calls.some(cmd => cmd.includes('ls -1')),
false
)
})
test('locateHermes prefers the explicit profile path when executable', async () => {
const ssh = fakeSsh([[/\[ -x .*\/opt\/hermes/, 'OK']])
assert.equal(await locateHermes(ssh, '/opt/hermes'), '/opt/hermes')
})
test('locateHermes throws (no silent fallback) when an EXPLICIT path is not executable', async () => {
// command -v WOULD find a different install, but an explicit path must not
// silently fall back to it — that is the "connected to the wrong hermes" bug.
const ssh = fakeSsh([
[/command -v hermes/, '/home/u/.local/bin/hermes\n'],
[/\[ -x .*\.local\/bin\/hermes/, 'OK']
])
await assert.rejects(
() => locateHermes(ssh, '/bad/path/hermes'),
(err: any) => {
assert.equal(err.kind, 'hermes-not-found')
assert.match(err.message, /\/bad\/path\/hermes/)
return true
}
)
})
test('locateHermes falls back to the login-shell command -v probe', async () => {
const ssh = fakeSsh([
[/command -v hermes/, '/home/u/.local/bin/hermes\n'],
[/\[ -x .*\.local\/bin\/hermes/, 'OK']
])
assert.equal(await locateHermes(ssh, ''), '/home/u/.local/bin/hermes')
})
test('locateHermes preserves an installer wrapper instead of resolving its interpreter', async () => {
// install.sh venv mode writes: exec "$HERMES_BIN" "$HERMES_ENTRYPOINT" "$@",
// where $HERMES_BIN is the venv python. The old canonicalization returned
// that interpreter, so `<python> --version` printed "Python x.y.z" and
// `<python> serve --help` failed outright (#74411). The wrapper itself is
// executable and forwards args correctly — return it untouched.
const ssh = fakeSsh([
[/command -v hermes/, '/home/u/.local/bin/hermes\n'],
[/\[ -x .*\.local\/bin\/hermes/, 'OK'],
// If the removed python3 wrapper-parser were ever reintroduced, this rule
// would reward it with an interpreter path and the assertions below fail.
[/python3 -c/, '/home/u/.hermes/hermes-agent/venv/bin/python\n']
])
assert.equal(await locateHermes(ssh, ''), '/home/u/.local/bin/hermes')
assert.ok(
!ssh.calls.some(cmd => cmd.includes('python3 -c')),
'locateHermes must not shell out to a python3 parser to rewrite the launcher'
)
})
test('locateHermes returns an explicit remoteHermesPath unchanged', async () => {
// The override half of #74411: an explicit remoteHermesPath pointing at a
// wrapper was also canonicalized to its interpreter, so overriding to
// ~/.local/bin/hermes changed nothing for affected users.
const ssh = fakeSsh([
[/\[ -x .*\.local\/bin\/hermes/, 'OK'],
[/python3 -c/, '/home/u/.hermes/hermes-agent/venv/bin/python\n']
])
assert.equal(await locateHermes(ssh, '~/.local/bin/hermes'), '~/.local/bin/hermes')
assert.ok(!ssh.calls.some(cmd => cmd.includes('python3 -c')), 'an explicit remoteHermesPath must never be rewritten')
})
test('locateHermes falls back to ~/.local/bin/hermes when the login-shell probe misses', async () => {
// ~/.local/bin is the non-root installer's command location (scripts/install.sh).
const ssh = fakeSsh([
[/command -v hermes/, ''],
[/\[ -x .*\.local\/bin\/hermes/, 'OK']
])
assert.equal(await locateHermes(ssh, ''), '~/.local/bin/hermes')
})
test('locateHermes tries the conventional venv path last', async () => {
const ssh = fakeSsh([[/\[ -x .*venv\/bin\/hermes/, 'OK']])
assert.equal(await locateHermes(ssh, ''), '~/.hermes/hermes-agent/venv/bin/hermes')
})
test('locateHermes throws a hermes-not-found error with an install hint', async () => {
const ssh = fakeSsh([]) // nothing is executable
await assert.rejects(
() => locateHermes(ssh, ''),
(err: any) => {
assert.equal(err.kind, 'hermes-not-found')
assert.match(err.message, /install/i)
return true
}
)
})
test('locateHermes uses a login shell for the command -v probe', async () => {
const ssh = fakeSsh([
[/command -v hermes/, '/x/hermes'],
[/\[ -x/, 'OK']
])
await locateHermes(ssh, '')
assert.ok(
ssh.calls.some(c => /bash -lc/.test(c)),
'must probe in a login shell (PATH pitfall)'
)
})
test('probeRemotePlatform accepts Linux and macOS', async () => {
assert.deepEqual(await probeRemotePlatform(fakeSsh([[/uname/, 'Linux\nx86_64']])), {
os: 'Linux',
arch: 'x86_64'
})
assert.deepEqual(await probeRemotePlatform(fakeSsh([[/uname/, 'Darwin\narm64']])), {
os: 'Darwin',
arch: 'arm64'
})
})
test('probeRemotePlatform rejects unsupported remote platforms', async () => {
await assert.rejects(
() => probeRemotePlatform(fakeSsh([[/uname/, 'MINGW64_NT\nx86_64']])),
(err: any) => {
assert.equal(err.kind, 'unsupported-platform')
return true
}
)
})
test('ownership paths are isolated by ownership ID and spawn nonce', () => {
assert.equal(ownershipDirectory(OWNERSHIP_ID), `~/.hermes/desktop-ssh/${OWNERSHIP_ID}`)
assert.equal(lockfilePath(OWNERSHIP_ID), `~/.hermes/desktop-ssh/${OWNERSHIP_ID}/backend.lock.json`)
assert.equal(spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE), `~/.hermes/desktop-ssh/${OWNERSHIP_ID}/${SPAWN_NONCE}.log`)
})
test('readLockfile returns null ONLY for a missing/empty lockfile', async () => {
assert.equal(await readLockfile(fakeSsh([[/cat/, '']]), OWNERSHIP_ID), null)
const good = ownedLock({ pid: 1, port: 2 })
assert.deepEqual(await readLockfile(fakeSsh([[/cat/, JSON.stringify(good)]]), OWNERSHIP_ID), good)
})
// #95532 fail-closed guard: a lockfile that EXISTS but doesn't match what this
// build writes is SKEW (foreign fork build, corruption, or a future schema) —
// it must be distinguishable from "no lockfile" so no reap/overwrite path can
// treat foreign live state as reapable.
test('readLockfile classifies existing-but-foreign lockfiles as skew, never null', async () => {
// (a) foreign-schema lockfile (fork build wrote a different shape)
const foreign = await readLockfile(fakeSsh([[/cat/, 'not json at all']]), OWNERSHIP_ID)
assert.equal(isLockfileSkew(foreign), true)
// (b) truncated lockfile (partial write / corruption)
const truncated = await readLockfile(fakeSsh([[/cat/, JSON.stringify(ownedLock()).slice(0, 40)]]), OWNERSHIP_ID)
assert.equal(isLockfileSkew(truncated), true)
// (c) future schemaVersion (newer build owns this remote)
const future = await readLockfile(
fakeSsh([[/cat/, JSON.stringify(ownedLock({ schemaVersion: LOCKFILE_SCHEMA_VERSION + 1 }))]]),
OWNERSHIP_ID
)
assert.equal(isLockfileSkew(future), true)
// unknown schema number entirely
const unknown = await readLockfile(fakeSsh([[/cat/, JSON.stringify({ schemaVersion: 999 })]]), OWNERSHIP_ID)
assert.equal(isLockfileSkew(unknown), true)
// missing ownershipId / foreign ownership
const foreignOwner = await readLockfile(
fakeSsh([[/cat/, JSON.stringify(ownedLock({ ownershipId: undefined }))]]),
OWNERSHIP_ID
)
assert.equal(isLockfileSkew(foreignOwner), true)
// every skew carries a diagnosable reason and is never a valid lock
for (const skew of [foreign, truncated, future, unknown, foreignOwner]) {
assert.equal(typeof (skew as any).reason, 'string')
assert.notEqual(skew, null)
}
// a valid lock and a missing lockfile are NOT skew
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, '']]), OWNERSHIP_ID)), false)
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, JSON.stringify(ownedLock())]]), OWNERSHIP_ID)), false)
})
// #95532: on skew the reap pass must FAIL CLOSED — no kill, no lockfile
// removal/overwrite, no fresh spawn on top of foreign live state.
test('connect() fails closed on lockfile schema/ownership skew: skips reap, touches nothing', async () => {
const skewShapes: Array<[string, string]> = [
['foreign-schema lockfile', '{"pid":333,"owner":"some-fork-desktop","version":"9.9.9"}'],
['truncated lockfile', JSON.stringify(ownedLock()).slice(0, 40)],
['future schemaVersion', JSON.stringify(ownedLock({ schemaVersion: LOCKFILE_SCHEMA_VERSION + 1 }))]
]
for (const [label, raw] of skewShapes) {
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, raw],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
await assert.rejects(
() => connect(connectDeps(ssh, { reuseToken: 'stored-token' })),
(error: any) => error.kind === 'remote-lockfile-skew',
`${label}: connect must refuse with remote-lockfile-skew`
)
assert.ok(
!ssh.calls.some(c => /(^|[^-\d])kill -?9? ?\d/.test(c) && !/kill -0/.test(c)),
`${label}: must not kill any pid`
)
assert.ok(!ssh.calls.some(c => /rm -f/.test(c)), `${label}: must not remove any remote file`)
assert.ok(!ssh.calls.some(c => /setsid|nohup/.test(c)), `${label}: must not spawn on top of foreign state`)
assert.ok(!ssh.calls.some(c => /printf '%s' '.*schemaVersion/.test(c)), `${label}: must not overwrite the lockfile`)
}
})
test('disconnect() fails closed on lockfile skew: never reaps, never drops the foreign lockfile', async () => {
const ssh = fakeSsh([
[/cat .*lock\.json/, JSON.stringify(ownedLock({ schemaVersion: LOCKFILE_SCHEMA_VERSION + 1 }))],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
await disconnect(ssh, OWNERSHIP_ID)
assert.ok(!ssh.calls.some(c => /(^|[^-\d])kill -?9? ?\d/.test(c) && !/kill -0/.test(c)), 'must not kill any pid')
assert.ok(!ssh.calls.some(c => /rm -f/.test(c)), 'must not remove the foreign lockfile or logs')
})
test('cleanupStale is inert when handed a skew sentinel (defense in depth)', async () => {
const ssh = fakeSsh([[/print\("OWNED"/, 'OWNED\n']])
await cleanupStale(ssh, OWNERSHIP_ID, { skew: true, reason: 'schema-version' } as any)
assert.equal(ssh.calls.length, 0, 'skew must short-circuit before any remote command')
})
test('writeLockfile mkdir -ps and stamps the schema version', async () => {
const ssh = fakeSsh([])
await writeLockfile(ssh, OWNERSHIP_ID, ownedLock({ pid: 7, port: 9 }))
const cmd = ssh.calls.join('\n')
assert.match(cmd, /mkdir -p/)
assert.match(cmd, new RegExp(`"schemaVersion":${LOCKFILE_SCHEMA_VERSION}`))
})
test('remotePidAlive maps kill -0 ALIVE/DEAD', async () => {
assert.equal(await remotePidAlive(fakeSsh([[/kill -0/, 'ALIVE']]), 123), true)
assert.equal(await remotePidAlive(fakeSsh([[/kill -0/, 'DEAD']]), 123), false)
assert.equal(await remotePidAlive(fakeSsh([]), null), false)
})
test('metadata and process proof transport failures remain indeterminate', async () => {
const failure = new Error('connection reset')
await assert.rejects(
() => readLockfile(fakeSsh([[/cat/, failure]]), OWNERSHIP_ID),
(error: any) => error.kind === 'transient-transport-error'
)
await assert.rejects(
() => remotePidAlive(fakeSsh([[/kill -0/, failure]]), 123),
(error: any) => error.kind === 'transient-transport-error'
)
await assert.rejects(
() => pidIsOurDashboard(fakeSsh([[/print\("OWNED"/, failure]]), 5, SPAWN_NONCE, '/x/hermes'),
(error: any) => error.kind === 'transient-transport-error'
)
})
test('pidIsOurDashboard requires the exact serve ownership nonce', async () => {
const ours = `/x/hermes serve --isolated --ssh-owner-nonce ${SPAWN_NONCE}`
assert.equal(await pidIsOurDashboard(fakeSsh([[/print\("OWNED"/, 'OWNED\n']]), 5, SPAWN_NONCE, '/x/hermes'), true)
assert.equal(
await pidIsOurDashboard(
fakeSsh([[/print\("OWNED"/, command => (command.includes('fedcba9876543210') ? 'FOREIGN\n' : 'OWNED\n')]]),
5,
'fedcba9876543210',
'/x/hermes'
),
false
)
assert.equal(await pidIsOurDashboard(fakeSsh([[/print\("OWNED"/, 'FOREIGN\n']]), 5, SPAWN_NONCE, '/x/hermes'), false)
})
test('pidIsOurDashboard accepts the venv entrypoint an installer wrapper execs into', async () => {
let ownershipProbe = ''
const ssh = fakeSsh([
[
/python3 -c/,
(command: string) => {
ownershipProbe = command
return 'OWNED\n'
}
]
])
assert.equal(
await pidIsOurDashboard(ssh, 5, SPAWN_NONCE, '~/.local/bin/hermes', '/Users/cd9c/.hermes', OWNERSHIP_ID, 'ops'),
true
)
assert.match(ownershipProbe, /hermes-agent.*venv.*bin.*hermes/)
assert.match(ownershipProbe, /desktop-ssh.*0123456789abcdef\.token/)
assert.match(ownershipProbe, /expected_profile=.*ops/)
})
test.skipIf(process.platform === 'win32')(
'pidIsOurDashboard recognizes an installer wrapper after it execs python + entrypoint',
async () => {
const temp = await mkdtemp(path.join(os.tmpdir(), 'hermes wrapper ownership '))
const installDir = path.join(temp, 'install dir')
const venvBin = path.join(installDir, 'venv', 'bin')
const pythonLink = path.join(venvBin, 'python')
const entrypoint = path.join(installDir, 'hermes')
const launcher = path.join(temp, 'hermes launcher')
const python = (await exec('command -v python3')).stdout.trim()
const tokenPath = path.join(os.homedir(), spawnTokenPath(OWNERSHIP_ID, SPAWN_NONCE).replace(/^~\//, ''))
await mkdir(venvBin, { recursive: true })
await symlink(python, pythonLink)
await writeFile(entrypoint, 'import time\ntime.sleep(30)\n', 'utf8')
await writeFile(launcher, `#!/bin/bash\nexec "${pythonLink}" "${entrypoint}" "$@"\n`, 'utf8')
await chmod(launcher, 0o755)
const backendFlags = [
'--host',
'127.0.0.1',
'--port',
'0',
'--ssh-session-token-file',
tokenPath,
'--ssh-owner-nonce',
SPAWN_NONCE
]
const children: ReturnType<typeof spawn>[] = []
const spawnInstaller = (args: string[]) => {
const process = spawn(launcher, args, { stdio: 'ignore' })
children.push(process)
return process
}
const child = spawnInstaller(['--profile', 'ops', 'serve', '--isolated', ...backendFlags])
const ssh = {
exec: async (command: string) => (await exec(command, { shell: '/bin/bash' })).stdout
}
const waitForEntrypoint = async (process: ReturnType<typeof spawn>) => {
for (let attempt = 0; attempt < 40; attempt += 1) {
const command = (await exec(`ps -ww -o command= -p ${process.pid}`)).stdout
if (command.includes(entrypoint)) {
return true
}
await new Promise(resolve => setTimeout(resolve, 25))
}
return false
}
try {
assert.equal(await waitForEntrypoint(child), true, 'wrapper must exec into the fake installer entrypoint')
assert.equal(
await pidIsOurDashboard(ssh, child.pid, SPAWN_NONCE, launcher, '/unrelated/hermes-home', OWNERSHIP_ID, 'ops'),
true
)
assert.equal(
await pidIsOurDashboard(
ssh,
child.pid,
SPAWN_NONCE,
launcher,
'/unrelated/hermes-home',
'fedcba9876543210fedcba9876543210',
'ops'
),
false
)
assert.equal(
await pidIsOurDashboard(
ssh,
child.pid,
SPAWN_NONCE,
launcher,
'/unrelated/hermes-home',
OWNERSHIP_ID,
'wrong-profile'
),
false
)
const misplacedIsolated = spawnInstaller(['--profile', 'ops', '--isolated', 'serve', ...backendFlags])
assert.equal(await waitForEntrypoint(misplacedIsolated), true)
assert.equal(
await pidIsOurDashboard(
ssh,
misplacedIsolated.pid,
SPAWN_NONCE,
launcher,
'/unrelated/hermes-home',
OWNERSHIP_ID,
'ops'
),
false,
'--isolated before serve must remain foreign'
)
const conflictingProfile = spawnInstaller([
'--profile',
'ops',
'serve',
'--isolated',
...backendFlags,
'--profile',
'foreign'
])
assert.equal(await waitForEntrypoint(conflictingProfile), true)
assert.equal(
await pidIsOurDashboard(
ssh,
conflictingProfile.pid,
SPAWN_NONCE,
launcher,
'/unrelated/hermes-home',
OWNERSHIP_ID,
'ops'
),
false,
'a duplicate conflicting profile must remain foreign'
)
} finally {
for (const process of children) {
process.kill('SIGTERM')
}
await rm(temp, { force: true, recursive: true })
}
}
)
test('disconnect reaps the backend recorded for this desktop ownership', async () => {
const lock = ownedLock()
const ssh = fakeSsh([
[/cat .*backend\.lock\.json/, JSON.stringify(lock)],
[/kill -0 333/, 'ALIVE\n'],
[/print\("OWNED"/, 'OWNED\n']
])
await disconnect(ssh, OWNERSHIP_ID)
assert.ok(ssh.calls.some(command => /kill 333\b/.test(command)))
assert.ok(ssh.calls.some(command => /rm -f .*backend\.lock\.json/.test(command)))
})
test('disconnect is a no-op when this desktop has no lockfile', async () => {
const ssh = fakeSsh([[/cat .*backend\.lock\.json/, '']])
await disconnect(ssh, OWNERSHIP_ID)
assert.ok(!ssh.calls.some(command => /\bkill\b/.test(command)))
})
test('cleanupStale kills ONLY a provably-ours pid, always drops the lockfile', async () => {
const notOurs = fakeSsh([[/print\("OWNED"/, 'FOREIGN\n']])
await cleanupStale(notOurs, OWNERSHIP_ID, {
pid: 5,
spawnNonce: SPAWN_NONCE,
hermesPath: '/x/hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE)
})
assert.ok(
notOurs.calls.some(c => /print\("OWNED"/.test(c)),
'must perform an ownership preflight'
)
assert.ok(notOurs.calls.some(c => /rm -f/.test(c)))
const ours = fakeSsh([
[/print\("OWNED"/, 'OWNED\n'],
[cmd => /printf TERMINATED/.test(cmd), 'TERMINATED\n']
])
await cleanupStale(ours, OWNERSHIP_ID, {
pid: 9,
spawnNonce: SPAWN_NONCE,
hermesPath: '/x/hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE)
})
assert.ok(ours.calls.some(c => /kill 9\b/.test(c)))
assert.ok(ours.calls.some(c => /rm -f/.test(c)))
})
test('buildSpawnCommand is headless serve, detached, token not in argv', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', { logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE) })
assert.match(cmd, /serve --isolated/)
assert.match(cmd, /--host 127\.0\.0\.1 --port 0/)
assert.doesNotMatch(cmd, /--skip-build|--no-open/)
assert.doesNotMatch(cmd, /\bdashboard\b/)
assert.match(cmd, /--profile/)
assert.match(cmd, /work/)
assert.match(cmd, /setsid/)
assert.match(cmd, /<\/dev\/null/)
assert.match(cmd, /echo \$!/)
assert.ok(!cmd.includes('tok_secret_value'), 'token must not appear in spawn command')
assert.ok(!cmd.includes('HERMES_DASHBOARD_SESSION_TOKEN'), 'token env var must not appear')
})
test('buildSpawnCommand always uses serve (legacy dashboard path removed)', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', { logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE) })
assert.match(cmd, /serve --isolated/)
assert.match(cmd, /--host 127\.0\.0\.1 --port 0/)
assert.doesNotMatch(cmd, /dashboard/)
assert.doesNotMatch(cmd, /--skip-build/)
assert.match(cmd, /setsid/)
})
test('buildSpawnCommand atomically reserves the ownership slot through spawn and lock publication', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', {
hermesHome: '~/.hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
ownershipId: OWNERSHIP_ID,
reservationNonce: SPAWN_NONCE,
spawnNonce: SPAWN_NONCE,
tokenFilePath: spawnTokenPath(OWNERSHIP_ID, SPAWN_NONCE),
lockMetadata: {
ownershipId: OWNERSHIP_ID,
spawnNonce: SPAWN_NONCE,
port: 0,
profile: 'work',
hermesPath: '/x/hermes',
hermesHome: '~/.hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
tokenFingerprint: fingerprintToken('stored-token'),
protocolVersion: PROTOCOL_VERSION,
startedAt: '2026-07-14T00:00:00.000Z'
}
})
assert.ok(cmd.includes('.connect.lock'))
assert.ok(cmd.includes('.hermes-update-in-progress.mutex'))
assert.match(cmd, /fcntl\.flock\(fd,fcntl\.LOCK_EX\)/)
assert.match(cmd, /os\.O_CLOEXEC/)
assert.match(
cmd,
/subprocess\.run\(\["sh","-c",payload,"hermes-update-mutex",str\(fd\)\],pass_fds=\(fd,\),check=False\)/
)
assert.doesNotMatch(cmd, /os\.set_inheritable\(fd,True\)/)
assert.match(cmd, /hermes-update-child "\$1"/)
assert.match(cmd, /eval "exec \$1>&-"/)
assert.ok(cmd.includes('backend.lock.json'))
assert.match(cmd, /lock_json/)
assert.match(cmd, /trap .*rm -rf/)
assert.ok(cmd.indexOf('lock_json') > cmd.indexOf('serve --isolated'))
})
test.skipIf(process.platform === 'win32')('detached backend does not inherit the update mutex descriptor', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'hermes-update-mutex-'))
const hermesPath = path.join(directory, 'hermes')
const reportPath = path.join(directory, 'descriptor-report')
const logPath = path.join(directory, 'spawn.log')
try {
await writeFile(
hermesPath,
`#!/bin/sh
: > ${reportPath}
for fd in /proc/$$/fd/*; do
target=$(readlink "$fd" 2>/dev/null || true)
case "$target" in
*hermes-update-in-progress.mutex) printf '%s\\n' "$target" >> ${reportPath} ;;
esac
done
`,
{ mode: 0o700 }
)
const command = buildSpawnCommand(hermesPath, '', {
hermesHome: path.join(directory, 'home'),
logPath
})
await exec(command, { shell: '/bin/bash' })
for (let attempt = 0; attempt < 40; attempt += 1) {
try {
const report = await readFile(reportPath, 'utf8')
assert.equal(report, '', 'the backend process must not retain the update mutex descriptor')
return
} catch (error: any) {
if (error?.code !== 'ENOENT') {
throw error
}
await new Promise(resolve => setTimeout(resolve, 25))
}
}
assert.fail('the detached backend did not write its descriptor report')
} finally {
await rm(directory, { recursive: true, force: true })
}
})
test('spawnRemoteDashboard returns exact ownership artifacts', async () => {
const ssh = fakeSsh([
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/printf '%s\\n'/, ''],
[/setsid|nohup/, '4242\n']
])
const { pid, spawnNonce, logPath } = await spawnRemoteDashboard(ssh, {
hermesPath: '/x/hermes',
profile: '',
token: 'tk',
ownershipId: OWNERSHIP_ID
})
assert.equal(pid, 4242)
assert.match(spawnNonce, /^[0-9a-f]{16}$/)
assert.equal(logPath, spawnLogPath(OWNERSHIP_ID, spawnNonce))
})
test('spawnRemoteDashboard always spawns serve (legacy dashboard path removed)', async () => {
const ssh = fakeSsh([
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/printf '%s\\n'/, ''],
[/setsid|nohup/, '4242\n']
])
await spawnRemoteDashboard(ssh, { hermesPath: '/x/hermes', profile: '', token: 'tk', ownershipId: OWNERSHIP_ID })
const spawn = ssh.calls.find(c => /setsid|nohup/.test(c))
assert.match(spawn, /serve --isolated/)
assert.doesNotMatch(spawn, /\bdashboard\b/)
})
test('READY_RE accepts both serve and dashboard sentinels', () => {
assert.equal(READY_RE.exec('HERMES_BACKEND_READY port=4321')?.[1], '4321')
assert.equal(READY_RE.exec('HERMES_DASHBOARD_READY port=8765')?.[1], '8765')
})
test('spawnRemoteDashboard rejects when no pid is returned', async () => {
const ssh = fakeSsh([
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/printf '%s\\n'/, ''],
[/setsid|nohup/, 'not-a-pid']
])
await assert.rejects(
() => spawnRemoteDashboard(ssh, { hermesPath: '/x/hermes', profile: '', token: 't', ownershipId: OWNERSHIP_ID }),
(err: any) => {
assert.equal(err.kind, 'spawn-failed')
return true
}
)
})
test('scrapeReadyPort reads only the named spawn log', async () => {
const logPath = spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE)
const ssh = fakeSsh([[/cat/, 'some noise\nHERMES_DASHBOARD_READY port=51234\n']])
const port = await scrapeReadyPort(ssh, logPath, { timeoutMs: 1000 })
assert.equal(port, 51234)
assert.ok(ssh.calls.every(call => !call.includes('desktop-ssh.log')))
})
test('scrapeReadyPort times out and reports a dead spawn', async () => {
// never emits a READY line
const ssh = fakeSsh([[/cat .*\.log/, 'still starting...']])
await assert.rejects(
() => scrapeReadyPort(ssh, spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE), { timeoutMs: 60 }),
(err: any) => {
assert.equal(err.kind, 'ready-timeout')
return true
}
)
// dead process before announcement → spawn-failed
await assert.rejects(
() =>
scrapeReadyPort(fakeSsh([[/cat/, '']]), spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE), {
timeoutMs: 1000,
isAlive: async () => false
}),
(err: any) => {
assert.equal(err.kind, 'spawn-failed')
return true
}
)
})
function connectDeps(ssh, over: any = {}) {
return {
ssh,
ownershipId: OWNERSHIP_ID,
profile: '',
forward: async () => {},
cancelForward: async () => {},
pickLocalPort: async () => 50001,
waitForHermes: async () => {},
probeReuseProof: async () => 'authenticated-ok',
adoptServedToken: async (_baseUrl, spawn) => spawn || 'served-token',
rememberLog: () => {},
readyTimeoutMs: 2000,
...over
}
}
test('connect() spawns fresh when there is no lockfile, adopts the served token', async () => {
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, ''], // no lockfile
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''], // token file write
[/printf '%s\\n'/, ''],
[/setsid/, '777\n'],
[/kill -0 777/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=51999\n']
])
const result = await connect(connectDeps(ssh, { adoptServedToken: async () => 'the-served-token' }))
assert.equal(result.reused, false)
assert.equal(result.remotePort, 51999)
assert.equal(result.localPort, 50001)
assert.equal(result.pid, 777)
assert.equal(result.token, 'the-served-token')
assert.equal(result.baseUrl, 'http://127.0.0.1:50001')
assert.equal(result.tokenFingerprint, fingerprintToken('the-served-token'))
})
test('managed SSH maps a local scope to a different non-default remote profile', async () => {
const localScope = 'work'
const sshConfig = profileSshOverride(
{
profiles: {
[localScope]: {
mode: 'ssh',
host: 'remote-box',
remoteProfile: 'writer_2'
}
}
},
localScope
)
assert.equal(sshConfig?.remoteProfile, 'writer_2')
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, ''],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/printf '%s\\n'/, ''],
[/setsid/, '778\n'],
[/kill -0 778/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_BACKEND_READY port=52000\n']
])
await connect(
connectDeps(ssh, {
profile: sshConfig?.remoteProfile,
adoptServedToken: async () => 'mapped-profile-token'
})
)
const spawn = ssh.calls.find(command => /setsid|nohup/.test(command)) || ''
assert.match(spawn, /--profile\b/)
assert.ok(spawn.includes('writer_2'))
assert.match(spawn, /serve\s+--isolated/)
assert.match(spawn, /\.hermes\/desktop-ssh\/[0-9a-f]{32}\/[0-9a-f]{16}\.token/)
assert.ok(!spawn.includes(' work'), 'the local Desktop scope must not become the remote profile')
})
test('connect() reuses a healthy dashboard when fingerprint + probe pass', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
const result = await connect(connectDeps(ssh, { reuseToken, adoptServedToken: async (_b, t) => t }))
assert.equal(result.reused, true)
assert.equal(result.pid, 333)
assert.equal(result.remotePort, 40000)
// never spawned
assert.ok(!ssh.calls.some(c => /setsid/.test(c)), 'reuse path must not spawn a new dashboard')
})
test('connect() respawns when the requested remote profile differs from the lockfile profile', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ profile: 'desktop-work', tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0 333/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n'],
[cmd => /pidfd_open/.test(cmd), 'TERMINATED\n'],
[/kill 333/, ''],
[/--version/, 'Hermes Agent v0.18.2\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '890\n'],
[/kill -0 890/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=52050\n']
])
const result = await connect(
connectDeps(ssh, { profile: 'default', reuseToken, adoptServedToken: async () => 'fresh' })
)
assert.equal(result.reused, false)
assert.ok(
ssh.calls.some(c => /setsid/.test(c)),
'profile mismatch must spawn a fresh dashboard'
)
})
test('connect() respawns when the lockfile hermesPath differs from the resolved path', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ hermesPath: '/old/stale/hermes', tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'FOREIGN\n'],
[/--version/, 'Hermes Agent v0.18.2\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '890\n'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=52050\n']
])
const result = await connect(
connectDeps(ssh, { reuseToken, remoteHermesPath: '/new/hermes', adoptServedToken: async () => 'fresh' })
)
assert.equal(result.reused, false, 'must respawn, not reuse the old-path dashboard')
assert.ok(
ssh.calls.some(c => /setsid/.test(c)),
'a fresh dashboard must be spawned'
)
})
test('connect() respawns when the lockfile protocolVersion is incompatible', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({
protocolVersion: PROTOCOL_VERSION + 99,
tokenFingerprint: fingerprintToken(reuseToken)
})
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0 333/, 'ALIVE'],
[/print\("OWNED"/, 'FOREIGN\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '901\n'],
[/kill -0 901/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=44100\n']
])
const result = await connect(connectDeps(ssh, { reuseToken, adoptServedToken: async () => 'fresh' }))
assert.equal(result.reused, false, 'incompatible protocol must force a fresh spawn, not a reattach')
assert.equal(result.pid, 901)
})
test('connect() fresh spawn writes hermesHome + protocolVersion into the lockfile', async () => {
const writes: string[] = []
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, ''], // no lockfile
[/HERMES_HOME/, '/home/alice/.hermes\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/printf '%s\\n'/, ''],
[/setsid/, '700\n'],
[/kill -0 700/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=45500\n'],
[
/printf '%s' '/,
c => {
writes.push(c)
return ''
}
]
])
await connect(connectDeps(ssh, { adoptServedToken: async () => 'fresh' }))
const lockWrite = writes.find(c => c.includes('schemaVersion')) || ''
assert.match(lockWrite, new RegExp(`"protocolVersion":${PROTOCOL_VERSION}`))
assert.match(lockWrite, /"hermesHome":"\/home\/alice\/\.hermes"/)
})
test('connect() respawns when the lockfile pid is dead (killed dashboard)', async () => {
const lock = ownedLock({ tokenFingerprint: fingerprintToken('t') })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0 333/, 'DEAD'],
[/print\("OWNED"/, 'FOREIGN\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '888\n'],
[/kill -0 888/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=42000\n']
])
const result = await connect(connectDeps(ssh, { reuseToken: 't', adoptServedToken: async () => 'fresh' }))
assert.equal(result.reused, false)
assert.equal(result.pid, 888)
assert.equal(result.remotePort, 42000)
assert.ok(
!ssh.calls.some(command => command.includes('pid=333') && command.includes('print("OWNED"')),
'a dead pid has no process identity to verify'
)
})
test('managed update drain preserves a live foreign POSIX owner and its lock bytes', async () => {
const lock = ownedLock()
const rawLock = JSON.stringify(lock)
const ssh = fakeSsh([
[/cat .*lock\.json/, rawLock],
[/kill -0 333/, 'ALIVE'],
[/value="linux:"/, 'linux:123456\n'],
[/print\("OWNED"/, 'FOREIGN\n']
])
await assert.rejects(terminateOwnedDashboardForUpdate(ssh, lock), /ownership is unproven/)
assert.equal(
ssh.calls.some(command => /kill 333 &&/.test(command)),
false,
'foreign process is never signalled'
)
assert.equal(
ssh.calls.some(command => /rm -f .*backend\.lock\.json/.test(command)),
false,
'foreign ownership bytes remain untouched'
)
})
test('managed update drain rechecks the POSIX ownership record before signalling', async () => {
const lock = ownedLock()
const replacement = ownedLock({ pid: 334, spawnNonce: 'fedcba9876543210' })
let reads = 0
const ssh = fakeSsh([
[
/cat .*lock\.json/,
() => {
reads += 1
return JSON.stringify(reads === 1 ? lock : replacement)
}
],
[/kill -0 333/, 'ALIVE'],
[/value="linux:"/, 'linux:123456\n'],
[/print\("OWNED"/, 'OWNED\n']
])
await assert.rejects(terminateOwnedDashboardForUpdate(ssh, lock), /changed during process verification/)
assert.equal(
ssh.calls.some(command => /kill 333 &&/.test(command)),
false
)
})
test('managed update drain refuses Darwin termination because PID signals cannot be atomically bound', async () => {
const lock = ownedLock()
const rawLock = JSON.stringify(lock)
const ssh = fakeSsh([
[/cat .*lock\.json/, rawLock],
[/kill -0 333/, 'ALIVE'],
[/value="linux:"/, 'linux:123456\n'],
[/print\("OWNED"/, 'OWNED\n'],
[/pidfd_open/, 'REFUSED\n']
])
await assert.rejects(terminateOwnedDashboardForUpdate(ssh, lock), /identity changed at the signal boundary/)
assert.equal(
ssh.calls.some(command => /^kill 333\b/.test(command.trim())),
false,
'the final signal must stay inside the identity-checking helper'
)
const termination = ssh.calls.find(command => command.includes('identity_before_signal'))
const darwinStart = termination.indexOf('if (sys.platform=="darwin"):')
const darwinEnd = termination.indexOf('\n try:', darwinStart)
const darwinGuard = termination.slice(darwinStart, darwinEnd)
assert.match(darwinGuard, /DARWIN_UNAVAILABLE/)
assert.doesNotMatch(darwinGuard, /os\.kill\(pid,signal\.SIGTERM\)/)
})
test('connect() respawns when the dashboard is wedged (alive pid, probe fails)', async () => {
const reuseToken = 'stored'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'FOREIGN\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '999\n'],
[/kill -0 999/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=43000\n']
])
const result = await connect(
connectDeps(ssh, {
reuseToken,
probeReuseProof: async () => 'authenticated-stale',
adoptServedToken: async () => 'fresh'
})
)
assert.equal(result.reused, false)
assert.equal(result.pid, 999)
assert.equal(result.remotePort, 43000)
})
test('connect() aborts on an unsupported remote platform before doing anything else', async () => {
const ssh = fakeSsh([[/uname/, 'SunOS\nsun4v']])
await assert.rejects(
() => connect(connectDeps(ssh)),
(err: any) => {
assert.equal(err.kind, 'unsupported-platform')
return true
}
)
assert.ok(!ssh.calls.some(c => /setsid/.test(c)))
})
test('openForward retries bind collisions only', async () => {
const ports = [41001, 41002]
const calls: number[] = []
const localPort = await openForward(
{
pickLocalPort: async () => ports.shift(),
forward: async port => {
calls.push(port)
if (calls.length === 1) {
throw new Error('bind: Address already in use')
}
}
},
9119
)
assert.equal(localPort, 41002)
assert.deepEqual(calls, [41001, 41002])
assert.equal(isForwardBindCollision(new Error('Permission denied')), false)
})
test('connect() preserves an owned backend when a reuse transport throws', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
await assert.rejects(
() =>
connect(
connectDeps(ssh, {
reuseToken,
forward: async () => {
throw new Error('network reset')
}
})
),
/network reset/
)
assert.ok(!ssh.calls.some(cmd => /kill 333\b/.test(cmd)))
})
test('validateRemotePath accepts absolute POSIX paths', () => {
assert.doesNotThrow(() => validateRemotePath('/usr/bin/hermes'))
assert.doesNotThrow(() => validateRemotePath('/home/user/.hermes/hermes-agent/venv/bin/hermes'))
})
test('validateRemotePath accepts ~/ prefix paths', () => {
assert.doesNotThrow(() => validateRemotePath('~/bin/hermes'))
assert.doesNotThrow(() => validateRemotePath('~/.hermes/logs/desktop-ssh.log'))
assert.doesNotThrow(() => validateRemotePath('~'))
})
test('validateRemotePath accepts paths with spaces and quotes', () => {
assert.doesNotThrow(() => validateRemotePath('/home/user/my project/hermes'))
assert.doesNotThrow(() => validateRemotePath("~/path with 'quotes'/file"))
assert.doesNotThrow(() => validateRemotePath('/path with "double quotes"/file'))
})
test('validateRemotePath rejects relative paths', () => {
assert.throws(() => validateRemotePath('hermes'), /absolute|relative/i)
assert.throws(() => validateRemotePath('./bin/hermes'), /absolute|relative/i)
assert.throws(() => validateRemotePath('../etc/passwd'), /absolute|relative/i)
})
test('validateRemotePath rejects NUL and newline', () => {
assert.throws(() => validateRemotePath('/usr/bin/hermes\x00'), /unsafe/i)
assert.throws(() => validateRemotePath('/usr/bin/hermes\n'), /unsafe/i)
assert.throws(() => validateRemotePath('/usr/bin/hermes\r'), /unsafe/i)
})
test('validateRemotePath preserves shell metacharacters as path data', () => {
for (const p of ['/usr/$(whoami)/hermes', '/usr/`id`/hermes', '/usr/a;b|c&d<e>f']) {
assert.doesNotThrow(() => validateRemotePath(p))
assert.match(expandRemotePath(p), /^'/)
}
})
test('expandRemotePath expands ~/ to "$HOME"/', () => {
const result = expandRemotePath('~/.hermes/logs/desktop-ssh.log')
assert.match(result, /\$HOME/)
assert.ok(!result.includes('eval'), 'must not use eval')
assert.ok(!result.includes('echo'), 'must not use echo for expansion')
})
test('expandRemotePath returns quoted absolute paths unchanged', () => {
const result = expandRemotePath('/usr/local/bin/hermes')
assert.ok(result.includes('/usr/local/bin/hermes'))
assert.ok(!result.includes('eval'))
})
test('expandRemotePath preserves spaces as data', () => {
const result = expandRemotePath('/home/user/my project/hermes')
assert.ok(result.includes('my project'), 'spaces must be preserved, not split')
})
test('buildSpawnCommand does not embed the token in the command string', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', { logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE) })
assert.ok(!cmd.includes('super_secret_token_value'), 'token must not appear in the spawn command')
assert.ok(!cmd.includes('HERMES_DASHBOARD_SESSION_TOKEN'), 'env var name must not appear')
})
test('buildSpawnCommand includes --ssh-session-token-file when tokenFilePath is provided', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', {
tokenFilePath: `~/.hermes/desktop-ssh/${OWNERSHIP_ID}/${SPAWN_NONCE}.token`,
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
spawnNonce: SPAWN_NONCE
})
assert.match(cmd, /--ssh-session-token-file/)
assert.match(cmd, /\.hermes\/desktop-ssh\//)
})
test('buildSpawnCommand always uses serve, never dashboard', () => {
const cmd = buildSpawnCommand('/x/hermes', '', { logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE) })
assert.match(cmd, /serve --isolated/)
assert.doesNotMatch(cmd, /\bdashboard\b/)
assert.doesNotMatch(cmd, /--skip-build/)
assert.doesNotMatch(cmd, /--no-open/)
})
test('buildSpawnCommand raises the SSH child file limit before execing Hermes', () => {
const cmd = buildSpawnCommand('/x/hermes', '', { logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE) })
assert.match(cmd, /ulimit -n 65536 2>\/dev\/null \|\| true; exec env HERMES_DESKTOP=1/)
assert.ok(cmd.indexOf('ulimit -n 65536') < cmd.indexOf('serve --isolated'))
})
test('buildSpawnCommand payload variables keep $HOME expandable (no double quoting)', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', {
hermesHome: '~/.hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
ownershipId: OWNERSHIP_ID,
reservationNonce: SPAWN_NONCE,
spawnNonce: SPAWN_NONCE,
tokenFilePath: spawnTokenPath(OWNERSHIP_ID, SPAWN_NONCE),
lockMetadata: { ownershipId: OWNERSHIP_ID, spawnNonce: SPAWN_NONCE }
})
// expandRemotePath() emits "$HOME"'/…' — a fragment the shell expands at
// assignment. Wrapping it in shq() again stores the quote characters in
// the variable, so mkdir "$reservation" creates (or fails on) a literal
// "$HOME" path and the reservation loop spins forever holding the mutex.
for (const name of ['reservation', 'lock', 'owner_file']) {
assert.match(cmd, new RegExp(`${name}="\\$HOME"`), `${name}= must start with an expandable "$HOME"`)
assert.doesNotMatch(cmd, new RegExp(`${name}='`), `${name}= must not be re-quoted`)
}
})
test('buildSpawnCommand lockfile publication is POSIX sh (no bash substitution)', () => {
const cmd = buildSpawnCommand('/x/hermes', 'work', {
hermesHome: '~/.hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE),
ownershipId: OWNERSHIP_ID,
reservationNonce: SPAWN_NONCE,
spawnNonce: SPAWN_NONCE,
tokenFilePath: spawnTokenPath(OWNERSHIP_ID, SPAWN_NONCE),
lockMetadata: { ownershipId: OWNERSHIP_ID, pid: '__PID__' }
})
// ${var//pat/rep} is bash-only; dash aborts the payload on it AFTER the
// serve was spawned, so the client sees an unknown failure, deletes the
// token file, and orphans the backend.
assert.doesNotMatch(cmd, /\$\{lock_json\/\//, 'must not use ${var//} substitution under sh')
assert.ok(cmd.includes('sed "s/__PID__/${child}/"'), 'pid substitution must use sed')
})
test('spawnRemoteDashboard removes a token file when upload reporting fails', async () => {
const failure = new Error('channel closed')
const ssh = fakeSsh([
[/grep -q ssh-session-token-file/, 'YES\n'],
[command => /python3 -c/.test(command) && !/rm -f/.test(command), failure],
[/rm -f/, '']
])
await assert.rejects(
() => spawnRemoteDashboard(ssh, { hermesPath: '/x/hermes', profile: '', token: 'tok', ownershipId: OWNERSHIP_ID }),
/channel closed/
)
assert.ok(ssh.calls.some(command => /rm -f .*\.token/.test(command)))
})
test('spawnRemoteDashboard streams the token over stdin, not argv/env', async () => {
const stdinCalls: string[] = []
const calls: string[] = []
const ssh = {
calls,
async exec(cmd, opts?) {
calls.push(cmd)
if (opts?.stdinData) {
stdinCalls.push(opts.stdinData)
}
if (/grep -q ssh-session-token-file/.test(cmd)) {
return 'YES\n'
}
if (/python3 -c/.test(cmd) && !/fcntl\.flock/.test(cmd)) {
return ''
}
if (/setsid|nohup/.test(cmd)) {
return '4242\n'
}
if (/printf '%s\\n'/.test(cmd)) {
return ''
}
return ''
}
}
const { pid } = await spawnRemoteDashboard(ssh as any, {
hermesPath: '/x/hermes',
profile: '',
token: 'secret_token_val',
ownershipId: OWNERSHIP_ID
})
assert.equal(pid, 4242)
assert.ok(stdinCalls.length > 0, 'token must be sent via stdin')
assert.ok(
stdinCalls.some(d => d === 'secret_token_val'),
'stdin must contain the token'
)
for (const cmd of calls) {
assert.ok(!cmd.includes('secret_token_val'), `token leaked into command: ${cmd}`)
}
})
test('spawnRemoteDashboard upload uses exclusive-create and O_NOFOLLOW', async () => {
const calls: string[] = []
const ssh = {
calls,
async exec(cmd, opts?) {
calls.push(cmd)
if (/grep -q ssh-session-token-file/.test(cmd)) {
return 'YES\n'
}
if (/python3 -c/.test(cmd) && !/fcntl\.flock/.test(cmd)) {
return ''
}
if (/setsid|nohup/.test(cmd)) {
return '4242\n'
}
if (/printf '%s\\n'/.test(cmd)) {
return ''
}
return ''
}
}
await spawnRemoteDashboard(ssh as any, {
hermesPath: '/x/hermes',
profile: '',
token: 'tk',
ownershipId: OWNERSHIP_ID
})
const uploadCmd = calls.find(c => /python3 -c/.test(c) && !/fcntl\.flock/.test(c))
assert.ok(uploadCmd, 'must use python3 -c for token upload')
assert.match(uploadCmd, /O_EXCL/, 'upload must use O_EXCL to reject existing files')
assert.match(uploadCmd, /O_NOFOLLOW/, 'upload must use O_NOFOLLOW to reject symlinks')
assert.match(uploadCmd, /O_WRONLY/, 'upload must open write-only')
assert.match(uploadCmd, /dir_fd=dd/, 'upload must create relative to the opened parent directory')
assert.match(uploadCmd, /os\.fstat\(dd\)/, 'upload must validate the opened parent directory')
assert.ok(!uploadCmd.includes('tk'), 'token must not appear in the upload command')
})
test('readLockfile treats a lock with non-integer pid as skew', async () => {
const lock = { schemaVersion: LOCKFILE_SCHEMA_VERSION, pid: 'not-a-number', port: 8080 }
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, JSON.stringify(lock)]]), OWNERSHIP_ID)), true)
})
test('readLockfile treats a lock with pid <= 0 as skew', async () => {
const lock = { schemaVersion: LOCKFILE_SCHEMA_VERSION, pid: -1, port: 8080 }
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, JSON.stringify(lock)]]), OWNERSHIP_ID)), true)
})
test('readLockfile treats a lock with port out of range as skew', async () => {
const lock = { schemaVersion: LOCKFILE_SCHEMA_VERSION, pid: 100, port: 99999 }
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, JSON.stringify(lock)]]), OWNERSHIP_ID)), true)
const lock2 = { schemaVersion: LOCKFILE_SCHEMA_VERSION, pid: 100, port: 0 }
assert.equal(isLockfileSkew(await readLockfile(fakeSsh([[/cat/, JSON.stringify(lock2)]]), OWNERSHIP_ID)), true)
})
test('readLockfile accepts a complete owned lock', async () => {
const lock = ownedLock({ pid: 42, port: 51234 })
const result = await readLockfile(fakeSsh([[/cat/, JSON.stringify(lock)]]), OWNERSHIP_ID)
assert.deepEqual(result, lock)
})
test('connect() reuse path does not write a token file', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
const result = await connect(connectDeps(ssh, { reuseToken, adoptServedToken: async (_b, t) => t }))
assert.equal(result.reused, true)
assert.ok(!ssh.calls.some(c => /sys\.stdin\.buffer\.read/.test(c)), 'reuse must not upload a token file')
})
test('spawnRemoteDashboard fails with update-required when remote lacks --ssh-session-token-file', async () => {
const ssh = fakeSsh([[/--ssh-session-token-file/, 'NO\n']])
await assert.rejects(
() => spawnRemoteDashboard(ssh, { hermesPath: '/x/hermes', profile: '', token: 'tk', ownershipId: OWNERSHIP_ID }),
(err: any) => {
assert.match(err.message, /update|upgrade/i)
assert.equal(err.kind, 'update-required')
return true
}
)
})
test('readLockfile treats a log path outside the exact ownership and spawn path as skew', async () => {
const lock = ownedLock({ logPath: '~/.hermes/desktop-ssh/other.log' })
const ssh = fakeSsh([[/cat .*lock\.json/, JSON.stringify(lock)]])
assert.equal(isLockfileSkew(await readLockfile(ssh, OWNERSHIP_ID)), true)
})
test('cleanupStale never deletes a lock-supplied unexpected log path', async () => {
const ssh = fakeSsh([
[/print\("OWNED"/, 'OWNED\n'],
[cmd => /pidfd_open/.test(cmd), 'TERMINATED\n']
])
await cleanupStale(ssh, OWNERSHIP_ID, ownedLock({ logPath: '~/.hermes/unrelated.log' }))
assert.ok(!ssh.calls.some(command => command.includes('unrelated.log')))
})
test('pidIsOurDashboard requires an exact nonce option value', async () => {
const prefix = `/x/hermes serve --isolated --ssh-owner-nonce ${SPAWN_NONCE}ff`
const suffix = `/x/hermes serve --isolated --ssh-owner-nonce xx${SPAWN_NONCE}`
assert.equal(await pidIsOurDashboard(fakeSsh([[/print\("OWNED"/, 'FOREIGN\n']]), 5, SPAWN_NONCE, '/x/hermes'), false)
assert.equal(await pidIsOurDashboard(fakeSsh([[/print\("OWNED"/, 'FOREIGN\n']]), 5, SPAWN_NONCE, '/x/hermes'), false)
})
test('connect removes the token file when a fresh backend fails after returning a pid', async () => {
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, ''],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '999\n'],
[/kill -0 999/, 'DEAD']
])
await assert.rejects(() => connect(connectDeps(ssh)), /exited before announcing/i)
assert.ok(ssh.calls.some(command => /rm -f .*\.token/.test(command)))
})
test('connect preserves an exact-owned backend when reuse proof transport fails', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n']
])
await assert.rejects(
() =>
connect(
connectDeps(ssh, {
reuseToken,
probeReuseProof: async () => {
throw new Error('connection reset')
}
})
),
(error: any) => error.kind === 'transient-transport-error'
)
assert.ok(!ssh.calls.some(command => /kill 333\b/.test(command)))
assert.ok(!ssh.calls.some(command => /rm -f .*backend\.lock\.json/.test(command)))
})
test('connect replaces an exact-owned backend only after authenticated stale proof', async () => {
const reuseToken = 'stored-token'
const lock = ownedLock({ tokenFingerprint: fingerprintToken(reuseToken) })
const ssh = fakeSsh([
[/uname/, 'Linux\nx86_64'],
[/\[ -x/, 'OK'],
[/cat .*lock\.json/, JSON.stringify(lock)],
[/kill -0 333/, 'ALIVE'],
[/print\("OWNED"/, 'OWNED\n'],
[cmd => /pidfd_open/.test(cmd), 'TERMINATED\n'],
[/grep -q ssh-session-token-file/, 'YES\n'],
[/python3 -c/, ''],
[/setsid/, '999\n'],
[/kill -0 999/, 'ALIVE'],
[/cat .*\.log/, 'HERMES_DASHBOARD_READY port=43000\n']
])
const result = await connect(
connectDeps(ssh, {
reuseToken,
probeReuseProof: async (_baseUrl, token, nonce) => {
assert.equal(token, reuseToken)
assert.equal(nonce, SPAWN_NONCE)
return 'authenticated-stale'
},
adoptServedToken: async () => 'fresh'
})
)
assert.equal(result.reused, false)
// The kill goes through main's cleanupStale (ownership-proved SIGTERM with
// SIGKILL escalation, #91668) — the PR's python re-proof command shape is
// used by the managed-update path (terminateOwnedDashboardForUpdate), not
// by connect's stale replacement. Assert the CONTRACT: the owned pid was
// signalled and the record reclaimed.
assert.ok(ssh.calls.some(command => /kill 333\b/.test(command)))
assert.ok(ssh.calls.some(command => /rm -f .*backend\.lock\.json/.test(command)))
})
test('remote SSH ownership capability requires both secure bootstrap flags', async () => {
let helpProbe = ''
const supported = fakeSsh([
[
/serve --help/,
command => {
helpProbe = command
return 'YES\n'
}
]
])
assert.equal(await remoteSupportsSshOwnership(supported, '/x/hermes'), true)
assert.match(helpProbe, /ssh-session-token-file/)
assert.match(helpProbe, /ssh-owner-nonce/)
const unsupported = fakeSsh([[/serve --help/, 'NO\n']])
assert.equal(await remoteSupportsSshOwnership(unsupported, '/x/hermes'), false)
})
test('cleanupStale escalates to SIGKILL when the backend survives the graceful wait (#91668 quit-during-active-turn)', async () => {
// A serve mid-turn (in-flight LLM call, live MCP children) can ride out
// SIGTERM well past the 5s graceful wait. Before-quit races the whole
// teardown against 6s and then closes SSH — so a give-up here reparents
// the still-running backend to pid 1: exactly the #91668 leak. The
// graceful-wait failure must escalate to SIGKILL and still drop the lock.
const ssh = fakeSsh([
[/print\("OWNED"/, 'OWNED\n'],
[(cmd: string) => /kill 9 &&/.test(cmd), new Error('exit 1: pid alive after graceful wait')]
])
await cleanupStale(ssh, OWNERSHIP_ID, {
pid: 9,
spawnNonce: SPAWN_NONCE,
hermesPath: '/x/hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE)
})
assert.ok(
ssh.calls.some(c => /kill -9 9\b/.test(c)),
'must escalate to SIGKILL after the graceful wait fails'
)
assert.ok(
ssh.calls.some(c => /rm -f .*backend\.lock\.json/.test(c)),
'lockfile must still be dropped after the forced kill'
)
})
test('cleanupStale keeps the lockfile when even SIGKILL cannot confirm the pid died', async () => {
const ssh = fakeSsh([
[/print\("OWNED"/, 'OWNED\n'],
[(cmd: string) => /kill 9 &&/.test(cmd), new Error('exit 1: pid alive after graceful wait')],
[(cmd: string) => /kill -9 9\b/.test(cmd), new Error('exit 1: unkillable (D-state)')]
])
await assert.rejects(
cleanupStale(ssh, OWNERSHIP_ID, {
pid: 9,
spawnNonce: SPAWN_NONCE,
hermesPath: '/x/hermes',
logPath: spawnLogPath(OWNERSHIP_ID, SPAWN_NONCE)
}),
/Could not terminate/
)
// The record must survive so the next connect's reap pass retries.
assert.ok(!ssh.calls.some(c => /rm -f .*backend\.lock\.json/.test(c)))
})