diff --git a/scripts/install.sh b/scripts/install.sh index 72cc81637da..25d566c9881 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -64,6 +64,7 @@ NODE_VERSION="22" # data still at /root/.hermes (HERMES_HOME). Matches Claude Code / Codex CLI # and keeps Docker bind-mounted /root/ volumes lean. ROOT_FHS_LAYOUT=false +DETECTED_BROWSER_EXECUTABLE="" # Options USE_VENV=true @@ -1421,6 +1422,7 @@ copy_config_templates() { else log_info "~/.hermes/.env already exists, keeping it" fi + configure_browser_env_from_system_browser # Create config.yaml at ~/.hermes/config.yaml (top level, easy to find) if [ ! -f "$HERMES_HOME/config.yaml" ]; then @@ -1469,6 +1471,68 @@ SOUL_EOF fi } +find_system_browser() { + # Prefer a user-specified browser path, then common Linux/macOS Chrome and + # Chromium command names. Arch-family distributions commonly ship plain + # `chromium`, while Debian-family systems often use `chromium-browser`. + if [ -n "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then + if [ -x "$AGENT_BROWSER_EXECUTABLE_PATH" ]; then + echo "$AGENT_BROWSER_EXECUTABLE_PATH" + return 0 + fi + if command -v "$AGENT_BROWSER_EXECUTABLE_PATH" >/dev/null 2>&1; then + command -v "$AGENT_BROWSER_EXECUTABLE_PATH" + return 0 + fi + fi + + 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 + + return 1 +} + +run_browser_install_with_timeout() { + local timeout_seconds="$1" + shift + + if command -v timeout >/dev/null 2>&1; then + timeout "$timeout_seconds" "$@" + else + "$@" + fi +} + +configure_browser_env_from_system_browser() { + local env_file="$HERMES_HOME/.env" + local browser_path="${DETECTED_BROWSER_EXECUTABLE:-}" + + if [ -z "$browser_path" ]; then + browser_path="$(find_system_browser 2>/dev/null || true)" + fi + + if [ -z "$browser_path" ] || [ ! -f "$env_file" ]; then + return 0 + fi + + if grep -q '^AGENT_BROWSER_EXECUTABLE_PATH=' "$env_file" 2>/dev/null; then + log_info "AGENT_BROWSER_EXECUTABLE_PATH already configured" + 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" +} + install_node_deps() { if [ "$HAS_NODE" = false ]; then log_info "Skipping Node.js dependencies (Node not installed)" @@ -1495,57 +1559,63 @@ install_node_deps() { # For Arch/Manjaro we install the system libs via pacman first. # Other systems must install Chromium dependencies manually. log_info "Installing browser engine (Playwright Chromium)..." - case "$DISTRO" in - ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot) - log_info "Playwright may request sudo to install browser system dependencies (shared libraries)." - log_info "This is standard Playwright setup — Hermes itself does not require root access." - cd "$INSTALL_DIR" && npx playwright install --with-deps chromium 2>/dev/null || { - log_warn "Playwright browser installation failed — browser tools will not work." - log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium" - } - ;; - arch|manjaro) - if command -v pacman &> /dev/null; then - log_info "Arch/Manjaro detected — installing Chromium system dependencies via pacman..." - if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then - sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \ - nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true - elif [ "$(id -u)" -eq 0 ]; then - pacman -S --noconfirm --needed \ - nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true - else - log_warn "Cannot install browser deps without sudo. Run manually:" - log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib" + DETECTED_BROWSER_EXECUTABLE="$(find_system_browser 2>/dev/null || true)" + if [ -n "$DETECTED_BROWSER_EXECUTABLE" ]; then + log_success "Found system Chrome/Chromium at $DETECTED_BROWSER_EXECUTABLE" + log_info "Skipping Playwright browser download; Hermes will use the system browser." + else + case "$DISTRO" in + ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot) + log_info "Playwright may request sudo to install browser system dependencies (shared libraries)." + log_info "This is standard Playwright setup — Hermes itself does not require root access." + cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install --with-deps chromium 2>/dev/null || { + log_warn "Playwright browser installation failed — browser tools will not work." + log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium" + } + ;; + arch|manjaro|cachyos|endeavouros|garuda) + if command -v pacman &> /dev/null; then + log_info "Arch-family distro detected — installing Chromium system dependencies via pacman..." + if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then + sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \ + nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true + elif [ "$(id -u)" -eq 0 ]; then + pacman -S --noconfirm --needed \ + nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true + else + log_warn "Cannot install browser deps without sudo. Run manually:" + log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib" + fi fi - fi - cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { - log_warn "Playwright browser installation failed — browser tools will not work." - } - ;; - fedora|rhel|centos|rocky|alma) - log_warn "Playwright does not support automatic dependency installation on RPM-based systems." - log_info "Install Chromium system dependencies manually before using browser tools:" - log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib" - cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { - log_warn "Playwright browser installation failed — install dependencies above and retry." - } - ;; - opensuse*|sles) - log_warn "Playwright does not support automatic dependency installation on zypper-based systems." - log_info "Install Chromium system dependencies manually before using browser tools:" - log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2" - cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { - log_warn "Playwright browser installation failed — install dependencies above and retry." - } - ;; - *) - log_warn "Playwright does not support automatic dependency installation on $DISTRO." - log_info "Install Chromium/browser system dependencies for your distribution, then run:" - log_info " cd $INSTALL_DIR && npx playwright install chromium" - log_info "Browser tools will not work until dependencies are installed." - cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || true - ;; - esac + cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || { + log_warn "Playwright browser installation failed — browser tools will not work." + } + ;; + fedora|rhel|centos|rocky|alma) + log_warn "Playwright does not support automatic dependency installation on RPM-based systems." + log_info "Install Chromium system dependencies manually before using browser tools:" + log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib" + cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || { + log_warn "Playwright browser installation failed — install dependencies above and retry." + } + ;; + opensuse*|sles) + log_warn "Playwright does not support automatic dependency installation on zypper-based systems." + log_info "Install Chromium system dependencies manually before using browser tools:" + log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2" + cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || { + log_warn "Playwright browser installation failed — install dependencies above and retry." + } + ;; + *) + log_warn "Playwright does not support automatic dependency installation on $DISTRO." + log_info "Install Chromium/browser system dependencies for your distribution, then run:" + log_info " cd $INSTALL_DIR && npx playwright install chromium" + log_info "Browser tools will not work until dependencies are installed." + cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || true + ;; + esac + fi log_success "Browser engine setup complete" fi diff --git a/tests/test_install_sh_browser_install.py b/tests/test_install_sh_browser_install.py new file mode 100644 index 00000000000..4e1908e4294 --- /dev/null +++ b/tests/test_install_sh_browser_install.py @@ -0,0 +1,35 @@ +"""Regression tests for install.sh browser setup. + +Browser automation is optional. The installer should not leave Hermes +half-installed just because Playwright's managed Chromium download hangs on an +unsupported distribution. +""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" + + +def test_install_script_skips_playwright_download_when_system_browser_exists() -> None: + text = INSTALL_SH.read_text() + + assert "find_system_browser()" in text + assert "google-chrome google-chrome-stable chromium chromium-browser chrome" in text + assert "Skipping Playwright browser download; Hermes will use the system browser." in text + + +def test_install_script_persists_system_browser_for_agent_browser() -> None: + text = INSTALL_SH.read_text() + + assert "configure_browser_env_from_system_browser()" in text + assert "AGENT_BROWSER_EXECUTABLE_PATH=$browser_path" in text + + +def test_playwright_installs_are_timeout_guarded() -> None: + text = INSTALL_SH.read_text() + + assert "run_browser_install_with_timeout()" in text + assert "run_browser_install_with_timeout 600 npx playwright install chromium" in text + assert "run_browser_install_with_timeout 600 npx playwright install --with-deps chromium" in text diff --git a/tests/tools/test_browser_chromium_check.py b/tests/tools/test_browser_chromium_check.py index ef3fca4352f..760dfa5d230 100644 --- a/tests/tools/test_browser_chromium_check.py +++ b/tests/tools/test_browser_chromium_check.py @@ -41,6 +41,16 @@ class TestChromiumSearchRoots: class TestChromiumInstalled: + def test_true_when_plain_chromium_on_path(self, monkeypatch): + monkeypatch.delenv("AGENT_BROWSER_EXECUTABLE_PATH", raising=False) + monkeypatch.setattr( + bt.shutil, + "which", + lambda name: "/usr/bin/chromium" if name == "chromium" else None, + ) + + assert bt._chromium_installed() is True + def test_true_when_chromium_dir_present(self, monkeypatch, tmp_path): monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(tmp_path)) (tmp_path / "chromium-1208").mkdir() @@ -108,4 +118,3 @@ class TestRunBrowserCommandChromiumGuard: """ - diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 40ba7cab25c..79a6c7e6172 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -3381,8 +3381,8 @@ def _chromium_installed() -> bool: 1. ``AGENT_BROWSER_EXECUTABLE_PATH`` env var — the official way to point agent-browser at a pre-installed Chrome/Chromium. - 2. System Chrome/Chromium in PATH (``google-chrome``, ``chromium-browser``, - ``chrome``). + 2. System Chrome/Chromium in PATH (``google-chrome``, ``chromium``, + ``chromium-browser``, ``chrome``). 3. Playwright's browser cache (current logic) — directories containing ``chromium-*`` or ``chromium_headless_shell-*``. @@ -3405,7 +3405,12 @@ def _chromium_installed() -> bool: return True # 2. System Chrome/Chromium in PATH (common names) - system_chrome = shutil.which("google-chrome") or shutil.which("chromium-browser") or shutil.which("chrome") + system_chrome = ( + shutil.which("google-chrome") + or shutil.which("chromium") + or shutil.which("chromium-browser") + or shutil.which("chrome") + ) if system_chrome: _cached_chromium_installed = True return True