Skip to content

fix(update): refresh installer bootstrap-cache scripts on every update (stale hermes-setup mitigation) - #82229

Merged
teknium1 merged 1 commit into
mainfrom
fix/update-refreshes-bootstrap-cache
Aug 9, 2026
Merged

fix(update): refresh installer bootstrap-cache scripts on every update (stale hermes-setup mitigation)#82229
teknium1 merged 1 commit into
mainfrom
fix/update-refreshes-bootstrap-cache

Conversation

@teknium1

@teknium1 teknium1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes the last layer of the 2026-08-09 Windows update-failure incident: hermes update now refreshes the Desktop installer's bootstrap-cache scripts from the fresh checkout on every run, so stale hermes-setup binaries stop executing months-old install code.

The problem

Pre-#67193 hermes-setup.exe builds (June 2026 and earlier — including the newest published binary, June 5) resolve bootstrap-cache/install-<branch>.ps1 as "exists → reuse forever". A branch-ref script cached at install time is never re-downloaded, and the binary has no self-update path, so every GUI update/repair runs frozen-in-time install code no matter how current the repo is.

Live incident: install-main.ps1 cached June 4 lacked #81327's venv process-tree sweep → the bootstrap venv stage died twice with Cannot remove item venv\Scripts\python.exe: Access denied on a straggler backend pair — hours after every relevant fix was already merged on main. The fixes existed; the installer never executed them.

The fix

_refresh_bootstrap_cache_scripts(), called at the end of all three update paths (git, zip fallback, already-up-to-date repair): overwrite mutable branch-ref cache entries with the freshly pulled scripts/install.ps1 / install.sh. The stale binary's unconditional-reuse bug becomes a feature — it "reuses" a file the update keeps permanently current. Post-#67193 installers re-download on every run anyway, so for them this is a harmless pre-seed of identical bytes.

Scope guards:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

How to Test

  • pytest tests/hermes_cli/test_update_bootstrap_cache_refresh.py -o "addopts=--timeout-method=thread" — 8 passed (overwrite ps1+BOM / sh no-BOM / BOM-not-doubled / SHA-pin untouched / current-entry silent / missing dir/sources no-op / never-raises).
  • Neighbor suites: test_update_venv_health.py, test_update_orphan_backend_reap.py, test_update_modified_notice.py — 22 passed.

E2E verification (the actual incident machine)

Poisoned the real %LOCALAPPDATA%\hermes\bootstrap-cache\install-main.ps1 with a stub, ran the real function: healed byte-exact to the checkout's script — BOM intact, #81327 tree-kill sweep present (taskkill /F /T /PID $treePid). Output: ✓ Refreshed installer bootstrap-cache script(s): install-main.ps1.

Incident fix-stack context

#82158 (scan truncation) → #81327 (installer tree-kill) → #82179 (orphan reap) → #82191 (Desktop teardown + tree-aware classifier) → this (make sure stale installers actually run all of the above).

Checklist

  • Conventional commits; no unrelated changes
  • Tests added; tested on Windows 11 against the real incident cache
  • Cross-platform: refreshes both .ps1 and .sh; pure-Python, no OS gating needed; silent no-op when bootstrap-cache/ doesn't exist (CLI-only installs)

… update

Pre-#67193 hermes-setup binaries (June 2026 and earlier, including the
newest published build) resolve bootstrap-cache/install-<branch>.ps1 by
"exists -> reuse forever": a branch-ref cache entry written at install
time is never re-downloaded, so every GUI update/repair executes a
months-stale install script. The binary has no self-update path, so no
amount of `hermes update` fixes it.

Live incident (2026-08-09, ryanc): install-main.ps1 cached June 4 lacked
the #81327 venv process-tree sweep; the bootstrap venv stage died with
"Cannot remove item venv\Scripts\python.exe: Access denied" on a
straggler backend pair, twice, despite every relevant fix already being
merged on main - the installer simply never ran that code.

