refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1} (#27851)

* refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1}

Delete 687 lines of duplicated browser bootstrap code from
acp_adapter/bootstrap/. All browser installation now routes through
dep_ensure -> install.{sh,ps1} --ensure, using agent-browser install
for Chromium. install.sh gains ensure_browser() with macOS app-bundle
detection and per-distro guidance.

Tracking: #27826

* fix(install.sh): add --ignore-scripts to npm install for camofox

@askjo/camofox-browser has a dependency (impit) whose postinstall
script runs `npx only-allow pnpm`, which fails under npm. Adding
--ignore-scripts avoids the spurious failure without affecting
functionality.

Tracking: #27826

* fix: add explicit return in ensure_browser, narrow exception in entry.py

ensure_browser() now returns 0 explicitly on all success paths.
_run_setup_browser() catches OSError instead of broad Exception,
letting ImportError propagate as a real packaging bug.
This commit is contained in:
Siddharth Balyan 2026-05-18 16:36:26 +05:30 committed by GitHub
parent e3a254d65b
commit d9b6f75c0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 140 additions and 831 deletions

View file

@ -1,288 +0,0 @@
# 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."

View file

@ -1,399 +0,0 @@
#!/usr/bin/env bash
#
# bootstrap_browser_tools.sh — install agent-browser + Playwright Chromium
# into ~/.hermes/node/ for use by Hermes Agent's browser tools.
#
# 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.sh `npm install`-in-repo flow doesn't apply. This script
# is a self-contained, idempotent slice of install.sh's browser block —
# safe to run from `hermes-acp --setup-browser`, from a fresh terminal,
# or from install.sh itself (it's a no-op when everything is already in place).
#
# Usage:
# bootstrap_browser_tools.sh # use defaults
# bootstrap_browser_tools.sh --yes # accept the ~400MB Chromium download
# bootstrap_browser_tools.sh --skip-chromium # only install Node + agent-browser
# HERMES_HOME=/custom/path bootstrap_browser_tools.sh
#
# Idempotent: re-running this is safe and fast. Each step checks whether
# the work is already done.
set -euo pipefail
# ─────────────────────────────────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────────────────────────────────
NODE_VERSION="22"
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
NODE_PREFIX="$HERMES_HOME/node"
SKIP_CHROMIUM=false
ASSUME_YES=false
# ─────────────────────────────────────────────────────────────────────────
# Logging
# ─────────────────────────────────────────────────────────────────────────
if [ -t 1 ]; then
C_GREEN='\033[0;32m'
C_YELLOW='\033[0;33m'
C_BLUE='\033[0;34m'
C_RED='\033[0;31m'
C_RESET='\033[0m'
else
C_GREEN='' ; C_YELLOW='' ; C_BLUE='' ; C_RED='' ; C_RESET=''
fi
log_info() { printf "${C_BLUE}[*]${C_RESET} %s\n" "$*"; }
log_success() { printf "${C_GREEN}[✓]${C_RESET} %s\n" "$*"; }
log_warn() { printf "${C_YELLOW}[!]${C_RESET} %s\n" "$*" >&2; }
log_error() { printf "${C_RED}[✗]${C_RESET} %s\n" "$*" >&2; }
# ─────────────────────────────────────────────────────────────────────────
# Arg parsing
# ─────────────────────────────────────────────────────────────────────────
while [ $# -gt 0 ]; do
case "$1" in
--skip-chromium) SKIP_CHROMIUM=true ;;
--yes|-y) ASSUME_YES=true ;;
-h|--help)
cat <<EOF
Bootstrap Hermes Agent browser tools.
Installs Node.js (into ~/.hermes/node/), the agent-browser npm package,
and the Playwright Chromium browser engine.
Options:
--skip-chromium Install Node + agent-browser but skip Chromium download
--yes, -y Accept the ~400 MB Chromium download without prompting
-h, --help Show this help
Environment:
HERMES_HOME Override Hermes data dir (default: \$HOME/.hermes)
EOF
exit 0
;;
*)
log_error "Unknown option: $1"
exit 2
;;
esac
shift
done
# ─────────────────────────────────────────────────────────────────────────
# OS / arch detection
# ─────────────────────────────────────────────────────────────────────────
OS="unknown"
case "$(uname -s)" in
Linux*) OS="linux" ;;
Darwin*) OS="macos" ;;
*)
log_error "Unsupported OS: $(uname -s)"
log_info "Windows users: run scripts/bootstrap_browser_tools.ps1 in PowerShell."
exit 1
;;
esac
NODE_ARCH=""
case "$(uname -m)" in
x86_64) NODE_ARCH="x64" ;;
aarch64|arm64) NODE_ARCH="arm64" ;;
armv7l) NODE_ARCH="armv7l" ;;
*)
log_error "Unsupported architecture: $(uname -m)"
exit 1
;;
esac
NODE_OS=""
case "$OS" in
linux) NODE_OS="linux" ;;
macos) NODE_OS="darwin" ;;
esac
DISTRO=""
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO="${ID:-}"
fi
# ─────────────────────────────────────────────────────────────────────────
# Step 1: Node.js
# ─────────────────────────────────────────────────────────────────────────
ensure_node() {
# Already on PATH and recent enough?
if command -v node >/dev/null 2>&1; then
local found_ver major
found_ver=$(node --version 2>/dev/null)
major=$(echo "$found_ver" | sed -E 's/^v([0-9]+).*/\1/')
if [ -n "$major" ] && [ "$major" -ge 20 ]; then
log_success "Node.js $found_ver found on PATH"
return 0
fi
log_warn "Node.js $found_ver is older than v20 — installing managed Node."
fi
if [ -x "$NODE_PREFIX/bin/node" ]; then
local found_ver
found_ver=$("$NODE_PREFIX/bin/node" --version 2>/dev/null || echo "?")
export PATH="$NODE_PREFIX/bin:$PATH"
log_success "Node.js $found_ver found (Hermes-managed at $NODE_PREFIX)"
return 0
fi
log_info "Installing Node.js $NODE_VERSION LTS into $NODE_PREFIX ..."
local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/"
local tarball_name
tarball_name=$(curl -fsSL "$index_url" \
| grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${NODE_OS}-${NODE_ARCH}\.tar\.xz" \
| head -1)
if [ -z "$tarball_name" ]; then
tarball_name=$(curl -fsSL "$index_url" \
| grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${NODE_OS}-${NODE_ARCH}\.tar\.gz" \
| head -1)
fi
if [ -z "$tarball_name" ]; then
log_error "Could not locate Node.js $NODE_VERSION tarball for $NODE_OS-$NODE_ARCH"
log_info "Install Node 20+ manually: https://nodejs.org/en/download/"
return 1
fi
local tmp_dir
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' RETURN
log_info "Downloading $tarball_name ..."
if ! curl -fsSL "${index_url}${tarball_name}" -o "$tmp_dir/$tarball_name"; then
log_error "Node.js download failed"
return 1
fi
if [[ "$tarball_name" == *.tar.xz ]]; then
tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir"
else
tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir"
fi
local extracted_dir
extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1)
if [ ! -d "$extracted_dir" ]; then
log_error "Node.js extraction failed"
return 1
fi
mkdir -p "$HERMES_HOME"
rm -rf "$NODE_PREFIX"
mv "$extracted_dir" "$NODE_PREFIX"
export PATH="$NODE_PREFIX/bin:$PATH"
local installed_ver
installed_ver=$("$NODE_PREFIX/bin/node" --version 2>/dev/null || echo "?")
log_success "Node.js $installed_ver installed to $NODE_PREFIX"
}
# ─────────────────────────────────────────────────────────────────────────
# Step 2: agent-browser + @askjo/camofox-browser via global npm install
# ─────────────────────────────────────────────────────────────────────────
ensure_agent_browser() {
if ! command -v npm >/dev/null 2>&1; then
log_error "npm not on PATH after Node install — aborting"
return 1
fi
# _find_agent_browser() in tools/browser_tool.py walks ~/.hermes/node/bin
# plus a few standard prefixes, so installing globally into the managed
# Node prefix is enough — no PATH manipulation needed from the agent side.
if [ -x "$NODE_PREFIX/bin/agent-browser" ] || command -v agent-browser >/dev/null 2>&1; then
log_success "agent-browser already installed"
return 0
fi
# When the system's `npm` resolves to a root-owned prefix (e.g.
# /usr/lib/node_modules), `npm install -g` fails with EACCES without
# sudo. Force the prefix to the user-writable Hermes-managed Node
# directory so we never need sudo and the agent can always find the
# result. If we installed Node ourselves above, this is a no-op
# (managed Node already uses $NODE_PREFIX). If the user has system
# Node, we still drop agent-browser under $NODE_PREFIX/bin/ — which
# is exactly where _browser_candidate_path_dirs() looks first.
mkdir -p "$NODE_PREFIX"
log_info "Installing agent-browser (npm, prefix=$NODE_PREFIX)..."
if ! npm install -g --prefix "$NODE_PREFIX" --silent \
agent-browser@^0.26.0 \
"@askjo/camofox-browser@^1.5.2"; then
log_error "npm install -g agent-browser failed"
return 1
fi
# macOS/Linux global installs place the shim into $NODE_PREFIX/bin/.
# Add it to PATH for any subsequent steps (npx playwright).
export PATH="$NODE_PREFIX/bin:$PATH"
log_success "agent-browser installed to $NODE_PREFIX/bin/"
}
# ─────────────────────────────────────────────────────────────────────────
# Step 3: Playwright Chromium
# ─────────────────────────────────────────────────────────────────────────
confirm_chromium_download() {
if [ "$ASSUME_YES" = true ]; then return 0; fi
if [ ! -t 0 ]; then
log_warn "Non-interactive shell — skipping Chromium prompt."
log_info "Re-run with --yes to install Chromium (~400 MB download)."
return 1
fi
printf "Install Playwright Chromium (~400 MB download)? [y/N] "
local reply=""
read -r reply || reply=""
case "$reply" in
y|Y|yes|YES) return 0 ;;
*) return 1 ;;
esac
}
# Detect a usable system Chrome/Chromium. agent-browser's Chrome engine can
# use it instead of downloading Playwright's bundled Chromium, saving the
# download cost. Returns the path or empty string.
find_system_browser() {
local candidate
for candidate in google-chrome google-chrome-stable chromium chromium-browser chrome; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
# macOS app-bundle locations
if [ "$OS" = "macos" ]; then
for candidate in \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium" ; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
fi
return 1
}
write_browser_env() {
local browser_path="$1"
local env_file="$HERMES_HOME/.env"
mkdir -p "$HERMES_HOME"
if [ -f "$env_file" ] && grep -q "^AGENT_BROWSER_EXECUTABLE_PATH=" "$env_file"; then
return 0
fi
{
echo ""
echo "# Hermes Agent browser tools — use the system Chrome/Chromium binary."
echo "AGENT_BROWSER_EXECUTABLE_PATH=$browser_path"
} >> "$env_file"
log_success "Configured browser tools to use $browser_path"
}
ensure_chromium() {
if [ "$SKIP_CHROMIUM" = true ]; then
log_info "Skipping Chromium install (--skip-chromium)"
return 0
fi
local system_browser
system_browser="$(find_system_browser 2>/dev/null || true)"
if [ -n "$system_browser" ]; then
log_success "Found system browser: $system_browser"
log_info "Skipping Playwright Chromium download; agent-browser will use it."
write_browser_env "$system_browser"
return 0
fi
if ! confirm_chromium_download; then
log_info "Chromium install skipped. Browser tools will only work if you"
log_info "set AGENT_BROWSER_EXECUTABLE_PATH or install Chromium later."
return 0
fi
if ! command -v npx >/dev/null 2>&1; then
log_error "npx not on PATH — cannot install Playwright Chromium"
return 1
fi
log_info "Installing Playwright Chromium (~400 MB) ..."
# On apt-based distros, --with-deps requires sudo. Try non-interactively
# only — never prompt — and fall back to the bare browser-only install.
local installed=false
if [ "$OS" = "linux" ]; then
case "$DISTRO" in
ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot)
if [ "$(id -u)" -eq 0 ] || (command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null); then
log_info "Installing system deps with --with-deps (sudo available)"
if npx --yes playwright install --with-deps chromium; then
installed=true
fi
else
log_warn "sudo not available non-interactively — installing Chromium without system deps."
log_info "If browser tools fail to launch, an administrator should run:"
log_info " sudo npx playwright install-deps chromium"
fi
;;
arch|manjaro|cachyos|endeavouros|garuda)
log_info "Arch-family system dependencies are not auto-installed."
log_info "If launch fails, run: sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib"
;;
fedora|rhel|centos|rocky|alma)
log_info "Fedora/RHEL system dependencies are not auto-installed."
log_info "If launch fails, run: sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib"
;;
opensuse*|sles)
log_info "openSUSE system dependencies are not auto-installed."
;;
esac
fi
if [ "$installed" = false ]; then
if npx --yes playwright install chromium; then
installed=true
fi
fi
if [ "$installed" = true ]; then
log_success "Playwright Chromium installed"
else
log_error "Playwright Chromium install failed"
log_info "Try again later: npx --yes playwright install chromium"
return 1
fi
}
# ─────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────
main() {
log_info "Hermes Agent: bootstrapping browser tools"
log_info " HERMES_HOME = $HERMES_HOME"
log_info " OS / arch = $NODE_OS-$NODE_ARCH ${DISTRO:+($DISTRO)}"
ensure_node
ensure_agent_browser
ensure_chromium
log_success "Browser tools setup complete."
log_info "Hermes Agent will pick up agent-browser from $NODE_PREFIX/bin/ on next launch."
}
main

View file

@ -182,56 +182,31 @@ def _run_setup() -> None:
def _run_setup_browser(assume_yes: bool = False) -> int:
"""Bootstrap agent-browser + Playwright Chromium for the registry-install path.
"""Bootstrap agent-browser + Chromium.
Shells out to the bundled platform-specific bootstrap script
(acp_adapter/bootstrap/bootstrap_browser_tools.{sh,ps1}) so the install
logic lives in one place readable, debuggable, and shareable with
install.sh / install.ps1 if we ever want to call it from there too.
Routes through dep_ensure -> install.{sh,ps1} --ensure, sharing code
with ``hermes postinstall`` and the runtime lazy installer.
Returns the script's exit code (0 on success).
Returns 0 on success, 1 on failure.
"""
import platform
import subprocess
from hermes_cli.dep_ensure import ensure_dependency
bootstrap_dir = Path(__file__).resolve().parent / "bootstrap"
if platform.system() == "Windows":
script = bootstrap_dir / "bootstrap_browser_tools.ps1"
if not script.is_file():
print(
f"Bootstrap script not found at {script} — wheel may be incomplete.",
file=sys.stderr,
)
return 1
cmd = [
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", str(script),
]
if assume_yes:
cmd.append("-Yes")
else:
script = bootstrap_dir / "bootstrap_browser_tools.sh"
if not script.is_file():
print(
f"Bootstrap script not found at {script} — wheel may be incomplete.",
file=sys.stderr,
)
return 1
cmd = ["bash", str(script)]
if assume_yes:
cmd.append("--yes")
# stdio is inherited so the user sees the bootstrap's progress live.
try:
result = subprocess.run(cmd, check=False)
except FileNotFoundError as exc:
# bash / powershell.exe not on PATH
print(f"Could not launch browser bootstrap: {exc}", file=sys.stderr)
node_ok = ensure_dependency("node", interactive=not assume_yes)
if not node_ok:
print("Node.js installation failed — cannot proceed with browser tools.",
file=sys.stderr)
return 1
browser_ok = ensure_dependency("browser", interactive=not assume_yes)
if not browser_ok:
print("Browser tools installation failed.", file=sys.stderr)
return 1
return 0
except OSError as exc:
print(f"Browser bootstrap failed: {exc}", file=sys.stderr)
return 1
return result.returncode
def main(argv: list[str] | None = None) -> None:

View file

@ -1512,6 +1512,17 @@ find_system_browser() {
fi
done
if [ "$(uname)" = "Darwin" ]; then
for app in \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium"; do
if [ -x "$app" ]; then
echo "$app"
return 0
fi
done
fi
return 1
}
@ -1534,10 +1545,15 @@ configure_browser_env_from_system_browser() {
browser_path="$(find_system_browser 2>/dev/null || true)"
fi
if [ -z "$browser_path" ] || [ ! -f "$env_file" ]; then
if [ -z "$browser_path" ]; then
return 0
fi
mkdir -p "$HERMES_HOME"
if [ ! -f "$env_file" ]; then
touch "$env_file"
fi
if grep -q '^AGENT_BROWSER_EXECUTABLE_PATH=' "$env_file" 2>/dev/null; then
log_info "AGENT_BROWSER_EXECUTABLE_PATH already configured"
return 0
@ -1888,6 +1904,73 @@ print_success() {
fi
}
ensure_browser() {
if ! command -v node >/dev/null 2>&1; then
local node_bin="$HERMES_HOME/node/bin/node"
if [ -x "$node_bin" ]; then
export PATH="$HERMES_HOME/node/bin:$PATH"
else
log_error "Node.js not found. Run with --ensure node first."
return 1
fi
fi
local npm_bin
npm_bin="$(command -v npm 2>/dev/null || echo "$HERMES_HOME/node/bin/npm")"
if [ ! -x "$npm_bin" ]; then
log_error "npm not found"
return 1
fi
log_info "Installing agent-browser..."
local log_file
log_file="$(mktemp)"
if ! "$npm_bin" install -g --prefix "$HERMES_HOME/node" --silent --ignore-scripts \
"agent-browser@^0.26.0" \
"@askjo/camofox-browser@^1.5.2" \
>"$log_file" 2>&1; then
log_error "npm install failed:"
cat "$log_file" >&2
rm -f "$log_file"
return 1
fi
rm -f "$log_file"
export PATH="$HERMES_HOME/node/bin:$PATH"
local sys_browser
sys_browser="$(find_system_browser 2>/dev/null || true)"
if [ -n "$sys_browser" ]; then
configure_browser_env_from_system_browser "$sys_browser"
log_info "System browser detected -- skipping Chromium download"
return 0
fi
log_info "Installing Chromium via agent-browser install..."
local ab_bin="$HERMES_HOME/node/bin/agent-browser"
if [ -x "$ab_bin" ]; then
"$ab_bin" install 2>/dev/null || {
log_warn "Chromium install failed. Browser tools may not work without a system browser."
# OS-specific hints (detect_os sets $DISTRO)
case "${DISTRO:-unknown}" in
ubuntu|debian)
log_info "Try: sudo apt-get install -y chromium-browser"
;;
arch)
log_info "Try: sudo pacman -S chromium"
;;
fedora|rhel|centos)
log_info "Try: sudo dnf install -y chromium"
;;
esac
}
else
log_warn "agent-browser not found at $ab_bin"
fi
return 0
}
ensure_mode() {
detect_os
@ -1901,19 +1984,7 @@ ensure_mode() {
browser)
check_node
if [ "$HAS_NODE" = true ]; then
DETECTED_BROWSER_EXECUTABLE="$(find_system_browser 2>/dev/null || true)"
if [ -z "$DETECTED_BROWSER_EXECUTABLE" ]; then
log_info "Installing agent-browser + Chromium..."
npm_bin="$(command -v npm 2>/dev/null || echo "")"
if [ -n "$npm_bin" ]; then
local agent_browser_dir="$HERMES_HOME/node_modules"
mkdir -p "$agent_browser_dir"
"$npm_bin" install --prefix "$HERMES_HOME" agent-browser 2>/dev/null || true
npx playwright install chromium 2>/dev/null || true
fi
else
log_success "System browser found: $DETECTED_BROWSER_EXECUTABLE"
fi
ensure_browser
fi
;;
ripgrep)
@ -1948,16 +2019,7 @@ postinstall_mode() {
install_system_packages
if [ "$HAS_NODE" = true ] && [ "$SKIP_BROWSER" = false ]; then
DETECTED_BROWSER_EXECUTABLE="$(find_system_browser 2>/dev/null || true)"
if [ -z "$DETECTED_BROWSER_EXECUTABLE" ]; then
log_info "Installing browser engine..."
npm_bin="$(command -v npm 2>/dev/null || echo "")"
if [ -n "$npm_bin" ]; then
npx playwright install chromium 2>/dev/null || true
fi
else
log_success "System browser found: $DETECTED_BROWSER_EXECUTABLE"
fi
ensure_browser
fi
HERMES_CMD="$(command -v hermes 2>/dev/null || echo "")"

View file

@ -94,103 +94,62 @@ def test_main_setup_skips_browser_prompt_on_no(monkeypatch):
assert called == []
def test_main_setup_browser_invokes_bundled_script(monkeypatch):
"""`hermes-acp --setup-browser` must shell out to the bundled bootstrap
script never reimplement the install logic inline."""
monkeypatch.setattr("platform.system", lambda: "Linux")
def test_main_setup_browser_calls_ensure_dependency(monkeypatch):
"""`hermes-acp --setup-browser` routes through dep_ensure.ensure_dependency."""
calls = []
captured = {}
def fake_ensure(dep, interactive=True):
calls.append((dep, interactive))
return True
def fake_run(cmd, check=False):
captured["cmd"] = cmd
class _R:
returncode = 0
return _R()
monkeypatch.setattr("subprocess.run", fake_run)
monkeypatch.setattr("hermes_cli.dep_ensure.ensure_dependency", fake_ensure)
entry.main(["--setup-browser"])
assert captured["cmd"][0] == "bash"
assert captured["cmd"][1].endswith("bootstrap_browser_tools.sh")
# --yes is NOT passed when the flag is absent.
assert "--yes" not in captured["cmd"]
assert ("node", True) in calls
assert ("browser", True) in calls
def test_main_setup_browser_forwards_yes_flag(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
"""--yes suppresses interactive prompts in ensure_dependency."""
calls = []
captured = {}
def fake_ensure(dep, interactive=True):
calls.append((dep, interactive))
return True
def fake_run(cmd, check=False):
captured["cmd"] = cmd
class _R:
returncode = 0
return _R()
monkeypatch.setattr("subprocess.run", fake_run)
monkeypatch.setattr("hermes_cli.dep_ensure.ensure_dependency", fake_ensure)
entry.main(["--setup-browser", "--yes"])
assert "--yes" in captured["cmd"]
assert ("node", False) in calls
assert ("browser", False) in calls
def test_main_setup_browser_uses_powershell_on_windows(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Windows")
def test_main_setup_browser_stops_on_node_failure(monkeypatch):
"""If node install fails, browser install is not attempted."""
calls = []
captured = {}
def fake_ensure(dep, interactive=True):
calls.append(dep)
return dep != "node" # node fails
def fake_run(cmd, check=False):
captured["cmd"] = cmd
class _R:
returncode = 0
return _R()
monkeypatch.setattr("subprocess.run", fake_run)
entry.main(["--setup-browser", "--yes"])
assert captured["cmd"][0] == "powershell.exe"
assert any(part.endswith("bootstrap_browser_tools.ps1") for part in captured["cmd"])
assert "-Yes" in captured["cmd"]
def test_main_setup_browser_propagates_failure(monkeypatch):
monkeypatch.setattr("platform.system", lambda: "Linux")
class _R:
returncode = 7
monkeypatch.setattr("subprocess.run", lambda cmd, check=False: _R())
monkeypatch.setattr("hermes_cli.dep_ensure.ensure_dependency", fake_ensure)
with pytest.raises(SystemExit) as excinfo:
entry.main(["--setup-browser"])
assert excinfo.value.code == 7
assert excinfo.value.code == 1
assert "node" in calls
assert "browser" not in calls
def test_bootstrap_scripts_ship_with_package():
"""The package-data wiring (pyproject.toml) must include the bootstrap
scripts otherwise `--setup-browser` 404s at runtime."""
from pathlib import Path
def test_main_setup_browser_propagates_browser_failure(monkeypatch):
"""If browser install fails, exit code is 1."""
def fake_ensure(dep, interactive=True):
return dep != "browser" # browser fails
bootstrap_dir = Path(entry.__file__).resolve().parent / "bootstrap"
sh = bootstrap_dir / "bootstrap_browser_tools.sh"
ps1 = bootstrap_dir / "bootstrap_browser_tools.ps1"
monkeypatch.setattr("hermes_cli.dep_ensure.ensure_dependency", fake_ensure)
assert sh.is_file(), f"missing bundled script: {sh}"
assert ps1.is_file(), f"missing bundled script: {ps1}"
sh_text = sh.read_text(encoding="utf-8")
ps1_text = ps1.read_text(encoding="utf-8")
# Sanity: scripts know how to find the Hermes-managed Node prefix.
assert "HERMES_HOME" in sh_text
assert "agent-browser" in sh_text
assert "HermesHome" in ps1_text
assert "agent-browser" in ps1_text
with pytest.raises(SystemExit) as excinfo:
entry.main(["--setup-browser"])
assert excinfo.value.code == 1