mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
Follow-up to the salvaged #66803 (@HexLab98): - Two-strike boot marker: a single mid-boot abort (task-manager kill, power loss) no longer disables the sandbox — only a second consecutive abort, or a signature-confirmed GPU/renderer STATUS_BREAKPOINT death, engages --no-sandbox. - Version-scoped stickiness: the fallback marker records the app version and re-probes the sandbox once after an update (new Electron or installer ACL repair may have fixed the host) instead of degrading forever. A failed re-probe returns straight to fallback. - Launch-time icacls repair now runs only when the marker shows a prior aborted boot (icacls /T recurses the whole install tree — healthy launches skip it; the installer grants the ACE at install time), and targets the install dir only. The userData grant is dropped: granting S-1-15-2-2 RX on userData would expose Hermes sessions/config to every AppContainer app on the machine. - Renderer crash-loop recovery (same class as #56726, credit @Sahil-SS9 in PR #57414): a Windows renderer crash loop bearing the breakpoint exit code gets the same one-shot --no-sandbox relaunch instead of a dead window; unrelated crash loops keep the sandbox. - Manual --no-sandbox launches are honored but never made sticky. Tests: 15/15 windows-sandbox-fallback vitest; full desktop electron suite 432 passed / 1 skipped.
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { test } from 'vitest'
|
|
|
|
import {
|
|
ALL_APPLICATION_PACKAGES_SID,
|
|
alreadyHasNoSandbox,
|
|
BOOT_ABORTS_BEFORE_FALLBACK,
|
|
buildIcaclsGrantArgs,
|
|
buildNoSandboxRelaunchArgs,
|
|
decideWindowsSandboxLaunch,
|
|
fallbackMarker,
|
|
grantAllApplicationPackagesAcl,
|
|
isWindowsSandboxBreakpointExit,
|
|
markerAfterSuccessfulBoot,
|
|
parseSandboxMarker,
|
|
readSandboxMarker,
|
|
sandboxMarkerPath,
|
|
shouldAttemptAclRepair,
|
|
shouldRelaunchForGpuSandboxCrash,
|
|
shouldRelaunchForRendererSandboxCrashLoop,
|
|
WINDOWS_SANDBOX_BREAKPOINT_EXIT,
|
|
WINDOWS_SANDBOX_MARKER_FILENAME,
|
|
writeSandboxMarker
|
|
} from './windows-sandbox-fallback'
|
|
|
|
test('isWindowsSandboxBreakpointExit recognizes signed and unsigned STATUS_BREAKPOINT', () => {
|
|
assert.equal(isWindowsSandboxBreakpointExit(WINDOWS_SANDBOX_BREAKPOINT_EXIT), true)
|
|
assert.equal(isWindowsSandboxBreakpointExit(-2147483645), true)
|
|
assert.equal(isWindowsSandboxBreakpointExit(0x80000003), true)
|
|
assert.equal(isWindowsSandboxBreakpointExit(1), false)
|
|
assert.equal(isWindowsSandboxBreakpointExit('nope'), false)
|
|
})
|
|
|
|
test('alreadyHasNoSandbox honors argv and ELECTRON_DISABLE_SANDBOX', () => {
|
|
assert.equal(alreadyHasNoSandbox(['--foo', '--no-sandbox'], {}), true)
|
|
assert.equal(alreadyHasNoSandbox([], { ELECTRON_DISABLE_SANDBOX: '1' }), true)
|
|
assert.equal(alreadyHasNoSandbox([], { ELECTRON_DISABLE_SANDBOX: 'true' }), true)
|
|
assert.equal(alreadyHasNoSandbox(['--disable-gpu'], {}), false)
|
|
})
|
|
|
|
test('decideWindowsSandboxLaunch stays off outside Windows and on clean markers', () => {
|
|
assert.equal(decideWindowsSandboxLaunch({ platform: 'linux', marker: { state: 'booting' } }).enable, false)
|
|
|
|
const cleanOk = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'ok' },
|
|
argv: [],
|
|
env: {}
|
|
})
|
|
|
|
assert.equal(cleanOk.enable, false)
|
|
assert.deepEqual(cleanOk.nextMarker, { state: 'booting' })
|
|
|
|
const noMarker = decideWindowsSandboxLaunch({ platform: 'win32', marker: null, argv: [], env: {} })
|
|
assert.equal(noMarker.enable, false)
|
|
assert.deepEqual(noMarker.nextMarker, { state: 'booting' })
|
|
})
|
|
|
|
test('a single mid-boot abort does NOT drop the sandbox (two-strike rule)', () => {
|
|
// First abort: prior launch left `booting` with no abort count. Could be a
|
|
// task-manager kill or power loss — sandbox stays ON, strike recorded.
|
|
const first = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'booting' },
|
|
argv: [],
|
|
env: {}
|
|
})
|
|
|
|
assert.equal(first.enable, false)
|
|
assert.deepEqual(first.nextMarker, { state: 'booting', bootAborts: 1 })
|
|
|
|
// Second consecutive abort: deterministic crash loop → fallback engages.
|
|
const second = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: first.nextMarker,
|
|
argv: [],
|
|
env: {},
|
|
appVersion: '1.2.3'
|
|
})
|
|
|
|
assert.equal(second.enable, true)
|
|
assert.equal(second.reason, 'boot-loop')
|
|
assert.deepEqual(second.nextMarker, { state: 'fallback', reason: 'boot-loop', version: '1.2.3' })
|
|
|
|
assert.equal(BOOT_ABORTS_BEFORE_FALLBACK, 2)
|
|
})
|
|
|
|
test('sticky fallback persists within one app version', () => {
|
|
const decision = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'fallback', reason: 'gpu-breakpoint', version: '1.2.3' },
|
|
argv: [],
|
|
env: {},
|
|
appVersion: '1.2.3'
|
|
})
|
|
|
|
assert.equal(decision.enable, true)
|
|
assert.equal(decision.reason, 'sticky-fallback')
|
|
assert.equal(decision.nextMarker.state, 'fallback')
|
|
assert.equal(decision.nextMarker.reason, 'gpu-breakpoint')
|
|
})
|
|
|
|
test('an app update re-probes the sandbox once instead of degrading forever', () => {
|
|
// Version changed since the fallback engaged → probe with sandbox ON.
|
|
const reprobe = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'fallback', reason: 'boot-loop', version: '1.2.3' },
|
|
argv: [],
|
|
env: {},
|
|
appVersion: '1.3.0'
|
|
})
|
|
|
|
assert.equal(reprobe.enable, false)
|
|
assert.equal(reprobe.nextMarker.state, 'booting')
|
|
assert.equal(reprobe.nextMarker.reprobe, true)
|
|
|
|
// The re-probe boot aborted → straight back to fallback, no second strike.
|
|
const failedReprobe = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: reprobe.nextMarker,
|
|
argv: [],
|
|
env: {},
|
|
appVersion: '1.3.0'
|
|
})
|
|
|
|
assert.equal(failedReprobe.enable, true)
|
|
assert.equal(failedReprobe.reason, 'reprobe-failed')
|
|
assert.equal(failedReprobe.nextMarker.state, 'fallback')
|
|
assert.equal(failedReprobe.nextMarker.version, '1.3.0')
|
|
|
|
// A legacy fallback marker without a version stays sticky (no re-probe).
|
|
const legacy = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'fallback' },
|
|
argv: [],
|
|
env: {},
|
|
appVersion: '1.3.0'
|
|
})
|
|
|
|
assert.equal(legacy.enable, true)
|
|
assert.equal(legacy.reason, 'sticky-fallback')
|
|
})
|
|
|
|
test('manual --no-sandbox is honored but never made sticky', () => {
|
|
const manual = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'ok' },
|
|
argv: ['--no-sandbox'],
|
|
env: {}
|
|
})
|
|
|
|
assert.equal(manual.enable, true)
|
|
assert.equal(manual.reason, 'already-enabled')
|
|
assert.equal(manual.nextMarker.state, 'booting')
|
|
|
|
// But a relaunch-written fallback marker is preserved through the flagged boot.
|
|
const relaunched = decideWindowsSandboxLaunch({
|
|
platform: 'win32',
|
|
marker: { state: 'fallback', reason: 'gpu-breakpoint', version: '1.2.3' },
|
|
argv: ['--no-sandbox'],
|
|
env: {},
|
|
appVersion: '1.2.3'
|
|
})
|
|
|
|
assert.equal(relaunched.enable, true)
|
|
assert.equal(relaunched.nextMarker.state, 'fallback')
|
|
})
|
|
|
|
test('marker transitions after a successful boot', () => {
|
|
assert.deepEqual(markerAfterSuccessfulBoot({ fallbackActive: false }), { state: 'ok' })
|
|
assert.deepEqual(markerAfterSuccessfulBoot({ fallbackActive: true, reason: 'gpu-breakpoint', appVersion: '1.2.3' }), {
|
|
state: 'fallback',
|
|
reason: 'gpu-breakpoint',
|
|
version: '1.2.3'
|
|
})
|
|
})
|
|
|
|
test('shouldAttemptAclRepair only fires on evidence of trouble', () => {
|
|
assert.equal(shouldAttemptAclRepair(null), false)
|
|
assert.equal(shouldAttemptAclRepair({ state: 'ok' }), false)
|
|
assert.equal(shouldAttemptAclRepair({ state: 'booting' }), true)
|
|
assert.equal(shouldAttemptAclRepair({ state: 'fallback' }), true)
|
|
})
|
|
|
|
test('sandbox marker round-trips through the userData file', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-sandbox-marker-'))
|
|
|
|
try {
|
|
assert.equal(sandboxMarkerPath(dir), path.join(dir, WINDOWS_SANDBOX_MARKER_FILENAME))
|
|
assert.equal(readSandboxMarker(dir), null)
|
|
|
|
writeSandboxMarker(dir, { state: 'booting', bootAborts: 1 })
|
|
assert.deepEqual(readSandboxMarker(dir), { state: 'booting', bootAborts: 1 })
|
|
|
|
writeSandboxMarker(dir, fallbackMarker('renderer-crash-loop', '1.2.3'))
|
|
assert.deepEqual(readSandboxMarker(dir), {
|
|
state: 'fallback',
|
|
reason: 'renderer-crash-loop',
|
|
version: '1.2.3'
|
|
})
|
|
|
|
assert.equal(parseSandboxMarker({ state: 'fallback' })?.state, 'fallback')
|
|
assert.equal(parseSandboxMarker({ state: 'nope' }), null)
|
|
// Unknown reason strings and junk fields are dropped, not fatal.
|
|
assert.deepEqual(parseSandboxMarker({ state: 'fallback', reason: 'weird', bootAborts: -3 }), {
|
|
state: 'fallback'
|
|
})
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('buildIcaclsGrantArgs targets ALL APPLICATION PACKAGES with inherited RX', () => {
|
|
assert.deepEqual(buildIcaclsGrantArgs('C:\\Hermes\\win-unpacked'), [
|
|
'C:\\Hermes\\win-unpacked',
|
|
'/grant',
|
|
`*${ALL_APPLICATION_PACKAGES_SID}:(OI)(CI)(RX)`,
|
|
'/T',
|
|
'/C',
|
|
'/Q'
|
|
])
|
|
})
|
|
|
|
test('grantAllApplicationPackagesAcl is a no-op off Windows and reports exec failures', () => {
|
|
assert.deepEqual(grantAllApplicationPackagesAcl('C:\\x', { platform: 'darwin' }), { ok: false })
|
|
|
|
const calls: Array<{ file: string; args: readonly string[] }> = []
|
|
|
|
const ok = grantAllApplicationPackagesAcl('C:\\Hermes', {
|
|
platform: 'win32',
|
|
execFileSync(file, args) {
|
|
calls.push({ file, args })
|
|
|
|
return Buffer.alloc(0)
|
|
}
|
|
})
|
|
|
|
assert.deepEqual(ok, { ok: true })
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0]?.file, 'icacls')
|
|
assert.deepEqual(calls[0]?.args, buildIcaclsGrantArgs('C:\\Hermes'))
|
|
|
|
const failed = grantAllApplicationPackagesAcl('C:\\Hermes', {
|
|
platform: 'win32',
|
|
execFileSync() {
|
|
throw new Error('access denied')
|
|
}
|
|
})
|
|
|
|
assert.equal(failed.ok, false)
|
|
assert.match(String(failed.error), /access denied/)
|
|
})
|
|
|
|
test('shouldRelaunchForGpuSandboxCrash only fires once for GPU breakpoint deaths', () => {
|
|
assert.equal(
|
|
shouldRelaunchForGpuSandboxCrash({
|
|
platform: 'win32',
|
|
details: { type: 'GPU', exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT },
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
true
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForGpuSandboxCrash({
|
|
platform: 'win32',
|
|
details: { type: 'GPU', exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT },
|
|
alreadyNoSandbox: true,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForGpuSandboxCrash({
|
|
platform: 'win32',
|
|
details: { type: 'GPU', exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT },
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: true
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForGpuSandboxCrash({
|
|
platform: 'win32',
|
|
details: { type: 'renderer', exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT },
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForGpuSandboxCrash({
|
|
platform: 'linux',
|
|
details: { type: 'GPU', exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT },
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
})
|
|
|
|
test('renderer crash-loop relaunch requires the sandbox breakpoint signature', () => {
|
|
assert.equal(
|
|
shouldRelaunchForRendererSandboxCrashLoop({
|
|
platform: 'win32',
|
|
reason: 'crashed',
|
|
exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT,
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
true
|
|
)
|
|
// Unrelated renderer crash loops (plain crash, OOM churn) keep the sandbox.
|
|
assert.equal(
|
|
shouldRelaunchForRendererSandboxCrashLoop({
|
|
platform: 'win32',
|
|
reason: 'crashed',
|
|
exitCode: 1,
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForRendererSandboxCrashLoop({
|
|
platform: 'win32',
|
|
reason: 'oom',
|
|
exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT,
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForRendererSandboxCrashLoop({
|
|
platform: 'win32',
|
|
reason: 'crashed',
|
|
exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT,
|
|
alreadyNoSandbox: true,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
assert.equal(
|
|
shouldRelaunchForRendererSandboxCrashLoop({
|
|
platform: 'linux',
|
|
reason: 'crashed',
|
|
exitCode: WINDOWS_SANDBOX_BREAKPOINT_EXIT,
|
|
alreadyNoSandbox: false,
|
|
relaunchAttempted: false
|
|
}),
|
|
false
|
|
)
|
|
})
|
|
|
|
test('buildNoSandboxRelaunchArgs appends a single --no-sandbox flag', () => {
|
|
assert.deepEqual(buildNoSandboxRelaunchArgs(['--foo', '--no-sandbox', 'hermes://x']), [
|
|
'--foo',
|
|
'hermes://x',
|
|
'--no-sandbox'
|
|
])
|
|
})
|