Skip to content

fix(install): add browser stage to $InstallStages so desktop Update actually installs/upgrades agent-browser - #67835

Open
LiteSoul wants to merge 1 commit into
NousResearch:mainfrom
LiteSoul:fix/install-ps1-browser-stage
Open

fix(install): add browser stage to $InstallStages so desktop Update actually installs/upgrades agent-browser#67835
LiteSoul wants to merge 1 commit into
NousResearch:mainfrom
LiteSoul:fix/install-ps1-browser-stage

Conversation

@LiteSoul

@LiteSoul LiteSoul commented Jul 20, 2026

Copy link
Copy Markdown

Summary

Sibling to PR #65701 (fix(browser): browser tools unusable after Hermes restart — zombie daemon holds port). #65701 fixes a runtime symptom — a zombie agent-browser daemon holding a TCP port after Hermes crashes/closes on Windows. This PR fixes the install-flow bug that explains why the Windows desktop population commonly lands on agent-browser@0.17.1 (the version with the zombie bug) instead of agent-browser@^0.26.0 (where the idle-timeout was wired in upstream commit 284e084bcc).

Sibling, not duplicate. #65701 is the runtime mitigation in the agent-browser-aug handling code; this PR is the install-path wiring so the bundled ^0.26.0 actually gets installed on Windows desktop flows.

The exact problem

The auto-driven stage list $InstallStages (scripts/install.ps1 L3501–L3527) contains 13 stages:

uv, python, git, node, system-packages, repository, venv, dependencies, node-deps,
[desktop if -IncludeDesktop], path, config-templates, platform-sdks, bootstrap-marker,
configure, gateway

There is no browser / agent-browser stage.

The only function in install.ps1 that runs npm install -g --prefix $HERMES_HOME\node "agent-browser@^0.26.0" is Install-AgentBrowser (L355). Its sole caller in the dispatch logic is Invoke-EnsureMode's "browser" case (L3680–L3688), which is reachable only via:

  • install.ps1 -PostInstallInvoke-PostInstallMode (L3703–L3706), or
  • install.ps1 -Ensure browser (L3728–L3734).

Neither is in the auto-driven $InstallStages sequence.

The desktop Update button reads the stage manifest (install.ps1 -Manifest) and iterates each stage via install.ps1 -Stage <name> -NonInteractive -Json (see apps/desktop/electron/bootstrap-runner.ts L779). Because no browser stage is declared, the desktop Update flow never invokes Install-AgentBrowser. On Windows, this means $HERMES_HOME\node\bin\agent-browser (the Hermes-bundled 0.26.0 the install code intends to populate) is NEVER installed or upgraded by the desktop-installed Hermes flow. Hermes falls through to whatever agent-browser is on the user's bare PATH — commonly a stale NVM/Node global install.

Evidence trail

