diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 69fa685138..44c3a671a3 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -1642,11 +1642,52 @@ def _run_cua_driver_installer( pass +def _ensure_browser_use_cli(*, verbose_hints: bool = False) -> None: + """Install the Browser Use CLI if it isn't already runnable. + + The Browser Use CLI 3.0 is the primary driver engine for EVERY browser + backend except Camofox (which is Firefox-based with no CDP surface, so + the CDP-only browser-use harness cannot drive it). Local, Browserbase, + Firecrawl, and the Nous-managed cloud rows all execute through + ``browser_exec`` when the CLI is runnable — so every one of those + picker selections must attempt this install, not just the explicit + "Browser Use" row. Failure is non-fatal: ``browser_exec`` can still run + zero-install via ``uvx browser-use``, and the built-in browser tools + remain the final fallback. + """ + if shutil.which("browser-use"): + _print_success(" browser-use CLI found on PATH") + else: + _print_info(" Installing browser-use CLI (uv tool install browser-use)...") + try: + from tools.browser_use_cli import install_cli + + ok, message = install_cli() + except Exception as exc: # pragma: no cover — defensive + ok, message = False, f"install failed: {exc}" + if ok: + _print_success(f" {message}") + else: + for line in str(message).splitlines(): + _print_warning(f" {line[:200]}") + if shutil.which("uvx"): + _print_info(" Falling back to zero-install runs via `uvx browser-use`") + else: + _print_info(" Install manually: uv tool install browser-use (https://docs.astral.sh/uv/)") + if verbose_hints: + _print_info(" Local Chrome needs remote debugging: chrome://inspect/#remote-debugging") + _print_info(" Cloud browsers: browser-use auth login (or set BROWSER_USE_API_KEY)") + + def _run_post_setup(post_setup_key: str): """Run post-setup hooks for tools that need extra installation steps.""" from hermes_constants import find_node_executable if post_setup_key in {"agent_browser", "browserbase"}: + # Every non-Camofox browser backend drives through the Browser Use + # CLI when it's runnable — install it here too, not only on the + # explicit "Browser Use" picker row. + _ensure_browser_use_cli() # agent-browser is no longer a root package.json dependency (#43564) # — it resolves lazily via npx (or a global/Hermes-managed install) # instead of a local `npm install`, so there's no node_modules/ @@ -1753,27 +1794,7 @@ def _run_post_setup(post_setup_key: str): _print_info(" Run manually: npx agent-browser install --with-deps") elif post_setup_key == "browser_use_cli": - if shutil.which("browser-use"): - _print_success(" browser-use CLI found on PATH") - else: - _print_info(" Installing browser-use CLI (uv tool install browser-use)...") - try: - from tools.browser_use_cli import install_cli - - ok, message = install_cli() - except Exception as exc: # pragma: no cover — defensive - ok, message = False, f"install failed: {exc}" - if ok: - _print_success(f" {message}") - else: - for line in str(message).splitlines(): - _print_warning(f" {line[:200]}") - if shutil.which("uvx"): - _print_info(" Falling back to zero-install runs via `uvx browser-use`") - else: - _print_info(" Install manually: uv tool install browser-use (https://docs.astral.sh/uv/)") - _print_info(" Local Chrome needs remote debugging: chrome://inspect/#remote-debugging") - _print_info(" Cloud browsers: browser-use auth login (or set BROWSER_USE_API_KEY)") + _ensure_browser_use_cli(verbose_hints=True) elif post_setup_key == "camofox": camofox_dir = PROJECT_ROOT / "node_modules" / "@askjo" / "camofox-browser" diff --git a/tests/hermes_cli/test_tools_config.py b/tests/hermes_cli/test_tools_config.py index 1f5328ec58..3b9993a8b7 100644 --- a/tests/hermes_cli/test_tools_config.py +++ b/tests/hermes_cli/test_tools_config.py @@ -359,6 +359,16 @@ class TestAgentBrowserPostSetup: (and Windows .cmd-shim) lookup. """ + @pytest.fixture(autouse=True) + def _stub_browser_use_install(self): + """Both browser branches now attempt a Browser Use CLI install first + (the CLI drives every non-Camofox backend). Stub it so these + Chromium-branch tests never bootstrap uv / hit the network, and so + their print/subprocess assertions stay scoped to the agent-browser + logic under test.""" + with patch("hermes_cli.tools_config._ensure_browser_use_cli") as stub: + yield stub + def test_warns_when_neither_npx_nor_agent_browser_on_path(self): with patch("shutil.which", return_value=None), patch( "subprocess.run" @@ -616,6 +626,55 @@ class TestAgentBrowserPostSetup: assert any("timed out" in c.args[0] for c in warn.call_args_list) +class TestBrowserUseCliInstalledForAllNonCamofoxBackends: + """The Browser Use CLI is the primary driver engine for every browser + backend except Camofox — so EVERY browser picker selection except + Camofox must attempt the CLI install, not just the explicit + "Browser Use" row.""" + + @pytest.mark.parametrize("key", ["agent_browser", "browserbase", "browser_use_cli"]) + def test_browser_post_setup_attempts_cli_install(self, key): + with patch("hermes_cli.tools_config._ensure_browser_use_cli") as ensure, patch( + "shutil.which", return_value=None + ), patch("subprocess.run"): + _run_post_setup(key) + ensure.assert_called_once() + + def test_camofox_post_setup_never_touches_browser_use(self): + """Camofox is Firefox-based with no CDP surface; the CDP-only + browser-use harness cannot drive it, so its setup must not pull + the CLI in.""" + with patch("hermes_cli.tools_config._ensure_browser_use_cli") as ensure, patch( + "hermes_constants.find_node_executable", return_value=None + ), patch("subprocess.run"): + _run_post_setup("camofox") + ensure.assert_not_called() + + def test_ensure_helper_short_circuits_when_cli_on_path(self): + with patch( + "hermes_cli.tools_config.shutil.which", return_value="/usr/bin/browser-use" + ), patch("tools.browser_use_cli.install_cli") as install: + from hermes_cli.tools_config import _ensure_browser_use_cli + + _ensure_browser_use_cli() + install.assert_not_called() + + def test_ensure_helper_install_failure_is_non_fatal(self): + """A failed install must warn and fall back, never raise — the + uvx zero-install path and the built-in tools remain available.""" + from hermes_cli.tools_config import _ensure_browser_use_cli + + with patch( + "hermes_cli.tools_config.shutil.which", return_value=None + ), patch( + "tools.browser_use_cli.install_cli", + return_value=(False, "`uv tool install browser-use` failed:\nboom"), + ), patch("hermes_cli.tools_config._print_warning") as warn: + _ensure_browser_use_cli() # must not raise + + assert any("failed" in c.args[0] for c in warn.call_args_list) + + class TestImagegenBackendRegistry: """IMAGEGEN_BACKENDS tags drive the model picker flow in tools_config."""