diff --git a/apps/desktop/electron/active-runtime-state.test.ts b/apps/desktop/electron/active-runtime-state.test.ts new file mode 100644 index 000000000000..afc3e74b9d4d --- /dev/null +++ b/apps/desktop/electron/active-runtime-state.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' + +import { test } from 'vitest' + +import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state' + +const VALID_MARKER = { + pinnedCommit: '1234567890abcdef1234567890abcdef12345678', + schemaVersion: 1 +} + +test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => { + assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true) +}) + +test('hasValidBootstrapMarker rejects missing, wrong-schema, and too-short markers', () => { + assert.equal(hasValidBootstrapMarker(null, 1), false) + assert.equal(hasValidBootstrapMarker({ schemaVersion: 2, pinnedCommit: VALID_MARKER.pinnedCommit }, 1), false) + assert.equal(hasValidBootstrapMarker({ schemaVersion: 1, pinnedCommit: 'abc123' }, 1), false) +}) + +test('classifyActiveRuntime uses a healthy active runtime even when the bootstrap marker is missing', () => { + assert.deepEqual(classifyActiveRuntime(null, 1, true), { + hasValidMarker: false, + shouldUseActiveRuntime: true, + usabilityReason: 'usable' + }) +}) + +test('classifyActiveRuntime uses a healthy active runtime even when the marker is stale or malformed', () => { + assert.deepEqual(classifyActiveRuntime({ schemaVersion: 999, pinnedCommit: 'abc1234' }, 1, true), { + hasValidMarker: false, + shouldUseActiveRuntime: true, + usabilityReason: 'usable' + }) +}) + +test('classifyActiveRuntime refuses an unusable runtime even if a valid marker exists', () => { + assert.deepEqual(classifyActiveRuntime(VALID_MARKER, 1, false), { + hasValidMarker: true, + shouldUseActiveRuntime: false, + usabilityReason: 'unusable' + }) +}) + +test('a CLI-installed runtime with no marker launches instead of re-running bootstrap', () => { + // The reported symptom (#60721): install.sh / install.ps1 produced a healthy + // repo+venv, no desktop-managed marker was ever written, and every launch + // dropped the user back into the first-run installer. + const state = classifyActiveRuntime(null, 1, true) + + assert.equal(state.shouldUseActiveRuntime, true, 'a usable runtime must launch') + assert.equal(state.hasValidMarker, false, 'marker provenance stays honest') +}) + +test('a repair that deleted the marker does not strand a healthy install', () => { + // #72166: the repair handler clears the marker unconditionally. Runtime + // usability, not marker presence, must decide the next boot. + assert.equal(classifyActiveRuntime(null, 1, true).shouldUseActiveRuntime, true) +}) diff --git a/apps/desktop/electron/active-runtime-state.ts b/apps/desktop/electron/active-runtime-state.ts new file mode 100644 index 000000000000..6d922a462778 --- /dev/null +++ b/apps/desktop/electron/active-runtime-state.ts @@ -0,0 +1,58 @@ +export interface BootstrapMarkerLike { + pinnedCommit?: unknown + schemaVersion?: unknown +} + +export interface ActiveRuntimeState { + hasValidMarker: boolean + shouldUseActiveRuntime: boolean + usabilityReason: 'usable' | 'unusable' +} + +export function hasValidBootstrapMarker( + marker: BootstrapMarkerLike | null | undefined, + schemaVersion: number +): boolean { + if (!marker || typeof marker !== 'object') { + return false + } + + if (marker.schemaVersion !== schemaVersion) { + return false + } + + if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) { + return false + } + + return true +} + +// The active install at ~/.hermes/hermes-agent can be real and runnable even if +// Desktop never wrote its first-run bootstrap marker (for example when Hermes +// was installed by the CLI first, or when a past desktop build forgot the +// marker). Runtime usability is authoritative for "can we launch local Hermes +// right now?"; the marker is only provenance about how that install was +// created. A missing/stale marker must never force a healthy local install into +// the first-run bootstrap UI. +export function classifyActiveRuntime( + marker: BootstrapMarkerLike | null | undefined, + schemaVersion: number, + runtimeUsable: boolean +): ActiveRuntimeState { + const hasValidMarker = hasValidBootstrapMarker(marker, schemaVersion) + + if (!runtimeUsable) { + return { + hasValidMarker, + shouldUseActiveRuntime: false, + usabilityReason: 'unusable' + } + } + + return { + hasValidMarker, + shouldUseActiveRuntime: true, + usabilityReason: 'usable' + } +} diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index f967aa24c3b8..4ccd6c74e96b 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -30,6 +30,7 @@ import { } from 'electron' import nodePty from 'node-pty' +import { classifyActiveRuntime } from './active-runtime-state' import { stopBackendChild as stopBackendChildImpl } from './backend-child' import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command' import { createBackendConnectionState } from './backend-connection-state' @@ -3383,11 +3384,11 @@ function readJson(filePath) { } } -// Bootstrap-complete marker helpers. The marker is written ONCE by the -// first-launch bootstrap runner (Phase 1D) after install.ps1 stages succeed -// AND the user has finished initial configuration. On every subsequent boot -// we check `isBootstrapComplete()` and skip the bootstrap flow entirely if -// the marker is present and current-schema. +// Bootstrap-complete marker helpers. The marker is written by whichever +// installer ran: install.ps1, install.sh, the Rust bootstrap installer, or the +// first-launch bootstrap runner. It is provenance ("a bootstrap finished +// here"), NOT the launch gate -- activeRuntimeState() decides that, because a +// healthy runtime can predate the marker or outlive a repair that cleared it. // // Marker schema (version 1): // { @@ -3420,29 +3421,13 @@ function isActiveRuntimeUsable() { ) } -function isBootstrapComplete() { - const marker = readBootstrapMarker() - - if (!marker || typeof marker !== 'object') { - return false - } - - if (marker.schemaVersion !== BOOTSTRAP_MARKER_SCHEMA_VERSION) { - return false - } - - if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) { - return false - } - +function activeRuntimeState() { // We DELIBERATELY do NOT verify that the checkout is currently at the // pinned commit -- users update via the in-app update path or `hermes - // update`, which moves HEAD legitimately. The marker just attests "we - // ran the bootstrap successfully at least once." We DO additionally require - // a runnable venv: an interrupted or split-home install can leave the marker - // + checkout without a venv, and trusting that spawns a dead backend - // ("gateway offline") instead of re-running bootstrap to repair it. - return isActiveRuntimeUsable() + // update`, which moves HEAD legitimately. The marker only attests "a + // desktop-managed bootstrap ran here at least once"; runtime usability is + // what decides whether we can actually launch. + return classifyActiveRuntime(readBootstrapMarker(), BOOTSTRAP_MARKER_SCHEMA_VERSION, isActiveRuntimeUsable()) } function writeBootstrapMarker(payload) { @@ -3702,13 +3687,23 @@ function resolveHermesBackend(backendArgs) { } } - // 3. Bootstrap-complete ACTIVE_HERMES_ROOT -- the canonical install at - // %LOCALAPPDATA%\hermes\hermes-agent (Windows) or ~/.hermes/hermes-agent. - // The bootstrap marker means install.ps1 stages finished and the user - // completed initial configuration; we trust the install and go straight - // to spawning hermes. Updates flow through the in-app update path - // (applyUpdates -> git pull) or `hermes update` from the CLI. - if (isBootstrapComplete()) { + // 3. ACTIVE_HERMES_ROOT — the canonical install at + // %LOCALAPPDATA%\\hermes\\hermes-agent (Windows) or ~/.hermes/hermes-agent. + // A valid bootstrap marker proves Desktop finished the first-run install + // flow, but marker provenance is NOT the same thing as runtime usability: + // the CLI can create the exact same repo+venv layout, and older desktop + // builds could leave a healthy install behind without the marker. If the + // active runtime is usable, launch it directly; only fall through to + // bootstrap when the runtime itself is unusable. + const activeRuntime = activeRuntimeState() + + if (activeRuntime.shouldUseActiveRuntime) { + if (!activeRuntime.hasValidMarker) { + rememberLog( + `[bootstrap] Active Hermes runtime at ${ACTIVE_HERMES_ROOT} is usable but the bootstrap marker is missing or stale; skipping first-run bootstrap.` + ) + } + return createActiveBackend(backendArgs) } @@ -3979,10 +3974,10 @@ async function ensureRuntime(backend) { // No venv at the expected location AND no bootstrap-needed sentinel // means we have a half-installed checkout: .git exists, source files // exist, but venv is missing or broken. This shouldn't happen in - // normal flow because isBootstrapComplete() requires - // isHermesSourceRoot() and the bootstrap writes the marker only after - // install.ps1 succeeds. If we hit this, the user (or a deleted venv) - // broke the invariant; tell them to re-run the install. + // normal flow because activeRuntimeState() requires isHermesSourceRoot() + // plus an importable hermes_cli before it hands back the active runtime. + // If we hit this, the user (or a deleted venv) broke the invariant; tell + // them to re-run the install. throw new Error( `Hermes venv missing at ${VENV_ROOT}. Re-run the desktop installer or ` + '`scripts/install.ps1` to rebuild it.' )