Files
hermes-agent/scripts/smoke-payload.sh
teknium1 8a3ede1be0 fix(bundle): payload smoke runs the harness with telemetry off, like runtime
browser-harness sends each CLI event from a detached python child; in the
smoke it outlived the check and held the relocated payload open on Windows,
so the restore rename failed with EPERM. Hermes's own harness env sets
ANONYMIZED_TELEMETRY=false; the smoke now does the same.
2026-09-27 23:53:40 -07:00

66 lines
3.5 KiB
Bash

#!/usr/bin/env bash
# Exercise the published launch contract after relocation. Network isolation is
# the runner's responsibility (Linux CI uses unshare --net), not a lazy-deps flag.
set -euo pipefail
PAYLOAD="${1:?usage: smoke-payload.sh <payload-dir>}"
command -v cygpath >/dev/null 2>&1 && PAYLOAD="$(cygpath -w "$PAYLOAD")"
node --input-type=module - "$PAYLOAD" <<'JS'
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs'
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { spawnSync } from 'node:child_process'
const original = resolve(process.argv[2])
// Same filesystem: moving a multi-GB payload should not copy it.
const scratch = mkdtempSync(join(dirname(original), '.payload-smoke-'))
const moved = join(scratch, 'relocated payload')
let relocated = false
try {
renameSync(original, moved)
relocated = true
const manifest = JSON.parse(readFileSync(join(moved, 'manifest.json'), 'utf8'))
const command = manifest.runtime.commands.hermes
if (!command) throw new Error('manifest has no hermes command')
const executable = realpathSync(resolve(moved, command))
const inside = relative(realpathSync(moved), executable)
if (isAbsolute(command) || isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`)) {
throw new Error('manifest command escapes the relocated payload')
}
const home = join(scratch, 'home')
mkdirSync(home)
const env = { HOME: home, USERPROFILE: home, HERMES_HOME: join(home, '.hermes'),
PYTHONUTF8: '1', PYTHONDONTWRITEBYTECODE: '1', HERMES_DISABLE_LAZY_INSTALLS: '1',
UV_OFFLINE: '1', npm_config_offline: 'true' }
// Keep OS process necessities only; no checkout, Python, PM, or Node overrides.
for (const [key, value] of Object.entries(process.env)) {
if (/^(path|systemroot|windir|comspec|pathext|temp|tmp)$/i.test(key)) env[key] = value
}
// tools list crosses the real application/config/registry imports, unlike
// version/help and PM's stdlib bootstrap fast paths.
for (const args of [['--version'], ['--help'], ['tools', 'list'], ['pm', 'doctor']]) {
console.log(`— published hermes ${args.join(' ')} (relocated) —`)
const child = spawnSync(executable, args, {
cwd: home, env, stdio: 'inherit', timeout: 120000,
})
if (child.error) throw child.error
if (child.status !== 0) throw new Error(`hermes ${args.join(' ')} failed: ${child.status ?? child.signal}`)
}
// browser_exec's engine (browser-harness) runs on the store interpreter with only the
// payload's site dir on PYTHONPATH (tools/browser_use_cli.py); -S drops any site of its own.
// Telemetry off as at runtime: its detached sender would outlive the check and pin the
// payload dir on Windows (EPERM on the restore rename).
console.log('— browser-harness on the store interpreter (relocated) —')
const harness = spawnSync(resolve(moved, manifest.runtime.storePython),
['-S', '-m', 'browser_harness.run', '--version'], {
cwd: home,
env: { ...env, ANONYMIZED_TELEMETRY: 'false', PYTHONPATH: resolve(moved, manifest.runtime.sitePackages) },
stdio: 'inherit', timeout: 120000,
})
if (harness.error) throw harness.error
if (harness.status !== 0) throw new Error(`browser-harness failed: ${harness.status ?? harness.signal}`)
console.log('SMOKE OK')
} finally {
// Restore even after a command fails, so its artifact remains inspectable.
if (relocated) renameSync(moved, original)
rmSync(scratch, { recursive: true, force: true })
}
JS