Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 53 additions & 18 deletions apps/desktop/electron/bootstrap-runner.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ function hiddenWindowsChildOptions(options = {}) {
}

const STAMP_COMMIT_RE = /^[0-9a-f]{7,40}$/i
const FALLBACK_COMMIT_RE = /^0{7,40}$/
const FALLBACK_BRANCH = 'main'

function isPinnedCommit(commit) {
return typeof commit === 'string' && STAMP_COMMIT_RE.test(commit) && !FALLBACK_COMMIT_RE.test(commit)
}

function installRefForStamp(installStamp) {
if (installStamp && isPinnedCommit(installStamp.commit)) {
return {
ref: installStamp.commit,
cacheKey: installStamp.commit,
pinned: true
}
}
if (installStamp && typeof installStamp.commit === 'string' && FALLBACK_COMMIT_RE.test(installStamp.commit)) {
const ref = installStamp.branch || FALLBACK_BRANCH
return {
ref,
cacheKey: `fallback-${ref.replace(/[^0-9A-Za-z._-]/g, '_')}`,
pinned: false
}
}
return null
}

// Stages flagged needs_user_input=true in the manifest are skipped by the
// runner (passed -NonInteractive to install.ps1, which the install script
Expand Down Expand Up @@ -104,12 +129,13 @@ function cachedScriptPath(hermesHome, commit) {
return path.join(bootstrapCacheDir(hermesHome), `install-${commit}.${process.platform === 'win32' ? 'ps1' : 'sh'}`)
}