Fix: `_refresh_bootstrap_cache_scripts()` runs at the end of every
update path (git, zip, already-up-to-date repair), overwriting mutable
branch-ref cache entries with the freshly pulled scripts/install.ps1 /
install.sh. The stale binary's unconditional reuse becomes a feature: it
"reuses" a file the update keeps permanently current. Post-#67193
installers re-download on every run anyway, so this is a harmless
pre-seed of identical bytes for them.

Scope guards: 40-hex commit-SHA entries are immutable pins and are never
touched; .ps1 gets the UTF-8 BOM to match the installer's cache format
(#67193); best-effort - a failed refresh never fails the update.

E2E on the incident machine: poisoned the real
bootstrap-cache/install-main.ps1 with a stub, ran the real function -
healed byte-exact to the checkout's script (BOM intact, #81327 tree-kill
sweep present).
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 7423ffa

⚠️ Warnings

OSV vulnerability scan · View job

21 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 4m12s vs 7m14s (-41.9%). 7 job(s) slower, 15 faster, 1 unchanged.

  • Python tests / Run tests slice 2/12: +27.0s
  • Python tests / Run tests slice 9/12: -26.0s
  • Python tests / Run tests slice 4/12: -20.0s
  • Python tests / Run tests slice 3/12: +15.0s
  • Python tests / Run tests slice 1/12: -15.0s

@teknium1
teknium1 merged commit 3dcbe90 into main Aug 9, 2026
43 checks passed
@teknium1
teknium1 deleted the fix/update-refreshes-bootstrap-cache branch August 9, 2026 03:54
teknium1 added a commit that referenced this pull request Aug 9, 2026
…match installer pin rules

Two cache-key correctness follow-ups to #82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
kotasiddharth pushed a commit to kotasiddharth/hermes-agent that referenced this pull request Aug 9, 2026
…match installer pin rules

Two cache-key correctness follow-ups to NousResearch#82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
ma1138569845 pushed a commit to ma1138569845/dechnicAuditor-agent that referenced this pull request Aug 10, 2026
…match installer pin rules

Two cache-key correctness follow-ups to NousResearch#82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…match installer pin rules

Two cache-key correctness follow-ups to NousResearch#82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-0a62495.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-0a62495.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("0a62495") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
blut-agent pushed a commit to blut-agent/hermes-agent-fork that referenced this pull request Aug 11, 2026
…match installer pin rules

Two cache-key correctness follow-ups to NousResearch#82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…match installer pin rules

Two cache-key correctness follow-ups to NousResearch#82229 (review feedback):

1. Abbreviated commit pins are immutable too. The installer's
   is_valid_commit() accepts 7-40 hex chars, but the Python refresh
   exempted only exactly-40-hex names — an abbreviated pin like
   install-4ce1994.ps1 could be overwritten with a branch script. The
   predicate now mirrors the Rust rule (7-40 hex = immutable, never
   rewritten), applied to the sanitized target ref.

2. Refresh only the update-target ref's cache key. The helper rewrote
   EVERY mutable-ref entry with the active checkout's script: with
   install-main.ps1 and install-bb_gui.ps1 coexisting, updating main
   replaced both with main's script — cross-branch cache poisoning in
   the other direction. It now computes the single cache key for the
   branch being updated, using the installer's own ref sanitization
   (sanitize_ref: non [A-Za-z0-9._-] -> '_', so bb/gui ->
   install-bb_gui.ps1), and touches nothing else. Entries the
   bootstrapper never wrote are not created.

The branch is threaded from the existing `branch =
_resolve_update_branch(args)` in both _cmd_update_impl call sites and
_update_via_zip (main-only by its own guard).

Regression tests lock down both invariants: abbreviated-SHA pin
untouched (including when passed as the branch), coexisting mutable
refs (main refresh leaves install-bb_gui.ps1 byte-identical),
sanitize_ref parity, and uncached-ref no-op.

E2E on the incident machine's real bootstrap-cache: planted a stale
install-main.ps1 + sibling install-bb_gui.ps1 + abbreviated pin
install-4ce1994.ps1; refresh("main") healed main byte-exact and left
both others untouched; refresh("4ce1994") was a no-op. The pre-existing
40-hex pin entry in the real cache was also untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant