mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
The Zed ACP Registry path (uvx --from 'hermes-agent[acp]==X' hermes-acp)
gets a Python-only install. Browser tools depend on the agent-browser npm
package + Chromium, neither of which are in the wheel. Without an
explicit bootstrap, registry users have no path to working browser tools.
Ship a bundled, idempotent bootstrap script (Linux/macOS bash + Windows
PowerShell) inside acp_adapter/bootstrap/ as wheel package-data. New
entry points:
hermes acp --setup-browser # interactive; prompts before Chromium download
hermes acp --setup-browser --yes # non-interactive
hermes-acp --setup-browser
The terminal-auth flow (hermes acp --setup) also offers the browser
bootstrap as a follow-up after model selection, so first-run registry
users get the option without knowing the flag exists.
Key design choices:
- npm install -g --prefix $NODE_PREFIX so we never need sudo. System Node
on PATH is respected; only the install target is redirected to the
user-writable Hermes-managed Node prefix.
- tools/browser_tool.py::_browser_candidate_path_dirs() already walks
$HERMES_HOME/node/bin, so installed binaries are discovered with no
agent-side code change.
- System Chrome/Chromium detection short-circuits the ~400 MB Playwright
download when a suitable browser already exists.
- Bash + PowerShell live as ONE copy each under acp_adapter/bootstrap/.
Not duplicated under scripts/. install.sh and install.ps1 keep their
inline browser blocks for the source-checkout path.
E2E validated end-to-end:
bash bootstrap_browser_tools.sh --skip-chromium
→ installs agent-browser into ~/.hermes/node/bin/
tools.browser_tool._find_agent_browser()
→ returns the installed path
check_browser_requirements()
→ returns True (browser tools register)
Tests:
- tests/acp/test_entry.py: 11 tests covering --setup-browser dispatch
(linux + windows + --yes forwarding + failure propagation), the
terminal-auth follow-up prompt path, and a package-data wheel-shipping
assertion that catches any future pyproject.toml regression.
Docs: website/docs/user-guide/features/acp.md gains a 'Browser tools
(optional)' subsection with the two-line install + what-it-does.
288 lines
13 KiB
PowerShell
288 lines
13 KiB
PowerShell
# bootstrap_browser_tools.ps1 — install agent-browser + Playwright Chromium
|
|
# into ~/.hermes/node/ for use by Hermes Agent's browser tools on Windows.
|
|
#
|
|
# Targets the registry-install path: users who got Hermes via
|
|
# `uvx --from 'hermes-agent[acp]==X' hermes-acp` don't have a repo clone,
|
|
# so the install.ps1 `npm install`-in-repo flow doesn't apply. This script
|
|
# is a self-contained, idempotent slice of install.ps1's browser block.
|
|
#
|
|
# Usage:
|
|
# .\bootstrap_browser_tools.ps1 # use defaults
|
|
# .\bootstrap_browser_tools.ps1 -Yes # accept Chromium download
|
|
# .\bootstrap_browser_tools.ps1 -SkipChromium # Node + agent-browser only
|
|
#
|
|
# Idempotent: re-running this is safe and fast.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$Yes,
|
|
[switch]$SkipChromium
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$NodeVersion = "22"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Logging
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
function Write-Info { param([string]$msg) Write-Host "[*] $msg" -ForegroundColor Cyan }
|
|
function Write-Success { param([string]$msg) Write-Host "[+] $msg" -ForegroundColor Green }
|
|
function Write-Warn { param([string]$msg) Write-Host "[!] $msg" -ForegroundColor Yellow }
|
|
function Write-Err { param([string]$msg) Write-Host "[x] $msg" -ForegroundColor Red }
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Paths
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
$HermesHome = $env:HERMES_HOME
|
|
if (-not $HermesHome) {
|
|
$HermesHome = Join-Path $env:USERPROFILE ".hermes"
|
|
}
|
|
$NodePrefix = Join-Path $HermesHome "node"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Step 1: Node.js
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
function Resolve-NpmExe {
|
|
# Same gotcha as install.ps1: prefer npm.cmd over npm.ps1 so the
|
|
# PowerShell execution policy doesn't block us.
|
|
$cmd = Get-Command npm -ErrorAction SilentlyContinue
|
|
if (-not $cmd) { return $null }
|
|
$npmExe = $cmd.Source
|
|
if ($npmExe -like "*.ps1") {
|
|
$sibling = Join-Path (Split-Path $npmExe -Parent) "npm.cmd"
|
|
if (Test-Path $sibling) { return $sibling }
|
|
}
|
|
return $npmExe
|
|
}
|
|
|
|
function Resolve-NpxExe {
|
|
$cmd = Get-Command npx -ErrorAction SilentlyContinue
|
|
if (-not $cmd) { return $null }
|
|
$npxExe = $cmd.Source
|
|
if ($npxExe -like "*.ps1") {
|
|
$sibling = Join-Path (Split-Path $npxExe -Parent) "npx.cmd"
|
|
if (Test-Path $sibling) { return $sibling }
|
|
}
|
|
return $npxExe
|
|
}
|
|
|
|
function Ensure-Node {
|
|
# System Node on PATH?
|
|
$sysNode = Get-Command node -ErrorAction SilentlyContinue
|
|
if ($sysNode) {
|
|
try {
|
|
$v = & $sysNode.Source --version
|
|
$major = [int]($v -replace '^v(\d+).*', '$1')
|
|
if ($major -ge 20) {
|
|
Write-Success "Node.js $v found on PATH"
|
|
return
|
|
}
|
|
Write-Warn "Node.js $v is older than v20 — installing managed Node."
|
|
} catch {
|
|
Write-Warn "Failed to query Node version: $_"
|
|
}
|
|
}
|
|
|
|
# Hermes-managed Node?
|
|
$managedNode = Join-Path $NodePrefix "node.exe"
|
|
if (Test-Path $managedNode) {
|
|
$v = & $managedNode --version
|
|
Write-Success "Node.js $v found (Hermes-managed at $NodePrefix)"
|
|
# Prepend to current-process PATH so subsequent npm/npx calls find it.
|
|
$env:PATH = "$NodePrefix;$env:PATH"
|
|
return
|
|
}
|
|
|
|
Write-Info "Installing Node.js $NodeVersion LTS into $NodePrefix ..."
|
|
|
|
$arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" }
|
|
$indexUrl = "https://nodejs.org/dist/latest-v${NodeVersion}.x/"
|
|
|
|
try {
|
|
$indexPage = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing
|
|
$matches = [regex]::Matches($indexPage.Content, "node-v${NodeVersion}\.\d+\.\d+-win-${arch}\.zip")
|
|
if ($matches.Count -eq 0) {
|
|
Write-Err "Could not locate Node.js $NodeVersion zip for win-$arch"
|
|
throw "no tarball"
|
|
}
|
|
$zipName = $matches[0].Value
|
|
$zipUrl = "$indexUrl$zipName"
|
|
|
|
$tmpDir = Join-Path $env:TEMP "hermes-node-$([guid]::NewGuid().ToString('N'))"
|
|
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
|
|
$zipPath = Join-Path $tmpDir $zipName
|
|
|
|
Write-Info "Downloading $zipName ..."
|
|
Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
|
|
|
|
Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force
|
|
$extracted = Get-ChildItem -Path $tmpDir -Directory | Where-Object { $_.Name -like "node-v*" } | Select-Object -First 1
|
|
|
|
if (-not $extracted) { Write-Err "Node.js extraction failed"; throw "extract" }
|
|
|
|
if (Test-Path $NodePrefix) { Remove-Item -Recurse -Force $NodePrefix }
|
|
New-Item -ItemType Directory -Force -Path $HermesHome | Out-Null
|
|
Move-Item -Path $extracted.FullName -Destination $NodePrefix
|
|
|
|
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
|
|
|
|
$env:PATH = "$NodePrefix;$env:PATH"
|
|
$v = & "$NodePrefix\node.exe" --version
|
|
Write-Success "Node.js $v installed to $NodePrefix"
|
|
} catch {
|
|
Write-Err "Node.js install failed: $_"
|
|
Write-Info "Install Node 20+ manually from https://nodejs.org/en/download/ and re-run."
|
|
throw
|
|
}
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Step 2: agent-browser
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
function Ensure-AgentBrowser {
|
|
$npmExe = Resolve-NpmExe
|
|
if (-not $npmExe) {
|
|
Write-Err "npm not on PATH after Node install — aborting"
|
|
throw "npm missing"
|
|
}
|
|
|
|
# Already installed?
|
|
$existing = Get-Command agent-browser -ErrorAction SilentlyContinue
|
|
if ($existing) {
|
|
Write-Success "agent-browser already installed at $($existing.Source)"
|
|
return
|
|
}
|
|
|
|
# When the user has system Node (winget / installer-based), `npm install
|
|
# -g` writes to a directory that may require admin rights. Force the
|
|
# prefix to the user-writable Hermes-managed Node directory so we never
|
|
# need elevation and the agent can always find the result. Mirrors the
|
|
# bash bootstrap's `--prefix $NODE_PREFIX` strategy.
|
|
New-Item -ItemType Directory -Force -Path $NodePrefix | Out-Null
|
|
|
|
Write-Info "Installing agent-browser (npm, prefix=$NodePrefix)..."
|
|
& $npmExe install -g --prefix $NodePrefix --silent `
|
|
"agent-browser@^0.26.0" "@askjo/camofox-browser@^1.5.2"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Err "npm install -g agent-browser failed (exit $LASTEXITCODE)"
|
|
throw "npm install"
|
|
}
|
|
|
|
# Windows npm global installs drop shims at $NodePrefix\ root (not bin/).
|
|
# Prepend to PATH so any subsequent npx call resolves them.
|
|
$env:PATH = "$NodePrefix;$env:PATH"
|
|
|
|
Write-Success "agent-browser installed to $NodePrefix"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Step 3: Playwright Chromium
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
function Find-SystemBrowser {
|
|
$candidates = @(
|
|
"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
"C:\Program Files\Chromium\Application\chromium.exe",
|
|
"${env:LOCALAPPDATA}\Google\Chrome\Application\chrome.exe",
|
|
"${env:LOCALAPPDATA}\Chromium\Application\chromium.exe"
|
|
)
|
|
foreach ($p in $candidates) {
|
|
if (Test-Path $p) { return $p }
|
|
}
|
|
# Edge — Chromium-based, agent-browser can use it
|
|
foreach ($p in @(
|
|
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
"C:\Program Files\Microsoft\Edge\Application\msedge.exe"
|
|
)) {
|
|
if (Test-Path $p) { return $p }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Write-BrowserEnv {
|
|
param([string]$BrowserPath)
|
|
$envFile = Join-Path $HermesHome ".env"
|
|
New-Item -ItemType Directory -Force -Path $HermesHome | Out-Null
|
|
if (Test-Path $envFile) {
|
|
$existing = Get-Content $envFile -Raw -ErrorAction SilentlyContinue
|
|
if ($existing -and ($existing -match "(?m)^AGENT_BROWSER_EXECUTABLE_PATH=")) {
|
|
return
|
|
}
|
|
}
|
|
Add-Content -Path $envFile -Value ""
|
|
Add-Content -Path $envFile -Value "# Hermes Agent browser tools — use the system Chrome/Chromium/Edge binary."
|
|
Add-Content -Path $envFile -Value "AGENT_BROWSER_EXECUTABLE_PATH=$BrowserPath"
|
|
Write-Success "Configured browser tools to use $BrowserPath"
|
|
}
|
|
|
|
function Confirm-ChromiumDownload {
|
|
if ($Yes) { return $true }
|
|
if (-not [Environment]::UserInteractive) {
|
|
Write-Warn "Non-interactive shell — skipping Chromium prompt."
|
|
Write-Info "Re-run with -Yes to install Chromium (~400 MB download)."
|
|
return $false
|
|
}
|
|
$reply = Read-Host "Install Playwright Chromium (~400 MB download)? [y/N]"
|
|
return ($reply -match "^(y|yes)$")
|
|
}
|
|
|
|
function Ensure-Chromium {
|
|
if ($SkipChromium) {
|
|
Write-Info "Skipping Chromium install (-SkipChromium)"
|
|
return
|
|
}
|
|
|
|
# agent-browser on Windows expects a Playwright-managed Chromium under
|
|
# %LOCALAPPDATA%\ms-playwright. The system-browser shortcut from the
|
|
# Linux/macOS path doesn't apply the same way on Windows — Playwright's
|
|
# default launch path won't pick up a stock Chrome install without an
|
|
# explicit AGENT_BROWSER_EXECUTABLE_PATH. We still offer it as a
|
|
# fallback when the user doesn't want the download.
|
|
|
|
if (-not (Confirm-ChromiumDownload)) {
|
|
$sys = Find-SystemBrowser
|
|
if ($sys) {
|
|
Write-Info "Using system browser at $sys (Chromium download skipped)."
|
|
Write-BrowserEnv -BrowserPath $sys
|
|
} else {
|
|
Write-Info "Chromium install skipped. Browser tools won't launch until"
|
|
Write-Info "Chromium is installed or AGENT_BROWSER_EXECUTABLE_PATH is set."
|
|
}
|
|
return
|
|
}
|
|
|
|
$npxExe = Resolve-NpxExe
|
|
if (-not $npxExe) {
|
|
Write-Err "npx not on PATH — cannot install Playwright Chromium"
|
|
throw "npx missing"
|
|
}
|
|
|
|
Write-Info "Installing Playwright Chromium (~400 MB) ..."
|
|
& $npxExe --yes playwright install chromium
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Err "Playwright Chromium install failed (exit $LASTEXITCODE)"
|
|
Write-Info "Try again later: npx --yes playwright install chromium"
|
|
throw "playwright"
|
|
}
|
|
Write-Success "Playwright Chromium installed"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# Main
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
Write-Info "Hermes Agent: bootstrapping browser tools"
|
|
Write-Info " HERMES_HOME = $HermesHome"
|
|
Write-Info " OS = Windows"
|
|
|
|
Ensure-Node
|
|
Ensure-AgentBrowser
|
|
Ensure-Chromium
|
|
|
|
Write-Success "Browser tools setup complete."
|
|
Write-Info "Hermes Agent will pick up agent-browser from $NodePrefix on next launch."
|