diff --git a/acp_adapter/bootstrap/__init__.py b/acp_adapter/bootstrap/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 b/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 deleted file mode 100644 index f840fd2d559..00000000000 --- a/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 +++ /dev/null @@ -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." diff --git a/acp_adapter/bootstrap/bootstrap_browser_tools.sh b/acp_adapter/bootstrap/bootstrap_browser_tools.sh deleted file mode 100755 index 9981069a6af..00000000000 --- a/acp_adapter/bootstrap/bootstrap_browser_tools.sh +++ /dev/null @@ -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 </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 diff --git a/acp_adapter/entry.py b/acp_adapter/entry.py index cf5c2ba9cfb..9ce6281824c 100644 --- a/acp_adapter/entry.py +++ b/acp_adapter/entry.py @@ -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: diff --git a/scripts/install.sh b/scripts/install.sh index c34c64267c6..3ece561a86f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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 "")" diff --git a/tests/acp/test_entry.py b/tests/acp/test_entry.py index 81d30cd868c..1d881565bd9 100644 --- a/tests/acp/test_entry.py +++ b/tests/acp/test_entry.py @@ -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