function downloadInstallScript(commit, destPath) {
// Fetch from GitHub raw at the pinned commit. The raw URL with a SHA
// is immutable (unlike a branch ref), so we don't need integrity
// verification beyond "did the file we wrote pass a syntax probe."
function downloadInstallScript(ref, destPath) {
// Fetch from GitHub raw at the install ref. Normal production builds pass a
// pinned SHA. Non-git fallback builds pass an unpinned branch ref so local
// builds can still bootstrap without pretending the all-zero placeholder is
// a real GitHub commit.
const scriptName = installScriptName()
const url = `https://raw.githubusercontent.com/NousResearch/hermes-agent/${commit}/scripts/${scriptName}`
const url = `https://raw.githubusercontent.com/NousResearch/hermes-agent/${ref}/scripts/${scriptName}`
return new Promise((resolve, reject) => {
fs.mkdirSync(path.dirname(destPath), { recursive: true })
const tmpPath = destPath + '.tmp'
Expand Down Expand Up @@ -190,33 +216,38 @@ async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome,
}

// 2. Packaged path: download from GitHub at the pinned commit (1B's stamp).
if (!installStamp || !installStamp.commit || !STAMP_COMMIT_RE.test(installStamp.commit)) {
// Non-git fallback builds carry an all-zero commit; treat it as an
// unpinned branch ref instead of trying to fetch a non-existent SHA.
const installRef = installRefForStamp(installStamp)
if (!installRef) {
throw new Error(
`Cannot resolve ${installScriptName()}: no SOURCE_REPO_ROOT and no install stamp. ` +
'This packaged build was produced without a valid build-time stamp.'
)
}

const cached = cachedScriptPath(hermesHome, installStamp.commit)
const cached = cachedScriptPath(hermesHome, installRef.cacheKey)
try {
await fsp.access(cached, fs.constants.R_OK)
emit({
type: 'log',
line: `[bootstrap] using cached ${installScriptName()} for ${installStamp.commit.slice(0, 12)}`
line: `[bootstrap] using cached ${installScriptName()} for ${installRef.ref.slice(0, 12)}`
})
return { path: cached, source: 'cache', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'cache', commit: installRef.pinned ? installRef.ref : null, kind: installScriptKind() }
} catch {
// not cached; download
}

emit({
type: 'log',
line: `[bootstrap] fetching ${installScriptName()} for ${installStamp.commit.slice(0, 12)} from GitHub`
line:
`[bootstrap] fetching ${installScriptName()} for ${installRef.ref.slice(0, 12)} from GitHub` +
(installRef.pinned ? '' : ' (fallback, unpinned)')
})
try {
await _download(installStamp.commit, cached)
await _download(installRef.ref, cached)
emit({ type: 'log', line: `[bootstrap] saved to ${cached}` })
return { path: cached, source: 'download', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'download', commit: installRef.pinned ? installRef.ref : null, kind: installScriptKind() }
} catch (err) {
// The pinned commit may not be fetchable from GitHub -- most commonly a
// locally-built desktop app stamped to an unpushed HEAD (see
Expand All @@ -234,10 +265,10 @@ async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome,
try {
fs.mkdirSync(path.dirname(cached), { recursive: true })
fs.copyFileSync(installed, cached)
return { path: cached, source: 'installed-agent', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'installed-agent', commit: installRef.pinned ? installRef.ref : null, kind: installScriptKind() }
} catch {
// Cache copy failed (read-only FS, etc.) -- use the source path directly.
return { path: installed, source: 'installed-agent', commit: installStamp.commit, kind: installScriptKind() }
return { path: installed, source: 'installed-agent', commit: installRef.pinned ? installRef.ref : null, kind: installScriptKind() }
}
}
throw err
Expand Down Expand Up @@ -446,7 +477,7 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
// instead of falling back to install.ps1's default ($Branch = "main").
function buildPinArgs(installStamp) {
const args = []
if (installStamp && installStamp.commit) {
if (installStamp && isPinnedCommit(installStamp.commit)) {
args.push('-Commit', installStamp.commit)
}
if (installStamp && installStamp.branch) {
Expand All @@ -460,7 +491,7 @@ function buildPosixPinArgs({ installStamp, activeRoot, hermesHome }) {
if (installStamp && installStamp.branch) {
args.push('--branch', installStamp.branch)
}
if (installStamp && installStamp.commit) {
if (installStamp && isPinnedCommit(installStamp.commit)) {
args.push('--commit', installStamp.commit)
}
return args
Expand Down Expand Up @@ -700,7 +731,7 @@ async function runBootstrap(opts) {

// 4. Write the bootstrap-complete marker.
const markerPayload = {
pinnedCommit: installStamp ? installStamp.commit : null,
pinnedCommit: installStamp && isPinnedCommit(installStamp.commit) ? installStamp.commit : null,
pinnedBranch: installStamp ? installStamp.branch : null
}
const marker = typeof writeMarker === 'function' ? writeMarker(markerPayload) : markerPayload
Expand All @@ -725,5 +756,9 @@ module.exports = {
resolveLocalInstallScript,
resolveInstallScript,
installedAgentInstallScript,
cachedScriptPath
cachedScriptPath,
buildPinArgs,
buildPosixPinArgs,
installRefForStamp,
isPinnedCommit
}
58 changes: 57 additions & 1 deletion apps/desktop/electron/bootstrap-runner.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ const {
runBootstrap,
resolveInstallScript,
installedAgentInstallScript,
cachedScriptPath
cachedScriptPath,
buildPinArgs,
buildPosixPinArgs,
installRefForStamp,
isPinnedCommit
} = require('./bootstrap-runner.cjs')

const SCRIPT_NAME = process.platform === 'win32' ? 'install.ps1' : 'install.sh'
const ZERO_COMMIT = '0000000000000000000000000000000000000000'

function mkTmpHome() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-bootstrap-test-'))
Expand Down Expand Up @@ -80,6 +85,57 @@ test('resolveInstallScript prefers a cached script without touching the network'
}
})

test('fallback install stamps use an unpinned branch ref', () => {
const stamp = { commit: ZERO_COMMIT, branch: 'main' }

assert.equal(isPinnedCommit(ZERO_COMMIT), false)
assert.deepEqual(installRefForStamp(stamp), {
ref: 'main',
cacheKey: 'fallback-main',
pinned: false
})
assert.deepEqual(buildPinArgs(stamp), ['-Branch', 'main'])
assert.deepEqual(buildPosixPinArgs({ installStamp: stamp, activeRoot: '/tmp/hermes', hermesHome: '/tmp/home' }), [
'--dir',
'/tmp/hermes',
'--hermes-home',
'/tmp/home',
'--branch',
'main'
])
})

test('resolveInstallScript downloads fallback stamps by branch instead of zero commit', async () => {
const home = mkTmpHome()
try {
const logs = []
const refs = []
const result = await resolveInstallScript({
installStamp: { commit: ZERO_COMMIT, branch: 'main' },
sourceRepoRoot: null,
hermesHome: home,
emit: ev => logs.push(ev),
_download: async (ref, destPath) => {
refs.push(ref)
fs.mkdirSync(path.dirname(destPath), { recursive: true })
fs.writeFileSync(destPath, '#!/bin/sh\necho fallback branch\n')
return destPath
}
})

assert.deepEqual(refs, ['main'])
assert.equal(result.source, 'download')
assert.equal(result.commit, null)
assert.equal(result.path, cachedScriptPath(home, 'fallback-main'))
assert.ok(
logs.some(ev => /fallback, unpinned/.test(ev.line || '')),
'emits an unpinned fallback log line'
)
} finally {
fs.rmSync(home, { recursive: true, force: true })
}
})

test('resolveInstallScript falls back to the installed agent checkout on a 404', async () => {
const home = mkTmpHome()
try {
Expand Down
37 changes: 30 additions & 7 deletions apps/desktop/scripts/write-build-stamp.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
* "branch": "<branch name>",
* "builtAt": "<ISO 8601 UTC timestamp>",
* "dirty": true|false,
* "source": "ci" | "local"
* "source": "ci" | "local" | "fallback"
* }
*
* Source preference order:
* 1. CI env vars ($GITHUB_SHA / $GITHUB_REF_NAME) -- avoid edge cases with
* shallow clones, detached HEADs, etc. in CI.
* 2. Local `git rev-parse` against the parent repo (../..).
* 3. Fallback stamp for local/personal builds from non-git source trees.
*
* Dev / out-of-repo builds without git produce an explicit error rather than
* silently writing an unstamped manifest -- the packaged app refuses to
* bootstrap without a stamp.
* Dev / out-of-repo builds without git produce an explicit fallback stamp
* rather than aborting the whole build.
*/

const fs = require("fs")
Expand Down Expand Up @@ -77,21 +77,44 @@ function fromLocalGit() {
}
}

function fromFallback() {
// Non-git builds (ZIP download, bootstrap installer without .git) cannot
// determine a real commit. Use a placeholder so local/personal builds
// can still complete. The desktop bootstrap treats the all-zero commit as
// "unknown" and falls back to an unpinned branch bootstrap instead of trying
// to fetch a non-existent GitHub commit.
return {
commit: "0000000000000000000000000000000000000000",
branch: "main",
dirty: false,
source: "fallback"
}
}

function main() {
const stamp = fromCI() || fromLocalGit()
const stamp = fromCI() || fromLocalGit() || fromFallback()
if (!stamp || !stamp.commit) {
// Should not happen — fromFallback() always provides a commit.
console.error(
"[write-build-stamp] ERROR: could not determine git commit.\n" +
" - $GITHUB_SHA not set\n" +
" - `git rev-parse HEAD` failed at " +
REPO_ROOT +
"\n" +
REPO_ROOT + "\n" +
"Packaged builds require a git ref to pin first-launch install.ps1\n" +
"against. Run from a git checkout or set $GITHUB_SHA explicitly."
)
process.exit(1)
}

if (stamp.commit === "0000000000000000000000000000000000000000") {
console.warn(
"[write-build-stamp] WARNING: no git commit found (non-git checkout?).\n" +
" Using placeholder commit — the packaged app will fall back to the\n" +
" default branch for first-launch bootstrap. For production builds,\n" +
" run from a git checkout."
)
}

if (stamp.dirty) {
console.warn(
"[write-build-stamp] WARNING: working tree is dirty.\n" +
Expand Down