fix(cli): quarantine running hermes.exe during update dep-verification repair on Windows - #40409
Conversation
…n repair on Windows
The dependency-verification repair in _verify_core_dependencies_installed
ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly,
bypassing the Windows shim-quarantine that the primary install path performs.
That reinstall rewrites the entry-point shims, and on Windows the live
hermes.exe is the running process — pip can neither delete nor overwrite it.
With no quarantine, the shim was left missing and 'hermes' dropped off PATH
('hermes' is not recognized... after update).
Extract the rename-out-of-the-way / restore-on-failure logic into a reusable
_run_quarantined_install helper and route both the primary editable installs
and the --reinstall -e . repair through it. The per-package repair installs
only third-party deps (never hermes-agent), so they don't touch the shims and
are left untouched. Add a regression test (fails on old code, passes on new).
|
Reviewed the diff — clean, surgical, correct. The root cause is exactly right: Things I checked and like:
One suggestion: add a one-line inline note that catching Minor (optional): LGTM. 👍 |
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
…n repair on Windows (NousResearch#40409) The dependency-verification repair in _verify_core_dependencies_installed ran 'pip install --reinstall -e .' via _run_install_with_heartbeat directly, bypassing the Windows shim-quarantine that the primary install path performs. That reinstall rewrites the entry-point shims, and on Windows the live hermes.exe is the running process — pip can neither delete nor overwrite it. With no quarantine, the shim was left missing and 'hermes' dropped off PATH ('hermes' is not recognized... after update). Extract the rename-out-of-the-way / restore-on-failure logic into a reusable _run_quarantined_install helper and route both the primary editable installs and the --reinstall -e . repair through it. The per-package repair installs only third-party deps (never hermes-agent), so they don't touch the shims and are left untouched. Add a regression test (fails on old code, passes on new).
Problem
On Windows,
hermes updatecan leave the user withhermes: The term 'hermes' is not recognized as a name of a cmdlet...— thehermes.exelauncher disappears from the venvScripts/directory after an otherwise-successful update.Root cause
hermes updatereinstalls deps via_install_python_dependencies_with_optional_fallback, which has a Windows-specific dance (added in #23394): before each editable install it quarantines the runninghermes.exe(renames it tohermes.exe.old.<ts>), because Windows blocks REPLACE on a mapped/running executable but allows RENAME. uv/pip then writes a fresh shim at the original path, and the old one is restored if the install fails before writing a replacement. That commit's stated intent was to wrap "every install attempt (primary, base-only fallback, and per-extra retries)."Later,
c136eb4deadded_verify_core_dependencies_installed— a post-install belt-and-suspenders check that reinstalls with--reinstall -e .if a declared base dep didn't land. But that repair install calls_run_install_with_heartbeat(...)directly, bypassing the quarantine.--reinstall -e .uninstalls + reinstalls thehermes-agentpackage, including its entry-point shims. On Windows pip can neither delete nor overwrite the livehermes.exe, and because no quarantine ran there was no.oldfallback either → the shim is left missing andhermesdrops off PATH.This matches the reported symptom: the update appears to run twice (the second "Requirement already satisfied" pass is the verification repair), errors on exit, and afterward
hermesis gone.Fix
_run_quarantined_install(cmd, env, scripts_dir)helper._installclosure is now a thin delegate — behaviorally identical) and the--reinstall -e .verification repair through it. This completes the original fix(windows): unbreak install + update on Windows (3 issues) #23394 invariant: every install that rewrites the shims is quarantined.--reinstall <specs>) installs only third-party deps (e.g.pathspec), neverhermes-agent, so it doesn't touch the shims and is intentionally left unquarantined to keep the change tight.Off-Windows (
scripts_dir=None) the helper is a pure pass-through — no behavior change.Test
Added
test_repair_reinstall_quarantines_running_shim_on_windows: asserts the--reinstall -e .repair calls_quarantine_running_hermes_exewith the venvScriptsdir on Windows. Verified it fails against the old code (quarantine not called) and passes against the fix.All existing
test_verify_core_dependencies(now 8),test_update_concurrent_quarantine+test_update_autostash(48) pass. A fulltests/hermes_cli/update-related selection shows the same 2 pre-existing flaky failures on a cleanorigin/mainbaseline (test_recommended_update_command_defaults_to_hermes_update,test_non_interactive_discard_throws_changes_away) — both pass in isolation; unrelated to this change.Recovery for already-affected users
The package itself installed fine — only the shim is missing:
(rewrites the shim in seconds, skips the dependency wall).