Confirmed by direct inspection of the install.ps1 source (line numbers above) plus a live repro on a Windows + NVM + Node 24 environment while validating #65701's fix:

  • rg -n "function Install-AgentBrowser" scripts/install.ps1 → L355 (definition, exactly one).
  • rg -n "Install-AgentBrowser" scripts/install.ps1 → L355 (def) + L3683 (sole pre-existing callsite in Invoke-EnsureMode). No callsite in Invoke-AllStages / Get-InstallStage.
  • rg -n "PostInstall|-Ensure" apps/desktop/electron/bootstrap-runner.tsno matches. The desktop updater iterates stages via -Stage <name>, never via -PostInstall / -Ensure.
  • $HERMES_HOME\node\bin\agent-browser does not exist on disk when Hermes was installed/upgraded solely via the desktop flow, even though the Hermes process's PATH includes $HERMES_HOME\node\bin (the bundled prefix Install-AgentBrowser is supposed to populate).
  • A bare-PATH agent-browser install (in NVM's node_modules/agent-browser/package.json) reports version 0.17.1 — the zombie-prone version.

The Linux/macOS equivalent — scripts/install.sh's ensure_browser()IS in the install flow via ensure_mode (L2550–L2600). The Windows gap is asymmetric. The asymmetry was introduced in #27224, when the stage-manifest API was added; install.sh's ensure_browser predates that.

The fix

Add a browser stage to $InstallStages, placed after node (the worker calls Resolve-NpmCmd which throws if npm is missing) and after desktop (when -IncludeDesktop is enabled, so a freshly-built Hermes.exe picks up the freshly-installed agent-browser on its first relaunch instead of inheriting a stale bare-PATH binary). The stage also runs for non-desktop installs (irm | iex) — browser tools are a general Hermes capability, not a desktop-only feature.

# Added AFTER the optional -IncludeDesktop insertion (install.ps1 L3532):
$InstallStages += @{ Name = "browser"; Title = "Installing agent-browser"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Browser" }
# Thin worker -- delegates to the existing Install-AgentBrowser (extend, don't
# duplicate, per AGENTS.md). Soft-skips when Node is unavailable (mirrors
# Stage-Node at L3548-L3558): browser tools are optional, the install flow
# MUST NOT abort.
function Stage-Browser          {
    [void](Test-Node)
    if (-not $script:HasNode) {
        $script:_StageSkippedReason = 'Node.js not available; agent-browser install skipped (browser tools will be unavailable)'
        return
    }
    Install-AgentBrowser -SkipChromium:$SkipChromium
}

Stage-Browser worker behavior (what the reviewer should verify)

Property How it's preserved Verified by
Idempotent. A second invocation with agent-browser already at ^0.26.0 is a near-no-op. npm install -g --prefix is idempotent against a satisfied version range; protocol version is NOT bumped (stages are additive per L3492). Pester manifest test (no version bump); cold install ~5–15s on Windows, warm reinstall faster (npm cache hits the satisfied version).
Graceful-skip on no Node. If Test-Node fails, the worker sets $script:_StageSkippedReason and returns — the install flow MUST NOT abort (browser tools are optional). Mirrors Stage-Node at L3548–L3558 (same $_StageSkippedReason channel); Invoke-Stage L3609–L3616 surfaces it as skipped: true, ok: true in the JSON frame. Python source-level test test_install_ps1_stage_browser_soft_skips_on_no_node.
-SkipChromium forwarding. Worker forwards the flag to Install-AgentBrowser. install.ps1 has no top-level -SkipChromium param today, so the forwarded value is $null (falsy) — identical to Install-AgentBrowser's own [switch]$SkipChromium defaulting to $false. Install-AgentBrowser -SkipChromium:$SkipChromium form (forwarding-shape); body at L356/L383–L399 re-checks Find-SystemBrowser internally. Python source-level test test_install_ps1_stage_browser_forwards_skipchromium_flag.
Respects $IncludeDesktop placement. When -IncludeDesktop is set, Stage-Desktop runs first then Stage-Browser; both run for non-desktop installs since browser tools are a general capability. Inserted after the optional desktop stage insertion (L3517); also added after node-deps (L3510). Pester manifest ordering assertion 'browser' appears after 'node'.
No process-kill pathway. Worker is a thin install passthrough only — does not introduce a parallel reaper pathway (defense against the same Blocking concern tonydwb raised on #65701). Worker body contains no Stop-Process/taskkill/Terminate/.agent-browser references. Python source-level test test_install_ps1_stage_browser_does_not_terminate_process_pool.

Coordination with #58687 (triage flag)

Triage helpfully flagged an interplay with #58687 (fix(update): honor configured bootstrap state). #58687 makes the Linux/macOS side skip the browser install when the browser toolset is configured off (agent.disabled_toolsets / platform_toolsets.cli excludes browser). My PR adds the browser stage to the Windows side so the desktop Update flow does run the browser install when needed.

These are not contradictory in intent — install when wanted, skip when not — but there is a real gap to own up to: Stage-Browser does NOT currently consult agent.disabled_toolsets / platform_toolsets config before spawning npm install. A Windows desktop user who ran hermes tools disable browser and then hits the desktop Update flow would still get the bundled agent-browser@^0.26.0 install via $InstallStages iteration — which is exactly the unconditional-install behavior #58687 was filed to stop on Linux.

Why the fix is not folded into this PR

  • fix(update): honor configured bootstrap state #58687 is itself still OPEN and unreviewed. Coordinating behavioral parity with an unmerged sibling PR is speculative wiring; in particular, the config-presence detection that PR adds (has_existing_hermes_config) does not exist on the PowerShell side yet and pulling it across means duplicating fix(update): honor configured bootstrap state #58687's import (hermes_cli.dep_ensure) logic into PowerShell — that's a much larger change than this PR's scope.
  • This PR's independent justification — the desktop Update path silently bypassing Install-AgentBrowser entirely — stands regardless of the disabled_toolsets interplay.
  • AGENTS.md cautions against speculative infrastructure with no concrete consumer; today, the consumer of the Windows disabled_toolsets-aware skip is the (unmerged) fix(update): honor configured bootstrap state #58687 design.

Recommended sequencing

  1. This PR (#67835): adds the Windows browser stage so the install-flow gap stops. The stage's necessary skip-paths (no Node, no Chromium override) are in place; the config-awareness skip is not.
  2. fix(update): honor configured bootstrap state #58687 (or a follow-up derived from it): adds the disabled_toolsets-aware skip. The natural shape is a symmetric Stage-Browser guard that reads $HERMES_HOME\config.yaml's agent.disabled_toolsets field and soft-skips when browser is excluded — reusing the $_StageSkippedReason channel this PR wires the no-Node case through. A follow-up authoring this is straightforward once fix(update): honor configured bootstrap state #58687 lands or a maintainer indicates preference for folding it in.

Existing-manual-install note (worth flagging for review)

A user with a manually-installed agent-browser 0.17.1 in their NVM continues to have that v0.17.1 in NVM. After this PR lands, Hermes-spawned subprocesses get 0.26.0 from the bundled prefix ($HERMES_HOME\node\bin, prepended to PATH). The user's OLD 0.17.1 stays installed in NVM but is no longer spawned by Hermes — the bundled-prefix prepend order wins. This is the same ordering install.sh relies on for POSIX. The user is not silently upgraded on their bare PATH; the bundled-install wins via PATH precedence, which is the install.ps1 design intent (the prefix override is what makes ^0.26.0 the Hermes-curated version rather than whatever happens to be on PATH).

Tests

Python source-level — tests/test_install_ps1_browser_stage.py (new, 11 tests)

Source-level by design: install.ps1 is Windows-only PowerShell; Linux CI cannot execute it. The existing tests/test_install_ps1_*.py family already pins install.ps1 contracts via source-text parsing (test_install_ps1_node_path_for_npm.py, test_install_ps1_ascii_only.py, etc). Following the same convention lets Linux CI verify the structural contract without running PowerShell.

Covers:

  • browser stage is declared in $InstallStages with the right shape (Name/Title/Category/NeedsUserInput/Worker).
  • NeedsUserInput = $false (matches the manifest driver's contract — stages are driven with -NonInteractive per bootstrap-runner.ts L779).
  • Stage ordering: browser appears after node AND node-deps, and before configure (interactive group runs last).
  • Stage-Browser worker is defined and delegates to Install-AgentBrowser (extend, don't duplicate — asserts Install-AgentBrowser is still defined exactly once and the npm install -g --prefix call still appears exactly once in the file).
  • Soft-skip when Test-Node returns false ($script:_StageSkippedReason set, return not throw).
  • Worker does not introduce a process-kill / app-dir reaper pathway — defense-in-test against the same Blocking concern tonydwb raised on fix(browser): browser tools unusable after Hermes restart — zombie daemon holds port #65701 (scope creep into the reaper pathway). Worker is a thin install passthrough only.
  • Forwards -SkipChromium to Install-AgentBrowser (the future-proofing shape noted above).
  • install.ps1 stays pure ASCII (cross-check the existing tests/test_install_ps1_ascii_only.py invariant for the lines added by this PR specifically).

Pester smoke — scripts/tests/test-install-ps1-stage-protocol.ps1 (extended, +25 lines)

Runtime-side cross-check on Windows where install.ps1 -Manifest actually executes. New assertions:

  • manifest contains stage 'browser'
  • 'browser' stage appears after 'node' stage
  • 'browser' stage appears before 'configure' stage
  • 'browser' stage declares needs_user_input=false
  • 'browser' stage category is 'install'

Test plan

  • Windows PowerShell 7 (pwsh): scripts/tests/test-install-ps1-stage-protocol.ps1 — all 5 new assertions PASS, all pre-existing smoke assertions PASS (29 OK / 0 FAIL).
  • Python 3.11.15 on Windows: 19/19 source-level install.ps1 tests pass (1 ascii + 3 node-path + 4 native-stderr-eap + 11 new browser-stage).
  • Linux/macOS: not directly verified. Pester suite is Windows-only by nature (shells out to powershell.exe); source-level Python tests use only stdlib regex + Path.read_bytes() so no host-specific behavior is expected, but is not asserted here.

What this PR deliberately does NOT touch

Cross-link / sibling PR

A top-level cross-link comment has been posted on #65701 so readers there can find this sibling.

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) tool/browser Browser automation (CDP, Playwright) area/install-update Installer, updater, packaging, wheels, doctor platform/windows Native Windows-specific behavior or breakage P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 20, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #65701 and #58687. This adds the missing Windows staged installer path, rather than the daemon-recovery path in #65701; please reconcile it with #58687's configuration-aware browser-install behavior.

@LiteSoul
LiteSoul force-pushed the fix/install-ps1-browser-stage branch from 505c195 to ab649be Compare July 20, 2026 15:14
@LiteSoul

Copy link
Copy Markdown
Author

@alt-glitch — thanks for flagging the #58687 interplay. Reconciling it explicitly so reviewers see the analysis:

The two PRs install/skip in opposite directions on purpose

Not contradictory — install when wanted, skip when not — but there's a real asymmetry left in this PR that I want to own up to:

The gap I deliberately did NOT fold here

Stage-Browser does NOT currently consult agent.disabled_toolsets / platform_toolsets config before spawning npm install. A Windows desktop user who ran hermes tools disable browser and then hits desktop Update would still get the bundled agent-browser@^0.26.0 install — exactly the unconditional-install behavior #58687 was filed to stop on Linux.

I considered folding the config-awareness into this PR and chose not to, for three reasons:

  1. fix(update): honor configured bootstrap state #58687 is itself still OPEN and unreviewed. The config-presence detection that PR adds (has_existing_hermes_config) and its dep_ensure._DEP_CHECKS['browser'] integration don't exist on the PowerShell side yet. Pulling it across means duplicating fix(update): honor configured bootstrap state #58687's import wiring into PowerShell — much larger change than this PR's install-flow scope.
  2. This PR's independent justification stands regardless. The desktop Update path silently bypassing Install-AgentBrowser entirely is the bug here; that fix is correct with or without the disabled_toolsets interplay.
  3. AGENTS.md cautions against speculative infrastructure with no concrete consumer — today the consumer of the Windows disabled_toolsets-aware skip is the (unmerged) fix(update): honor configured bootstrap state #58687 design.

Recommended sequencing

  1. fix(install): add browser stage to $InstallStages so desktop Update actually installs/upgrades agent-browser #67835 (this PR) lands first: Windows browser stage so the install-flow gap stops. Skip-paths in place (no Node, no Chromium override); disabled_toolsets config-awareness skip is not.
  2. Follow-up (could be fix(update): honor configured bootstrap state #58687's follow-through, or a separate PR I can author when a maintainer wants it): symmetric Stage-Browser guard reading $HERMES_HOME\config.yaml's agent.disabled_toolsets field and soft-skipping when browser is excluded — reusing the same $_StageSkippedReason channel this PR already wires the no-Node case through. Drop me a 👍 if you'd like me to author that follow-up now so fix(update): honor configured bootstrap state #58687's maintainers can review the Linux + Windows sides together.

On the sweeper:risk-compatibility / sweeper:risk-platform-windows labels

Useful — these are accurate: this PR does change Windows install/upgrade behavior for the desktop flow (introduces a previously-missing install step). The compatibility surface I can name concretely:

  • First Update post-merge for Windows desktop users will trigger a cold npm install -g --prefix of agent-browser@^0.26.0 (~5-15s), where today that step silently no-ops (because the stage was missing). That's intended behavior but is a perceptible latency change.
  • Users with a manually-installed NVM agent-browser@0.17.1 will see Hermes-spawned subprocesses stop using that install (the bundled prefix prepended to PATH wins). Their NVM install isn't deleted, just no longer first on PATH. Documented in the PR body.

If the needs-decision label is about the #58687 sequencing question above, my recommendation is land #67835 and follow with the symmetrical config-awareness guard. If it's about a different decision you want a maintainer to make explicit, please flag and I'll address.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing the missing staged-install path. The premise is verified on current main: scripts/install.ps1:3517-3543 has no browser stage, and apps/desktop/electron/bootstrap-runner.ts:933-977 runs only manifest stages and stops on a failure.

Problems

  • scripts/install.ps1:3606 calls Install-AgentBrowser without handling its npm failure. That helper throws on a non-zero npm exit (scripts/install.ps1:374-379), and the desktop runner aborts bootstrap for a failed stage (bootstrap-runner.ts:973-976). This makes an optional browser install fatal during an update.
  • The new stage at scripts/install.ps1:3532 is unconditional. As noted in the PR discussion, it will install browser dependencies even when the user disabled that toolset; current runtime resolution applies agent.disabled_toolsets as a final override (hermes_cli/tools_config.py:1988-1996). Please reconcile that policy before adding the stage.
  • tests/test_install_ps1_browser_stage.py:47 onward reads installer source and asserts regex shapes rather than exercising stage behavior. It cannot cover the failure path above.

Suggested changes

  • Convert npm-install failures to a skipped stage with _StageSkippedReason, and test the emitted JSON result in an isolated Windows fixture with controlled npm behavior.
  • Add the configuration-aware guard or obtain an explicit maintainer decision for the intended divergence.
  • Replace the source-text test suite with behavioral PowerShell coverage.

Automated hermes-sweeper review.

Comment thread scripts/install.ps1
# browser tools are a general Hermes capability, not a desktop-only feature.
# Soft-skip when Node is unavailable (browser tools degrade gracefully);
# see the Stage-Node note at L3548-3558 for the same pattern.
$InstallStages += @{ Name = "browser"; Title = "Installing agent-browser"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Browser" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stage is added to every Windows manifest without consulting persisted browser-tool configuration. hermes_cli/tools_config.py:1988-1996 treats agent.disabled_toolsets as a final user override, and the PR discussion confirms this update would reinstall agent-browser after a user disabled browser tools. Please add the configuration-aware skip here or resolve the intended policy before merging.

@LiteSoul LiteSoul Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in cc6c08e83 (amended from 4bb103263 — same logical content, rebased to incorporate the behavioral test rewrite): Stage-Browser now consults agent.disabled_toolsets from config.yaml before installing. A new Get-HermesConfigDisabledToolsets helper (scripts/install.ps1, after Write-BrowserEnv) reads $HermesHome\config.yaml and returns the suppressed toolset names as a hashtable. If browser is in that set, the worker sets $script:_StageSkippedReason and returns — surfacing skipped=true, ok=true in the JSON frame, matching Stage-Node's soft-skip pattern. This mirrors the runtime resolver's behavior (hermes_cli/tools_config.py:2146-2154agent.disabled_toolsets applied as a final user override that runs last).

The helper uses a small line-oriented state machine rather than a YAML parser — PowerShell has no built-in YAML module, and importing one would widen the install bootstrap's dependency footprint for a single scalar key. It handles both inline ([browser, memory]) and block-sequence (- browser) YAML forms. When config.yaml is absent (fresh install), the function returns an empty set and the stage proceeds — the guard only applies during Update runs where config.yaml already exists.

Comment thread scripts/install.ps1 Outdated
$script:_StageSkippedReason = 'Node.js not available; agent-browser install skipped (browser tools will be unavailable)'
return
}
Install-AgentBrowser -SkipChromium:$SkipChromium

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install-AgentBrowser throws when npm exits non-zero, while runBootstrap aborts on any failed stage (apps/desktop/electron/bootstrap-runner.ts:973-976). Since browser tooling is optional, catch this failure and emit a skipped stage via _StageSkippedReason rather than failing the desktop update.

@LiteSoul LiteSoul Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in cc6c08e83 (amended from 4bb103263 — same logical content, rebased): Stage-Browser now wraps Install-AgentBrowser in a try/catch that converts npm failures to a soft-skip via $script:_StageSkippedReason, so the JSON frame emits skipped=true, ok=true instead of ok=false. The desktop bootstrap pipeline (bootstrap-runner.ts:973-976 aborts on any stage that re-throws) will no longer abort an Update when the optional browser install fails.

The catch block sets the reason to "agent-browser install failed: $_" so the failure is still surfaced in the JSON frame for diagnostics — just without aborting the entire install flow.

A targeted Python invariant (test_install_ps1_stage_browser_converts_npm_failure_to_skip) pins that the try/catch structure exists and sets _StageSkippedReason. The npm-failure path is also covered behaviorally by the Pester suite's npm-install-failure-to-skip case, which overrides Install-AgentBrowser to throw and asserts $_StageSkippedReason is set.

INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1"


def _install_ps1() -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace this source-text/regex suite with a behavioral PowerShell fixture that executes -Stage browser against an isolated home and controlled npm. These assertions pin implementation shape and do not verify the stage JSON contract or the npm-failure behavior.

@LiteSoul LiteSoul Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in cc6c08e83 (amended from 4bb103263 — same logical content, rebased to incorporate the behavioral test rewrite): The source-text/regex suite has been replaced with behavioral PowerShell coverage in scripts/tests/test-install-ps1-browser-stage.ps1. The behavioral suite uses a -Manifest dot-source approach: it dot-sources install.ps1 -Manifest (which loads all functions then returns control via exit 0 without running any stages), overrides Test-Node and Install-AgentBrowser in the same scope (last-definition-wins in PowerShell), then invokes Stage-Browser directly and inspects $script:_StageSkippedReason. This avoids all real side effects (no npm installs, no PATH writes, no child processes). 8 cases:

  • Node missingskipped=true reason mentions Node
  • disabled_toolsets block form (- browser) → skipped=true, reason mentions config.yaml
  • disabled_toolsets inline form ([browser, memory]) → same
  • complex config (other agent/model keys before the list) → same
  • browser NOT in disabled_toolsets → stage proceeds, no skip reason
  • no config.yaml (fresh install) → stage proceeds, no skip reason
  • npm install failure → override Install-AgentBrowser to throw, assert $_StageSkippedReason is set with the error
  • manifest shape → browser stage present in $InstallStages, category=install, after node, before configure

The Python file (tests/test_install_ps1_browser_stage.py) is slimmed to 3 host-independent invariants that Linux CI can run without PowerShell:

  1. browser stage name exists in $InstallStages (structural existence, not shape)
  2. Stage-Browser contains a try/catch that converts npm failures to _StageSkippedReason (the npm-failure path also covered behaviorally by the Pester suite, but this invariant is the CI-feasible guard for Linux runners without PowerShell)
  3. install.ps1 stays pure ASCII (regression guard for [Setup]: Installation didn't finish error #66994 / [Bug]: Installer log #67000)

All three focus on behavior contracts / invariants, not implementation shape — no regex assertions on field ordering or variable name presence.

@LiteSoul
LiteSoul force-pushed the fix/install-ps1-browser-stage branch from ab649be to 4bb1032 Compare July 28, 2026 19:12
…nstalls agent-browser

Adds a `browser` stage to $InstallStages so the desktop Update flow
(bootstrap-runner.ts L779 - drives install.ps1 -Stage <name> per-stage)
installs agent-browser into the Hermes-bundled npm prefix
($HERMES_HOME\node) on Windows, closing the asymmetry with install.sh's
ensure_browser() on Linux/macOS.

Stage-Browser is a thin wrapper delegating to the existing
Install-AgentBrowser function (extend, don't duplicate). Three soft-skip
paths surface $script:_StageSkippedReason so the JSON frame emits
skipped=true / ok=true, never ok=false:

  1. Node.js unavailable -- browser tools degrade gracefully.
  2. agent.disabled_toolsets contains "browser" in config.yaml -- the
     runtime toolset resolver (hermes_cli/tools_config.py:2146-2154)
     applies that field as a final user override; honoring it here
     prevents a desktop Update from reinstalling a toolset the user
     explicitly turned off (per teknium1 review on NousResearch#67835).
  3. Install-AgentBrowser throws (npm non-zero exit, network failure) --
     browser tooling is optional, so a failed install is converted to a
     skip rather than aborting the bootstrap pipeline (per teknium1
     review on NousResearch#67835).

Testing:
- scripts/tests/test-install-ps1-browser-stage.ps1: behavioral Pester
  suite invoking install.ps1 -Stage browser -Json in isolated child pwsh
  processes with temp $HERMES_HOME and config.yaml fixtures. Asserts the
  JSON result frame (skipped=true, ok=true, reason) for each
  disabled_toolsets soft-skip path (block, inline, complex config),
  browser-not-disabled proceeding, no-config (fresh install), and
  manifest shape.
- tests/test_install_ps1_browser_stage.py: slimmed to host-independent
  invariants (stage existence, ASCII purity, try/catch structure for
  rpm-failure-to-skip) for Linux CI without PowerShell.
- scripts/tests/test-install-ps1-stage-protocol.ps1: existing smoke test
  extended to assert browser appears in -Manifest output, after node,
  before configure, with category=install / needs_user_input=false.
@LiteSoul
LiteSoul force-pushed the fix/install-ps1-browser-stage branch from 4bb1032 to cc6c08e Compare July 28, 2026 19:31
@LiteSoul

LiteSoul commented Jul 28, 2026

Copy link
Copy Markdown
Author

Review feedback addressed — updated to cc6c08e83

All three concerns from the review are resolved by code changes (not just rebuttals). The branch was force-pushed with --force-with-lease after amending the commit to incorporate the fixes.

1. disabled_toolsets guard (install.ps1:3532)

Concern: The browser stage was unconditional — it would reinstall agent-browser even when the user disabled the browser toolset via agent.disabled_toolsets in config.yaml.

Fix: Stage-Browser now calls a new Get-HermesConfigDisabledToolsets helper (install.ps1, after Write-BrowserEnv) that reads $HermesHome\config.yaml and parses the agent.disabled_toolsets list. If browser is in that set, the worker sets $script:_StageSkippedReason and returns — emitting skipped=true, ok=true in the JSON frame. This mirrors the runtime resolver's behavior (hermes_cli/tools_config.py:2146-2154agent.disabled_toolsets applied as a final user override). When config.yaml is absent (fresh install), the function returns an empty set and the stage proceeds.

The helper uses a line-oriented state machine (no YAML parser — PowerShell has none built-in, and importing one would widen the bootstrap's dependency footprint for a single scalar key). Handles both inline ([browser, memory]) and block-sequence (- browser) YAML forms.

2. npm-failure → soft-skip (install.ps1:3606)

Concern: Install-AgentBrowser throws on non-zero npm exit, and bootstrap-runner.ts:973-976 aborts on any failed stage — making an optional browser install fatal during a desktop Update.

Fix: Stage-Browser wraps Install-AgentBrowser in a try/catch that converts npm failures to a soft-skip via $script:_StageSkippedReason. The JSON frame now emits skipped=true, ok=true instead of ok=false, so the desktop Update flow continues. The failure reason is still surfaced for diagnostics.

3. Source-text tests → behavioral Pester suite (tests/test_install_ps1_browser_stage.py:47)

Concern: Source-text/regex assertions pin implementation shape and don't verify the stage JSON contract or the npm-failure behavior.

Fix: The source-text suite is replaced with a behavioral Pester suite (scripts/tests/test-install-ps1-browser-stage.ps1) that dot-sources install.ps1 -Manifest (loads all functions then returns), overrides Test-Node and Install-AgentBrowser, invokes Stage-Browser directly, and asserts $script:_StageSkippedReason for each path. All 8 tests pass:

Test Scenario Expected
1 Node unavailable skip, reason mentions "Node.js not available"
2 disabled_toolsets block form skip, reason mentions config.yaml
3 disabled_toolsets inline form skip, reason mentions config.yaml
4 browser NOT in disabled_toolsets proceeds (no skip reason)
5 No config.yaml (fresh install) proceeds (no skip reason)
6 Install-AgentBrowser throws skip, reason mentions "install failed"
7 Complex config (other keys) skip, reason mentions config.yaml
8 Manifest shape browser present, correct category/ordering

The Python file (tests/test_install_ps1_browser_stage.py) retains 3 host-independent invariants for Linux CI: (1) stage-name existence (structural, not shape-pinning), (2) try/catch structure that converts npm failures to _StageSkippedReason (the npm-failure path is also covered behaviorally by Pester test 6, but this invariant is the CI-feasible guard for Linux runners without PowerShell), and (3) ASCII purity (regression guard for #66994/#67000).

Verification

  • Python tests: 3 passed (pytest 9.1.1, Python 3.11.15)
  • Pester behavioral suite: 8/8 passed (pwsh 7.4)
  • Pester smoke test: all passed (pwsh 7.4 and Windows PowerShell 5.1 parity)
  • Merge probe against origin/main: git merge --no-commit --no-ff origin/main auto-merged scripts/install.ps1 cleanly with zero conflict files
  • Platforms: verified on Windows 11. Not directly verified on Linux/macOS (the Python invariants are designed to be host-independent but this was not exercised on a Linux runner).

@LiteSoul
LiteSoul requested a review from teknium1 July 28, 2026 19:39
@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants