feat: Windows bootstrap via install.ps1 -Ensure/-PostInstall + dep_ensure Windows awareness#26620
feat: Windows bootstrap via install.ps1 -Ensure/-PostInstall + dep_ensure Windows awareness#26620alt-glitch wants to merge 13 commits into
Conversation
🔎 Lint report:
|
ReviewPR #26620 — @alt-glitch What it does: Adds Windows-aware bootstrap to What it solves: Windows pip users had no bootstrap path — How: 14 files, +434/-58. Problems with it: None critical.
Verified against current main: All 5 feature areas (dep_ensure Windows, install.ps1 -Ensure/-PostInstall, stamp_install_method, Docker detection, PS1 wheel bundling) are genuinely new — none exist on origin/main. DRY refactor of Resolve-NpmCmd/NpxCmd matches the old inline code exactly. stamp_install_method degrades gracefully on write failure. Test mocks patch correct module paths. Recommendation: Merge. Follow-up #26668 builds on this to consolidate the ACP bootstrap scripts. |
Adds -Ensure <dep> and -PostInstall switch params so that dep_ensure.py can lazily bootstrap non-Python deps on Windows (mirrors install.sh --ensure / --postinstall). Also stamps .install_method="git" in Main.
- Add Resolve-NpmCmd and Resolve-NpxCmd helpers to deduplicate the npm.cmd sibling and npx resolution logic repeated across three call sites - Replace inline resolution in Install-NodeDeps, Invoke-EnsureMode, and Invoke-PostInstallMode with the new helpers - Fix Invoke-EnsureMode browser branch: collapse double Push-Location into a single try/finally wrapping both npm install and playwright install - Fix Invoke-PostInstallMode: add agent-browser npm install before running npx playwright install chromium so Playwright is resolvable via npx
…and PowerShell invocation
… pip docs - Add docker container detection (_is_docker) and .install_method stamp file - detect_install_method() checks stamp first, then heuristics - stamp_install_method() called by postinstall, install.sh, Dockerfile - Nix update message is guidance-style, not prescriptive command - Docs default to `uv pip install` everywhere - Remove hermes_cli/scripts/ from .gitignore
…ract Install-AgentBrowser, fix TOCTOU+encoding
a6c3479 to
a367b15
Compare
Bug 1 (HIGH): Install-AgentBrowser used local package.json + npm install in $HermesHome/agent-browser/, putting the binary at a path that nothing searches. Rewrote to use npm install -g --prefix $HermesHome/node, matching the canonical ACP bootstrap approach (PR #26234). browser_tool.py's _browser_candidate_path_dirs() already searches $HERMES_HOME/node/bin. Bug 2 (LOW): _find_install_script fell back from POSIX to install.ps1, giving confusing 'PowerShell not found' errors on Linux. Removed the cross-platform fallback — POSIX only looks for .sh, Windows only for .ps1. Bug 3 (LOW): Out-File -Encoding utf8 wrote BOM on PS 5.1 — eliminated entirely since npm -g --prefix doesn't need a package.json. Also updated: - _has_hermes_agent_browser() checks canonical node/ prefix path - browser_tool.py post-install recheck includes node/ prefix - 3 new tests for node-prefix detection + no-cross-platform-fallback
When install.ps1 is bundled in the wheel and executed from disk via 'powershell -File', PS 5.1 reads it with the system ANSI codepage (not UTF-8). The box-drawing characters, arrows, and emoji in string literals cause parse failures. The BOM tells PS 5.1 to use UTF-8. This was previously attempted but reverted in favor of replacing em-dashes with ASCII. The em-dash replacement was correct but insufficient — other Unicode chars (box drawing ┌─┐, arrows →, checkmarks ✓✗, emoji 🚀📁⚡) still need the BOM.
npx playwright install chromium fails with npm -g --prefix installs because there's no local package.json with playwright in the dependency tree — npx emits a 'install your project dependencies first' warning and exits without downloading Chromium. agent-browser v0.26+ has its own 'agent-browser install' command that downloads Chrome directly into ~/.agent-browser/browsers/ (182MB), bypassing the playwright npm dependency chain entirely. This is the correct approach for the pip/wheel install path where there is no project-level package.json.
ensure_dependency() with an unrecognized dep name (not in _DEP_CHECKS) would run the install script (which warns and exits 0), then hit the final 'return True' — claiming the dep is available when we have no check function to verify it. Return False instead so callers don't proceed on a false positive.
…sh,ps1}
Eliminates 687 lines of duplicated browser bootstrap code by routing all
bootstrap paths through dep_ensure.py -> install.{sh,ps1} --ensure.
install.sh:
- New ensure_browser() with agent-browser + camofox install, system browser
detection + .env writing, per-distro Playwright deps (apt/arch/fedora/suse)
- macOS app-bundle paths added to find_system_browser()
- configure_browser_env_from_system_browser() creates .env if missing
- postinstall_mode() uses ensure_browser() instead of inline duplication
install.ps1:
- New -Ensure and -PostInstall params (coexists with stage protocol)
- New functions: Resolve-NpmCmd, Resolve-NpxCmd, Find-SystemBrowser,
Write-BrowserEnv, Install-AgentBrowser (with -SkipPlaywright)
- Invoke-EnsureMode dispatches node/browser/ripgrep/ffmpeg
- Invoke-PostInstallMode runs full post-pip-install bootstrap
- ErrorActionPreference guards on all native command calls
- ASCII-only convention maintained (no Unicode)
- Mutual exclusion guard: -Ensure + -Stage = error
dep_ensure.py:
- Windows-aware: _IS_WINDOWS, _find_install_script returns (path, shell) tuple
- PowerShell invocation with powershell/pwsh guard + -ExecutionPolicy Bypass
- _has_hermes_agent_browser() checks platform-correct paths
- _has_system_browser() checks Windows browser names (chrome, msedge, chromium)
- env_extra parameter for forwarding install flags
config.py:
- stamp_install_method() writes ~/.hermes/.install_method
- detect_install_method() checks stamp first (before heuristics)
acp_adapter:
- _run_setup_browser() rewritten: ensure_dependency('node') + ensure_dependency('browser')
- acp_adapter/bootstrap/ deleted (399 + 288 lines)
Rebased onto main -- drops #26620 dependency (upstream stage protocol merged
via #27224). Closes follow-up from #26593.
Summary
Windows
pip install hermes-agentnow bootstraps non-Python deps (Node.js, browser engine, ripgrep, ffmpeg) viainstall.ps1 -Ensure/-PostInstall, mirroring the existinginstall.sh --ensure/--postinstallflow on Linux/macOS. E2E verified on Windows 11 (PS 5.1) with a real PyPI wheel install.Follow-up to #26593 (initial PyPI packaging) and #26234 (ACP browser bootstrap).
What changed (by file)
scripts/install.ps1-Ensure <dep>and-PostInstallparams added to the top-levelparam()block. Entry point dispatches:-Ensure→Invoke-EnsureMode,-PostInstall→Invoke-PostInstallMode, else →Main(full git-clone install).Invoke-EnsureMode— accepts comma-separated dep names (node,browser,ripgrep,ffmpeg). Each dep has its own switch case. Unknown deps warn and skip. Commas, spaces, duplicates handled gracefully.Invoke-PostInstallMode— runs the full bootstrap: Test-Node → Install-SystemPackages → Install-AgentBrowser → Find-SystemBrowser → hermes setup.Install-AgentBrowser— rewritten to usenpm install -g --prefix "$HermesHome\node"(canonical approach from PR feat(acp): hermes acp --setup-browser bootstraps browser tools for registry installs #26234). Previously used a localpackage.json+npm installin$HermesHome\agent-browser\, which put the binary at a path nothing in Hermes searches. Now the binary lands at$HermesHome\node\agent-browser.cmd, whichbrowser_tool.py::_browser_candidate_path_dirs()already discovers.agent-browser install(v0.26+) instead ofnpx playwright install chromium. The npx approach fails withnpm -g --prefixinstalls because there is no localpackage.jsonwith playwright in the dependency tree — npx emits a "install your project deps first" warning and exits without downloading.agent-browser installdownloads Chrome directly into~/.agent-browser/browsers/.Resolve-NpmCmd/Resolve-NpxCmd— extracted from 3 duplicate inline blocks. Handles the PS 5.1 execution-policy gotcha wherenpm.ps1is found beforenpm.cmd.Find-SystemBrowser/Write-BrowserEnv— ported from PR feat(acp): hermes acp --setup-browser bootstraps browser tools for registry installs #26234 ACP bootstrap. Detects system Chrome/Edge/Chromium at standard Windows paths, writesAGENT_BROWSER_EXECUTABLE_PATHto.env.--. UTF-8 BOM added (PS 5.1 reads.ps1from disk with ANSI codepage — box-drawing chars, arrows, emoji break without BOM). No PS7-only syntax (?., ternary,&&).$ErrorActionPreferenceguards aroundagent-browser install— it writes download progress to stderr, which PS 5.1 wraps asErrorRecordand throws on withStop. Same pattern already used foruvin the script.Mainwrites"git"to$HermesHome\.install_method.Invoke-PostInstallModedoes not stamp (Python side handles it).hermes_cli/dep_ensure.py_IS_WINDOWS— module-levelplatform.system() == "Windows"flag._find_install_script— returns(path, shell)tuple. On Windows: looks forinstall.ps1only. On POSIX: looks forinstall.shonly. No cross-platform fallback (previously POSIX fell back to.ps1, giving confusing "PowerShell not found" errors on Linux).ensure_dependency— whenshell == "powershell", builds[powershell, -ExecutionPolicy, Bypass, -File, <script>, -Ensure, <dep>, -HermesHome, <path>]command. Falls back tobashpath for POSIX._has_hermes_agent_browser— checks two paths: canonical$HERMES_HOME/node/agent-browser.cmd(Windows) or$HERMES_HOME/node/bin/agent-browser(POSIX) fromnpm -g --prefix, plus legacy$HERMES_HOME/node_modules/.bin/agent-browserfrominstall.sh._has_system_browser— Windows-specific binary names:chrome,msedge,chromium(vs Linuxgoogle-chrome,google-chrome-stable, etc.).ensure_dependency("foobar")previously returnedTruebecausecheck = _DEP_CHECKS.get(dep)→None, script runs (exits 0 with warning), thenif check:is falsy → fell through toreturn True. Now returnsFalse— can't claim success without a verification function.hermes_cli/config.pydetect_install_method— new resolution order: (1).install_methodstamp file, (2) HERMES_MANAGED env / .managed marker, (3)is_container()→"docker", (4).gitdir →"git", (5) fallback"pip".stamp_install_method— writes method string to~/.hermes/.install_method. Silently swallows OSError (best-effort).recommended_update_command_for_method("docker")— returns"docker pull nousresearch/hermes-agent:latest"._NIX_UPDATE_MSG— replaces hardcoded"sudo nixos-rebuild switch"with generic Nix flake guidance.hermes_cli/main.pycmd_postinstall— callsstamp_install_method("pip")before bootstrapping deps.tools/browser_tool.pyensure_dependency("browser")triggers installation, the recheck now also searches$HERMES_HOME/node/and$HERMES_HOME/node/bin/(the canonicalnpm -g --prefixpaths), not justnode_modules/.bin/.pyproject.tomlpackage-data—hermes_cli/scripts/install.ps1added alongsideinstall.sh..github/workflows/upload_to_pypi.ymlinstall.ps1intohermes_cli/scripts/before wheel build.DockerfileRUN echo "docker" > /opt/data/.install_method.scripts/install.shecho "git" > "$HERMES_HOME/.install_method"at end ofmain().website/docs/getting-started/quickstart.md+updating.mdpip install→uv pip installin code examples.Bugs found and fixed during review
Install-AgentBrowserused localpackage.json+npm install→ binary at$HermesHome\agent-browser\node_modules\.bin\which nothing searchesnpm -g --prefix $HermesHome\node(canonical, matchesbrowser_tool.pydiscovery)npx playwright install chromiumalways failspackage.jsonwith playwright dep → npx shows warning banner and exits without downloadingagent-browser install(v0.26+ downloads Chrome directly)agent-browser installstderr$ErrorActionPreference = StopContinue(same pattern asuv)ensure_dependency("unknown")returns Truecheck = None→ script runs (exits 0) →if check:falsy →return Truereturn False_find_install_scriptfell back to.ps1on Linux → confusing "PowerShell not found" errorUX pathways
Path A:
pip install hermes-agent→hermes postinstall(interactive)cmd_postinstall()stamps"pip"into~/.hermes/.install_methodnode,browser,ripgrep,ffmpeg):ensure_dependency(dep)checks if already presentpowershell -File install.ps1 -Ensure <dep> -HermesHome <path>bash install.sh --ensure <dep>hermes setupPath B:
pip install hermes-agent→ browser tool triggers lazy installbrowser_tool._find_agent_browser()runsensure_dependency("browser")_DEP_CHECKS["browser"]checks:shutil.which→_has_system_browser()→_has_hermes_agent_browser()npm -g --prefixinstalls agent-browser →agent-browser installdownloads Chrome$HERMES_HOME/node/→ findsagent-browser.cmd→ browser tools registerPath C:
install.ps1 -PostInstall(non-interactive, called fromhermes postinstall)Write-Banner→Test-Node→Install-SystemPackages(ripgrep + ffmpeg via winget/choco/scoop)Install-AgentBrowser→Find-SystemBrowser→Write-BrowserEnvhermeson PATH: runshermes setupPath D:
install.ps1 -Ensure node,browser(targeted deps)"Unknown dep '<name>' -- skipping", continues to nextPath E:
detect_install_method()resolution chain~/.hermes/.install_methodstamp file → highest priority (written by installers)HERMES_MANAGEDenv /.managedmarker → NixOS / Homebrewis_container()(dockerenv / containerenv / cgroup) →"docker".gitdirectory in project root →"git""pip"Test results
Unit tests (23/23 pass)
E2E on Windows 11 (PS 5.1.26100, Python 3.11.15, Node 24.15, uv 0.11.11)
_IS_WINDOWSflagdetect_install_method()= pipinstall.ps1bundled in wheelinstall.shbundled in wheel_find_install_script→ ps1/powershellensure_dependency('node')(already present)stamp_install_method+ detectrecommended_update_command('pip')recommended_update_command('docker')install.ps1 -Ensure node(live PS 5.1)_has_hermes_agent_browserafter-Ensure browserhermes --versionfrom wheelhermes doctorhermes --tui(exits cleanly: "no TTY")_find_bundled_tui()→ entry.jsweb_distassets complete (27 files)tui_dist/entry.js= 2.9MB real bundlenode --check entry.jssyntax validinstall.ps1 -Ensure browserfull pipelineFull browser pipeline:
npm -g --prefix→ agent-browser 0.26.0 →agent-browser install→ Chrome 148.0.7778.167 (182MB) →_has_hermes_agent_browser()= True.Edge case coverage (48 tests via 3 parallel subagents)
dep_ensure.py edge cases (17 tests): empty stamp falls through ✅, nonexistent HERMES_HOME ✅, unreadable stamp file ✅, unknown dep returns False ✅, stamp priority over .git ✅, no cross-platform leakage ✅, browser short-circuit when present ✅
install.ps1 edge cases (21 tests): unknown dep warns+skips ✅, multiple deps ✅, spaces in path ✅, duplicate deps idempotent ✅, mixed known/unknown ✅, injection attempt safe ✅,
-PostInstallwithout hermes on PATH ✅, no package managers graceful ✅,-Ensure browserwith no node (warns) ✅TUI/web/pip (10 tests):
hermes --version✅,hermes doctor✅,hermes --tuigraceful no-TTY exit ✅, web_dist 27 files + asset integrity ✅, tui_dist 2.9MB bundle ✅, all module imports ✅, node syntax check ✅