feat(browser): make Browser Use mode the default browser backend
An unset browser.backend ("") now resolves to Browser Use mode whenever
the browser-use CLI is runnable (installed binary or uvx); otherwise the
built-in browser tools are kept so browsing never silently breaks.
Camofox setups always keep the built-in tools (no CDP surface), and
backend: off (including YAML 1.1 bare off -> False) forces the built-in
stack. hermes tools row highlighting follows the same effective-mode
resolution, and tests/tools/ pins CLI discovery off so host uvx installs
can't flip built-in-browser tests.
This commit is contained in:
parent
37e46c774c
commit
8d8bc85dca
|
|
@ -404,10 +404,15 @@ DEFAULT_CONFIG = {
|
|||
|
||||
"browser": {
|
||||
# Browser tool implementation.
|
||||
# "" — built-in browser tools (browser_navigate, browser_click, …)
|
||||
# "browser-use" — Browser Use mode: one browser_exec tool driving the
|
||||
# Browser Use CLI 3.0 (local Chrome over CDP or Browser
|
||||
# Use cloud browsers)
|
||||
# "" — DEFAULT: Browser Use mode when the browser-use CLI
|
||||
# (or uvx) is available; otherwise the built-in
|
||||
# browser tools. Camofox setups always keep the
|
||||
# built-in tools (no CDP surface).
|
||||
# "browser-use" — force Browser Use mode: one browser_exec tool
|
||||
# driving the Browser Use CLI 3.0 over any CDP
|
||||
# backend (local Chrome, cloud browsers)
|
||||
# "off" — force the built-in browser tools
|
||||
# (browser_navigate, browser_click, …)
|
||||
"backend": "",
|
||||
"inactivity_timeout": 120,
|
||||
"command_timeout": 30, # Timeout for browser commands in seconds (screenshot, navigate, etc.)
|
||||
|
|
|
|||
|
|
@ -3666,17 +3666,35 @@ def _is_provider_active(
|
|||
current = cfg_get(config, "browser", "cloud_provider")
|
||||
return provider["browser_provider"] == current
|
||||
if provider.get("browser_backend"):
|
||||
if cfg_get(config, "browser", "backend") == provider["browser_backend"]:
|
||||
backend = cfg_get(config, "browser", "backend")
|
||||
if backend is False:
|
||||
backend = "off" # YAML 1.1: unquoted `off` parses as boolean False
|
||||
if backend == provider["browser_backend"]:
|
||||
return True
|
||||
# Legacy direct-API Browser Use cloud auto-routes to CLI
|
||||
if backend:
|
||||
return False # explicit other choice ("off", …) wins
|
||||
if provider["browser_backend"] != "browser-use":
|
||||
return False
|
||||
# Backend unset: Browser Use mode is the default — the row is active
|
||||
# whenever the effective mode resolves on (legacy direct-API cloud
|
||||
# config, or CLI runnable and no Camofox).
|
||||
browser_cfg = config.get("browser") if isinstance(config, dict) else None
|
||||
try:
|
||||
from tools.browser_use_cli import is_legacy_browser_use_cloud_config
|
||||
|
||||
browser_cfg = config.get("browser") if isinstance(config, dict) else None
|
||||
return (
|
||||
provider["browser_backend"] == "browser-use"
|
||||
and is_legacy_browser_use_cloud_config(browser_cfg or {})
|
||||
from tools.browser_use_cli import (
|
||||
_find_cli,
|
||||
is_legacy_browser_use_cloud_config,
|
||||
)
|
||||
|
||||
if is_legacy_browser_use_cloud_config(browser_cfg or {}):
|
||||
return True
|
||||
try:
|
||||
from tools.browser_camofox import is_camofox_mode
|
||||
|
||||
if is_camofox_mode():
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return _find_cli() is not None
|
||||
except Exception:
|
||||
return False
|
||||
if provider.get("web_backend"):
|
||||
|
|
|
|||
|
|
@ -13,6 +13,28 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_host_browser_use_cli():
|
||||
"""Keep the host's browser-use/uvx install out of tests.
|
||||
|
||||
Browser Use mode is default-on when the CLI is runnable, so a developer
|
||||
machine with uvx on PATH would silently flip every built-in-browser test
|
||||
into CLI mode. Pin discovery to "not installed"; tests that exercise the
|
||||
CLI path monkeypatch ``bu_cli._find_cli`` themselves.
|
||||
"""
|
||||
try:
|
||||
import tools.browser_use_cli as bu_cli
|
||||
except Exception:
|
||||
yield
|
||||
return
|
||||
# Keep a handle to the real discovery function so TestFindCli (and any
|
||||
# test that wants genuine PATH probing) can restore it explicitly.
|
||||
if not hasattr(bu_cli, "_find_cli_unpatched"):
|
||||
bu_cli._find_cli_unpatched = bu_cli._find_cli
|
||||
with patch.object(bu_cli, "_find_cli", lambda: None):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _materialize_mcp_sdk_symbols():
|
||||
"""Materialize the lazily-imported MCP SDK before each tools test.
|
||||
|
|
|
|||
|
|
@ -38,8 +38,33 @@ def _fake_cli(tmp_path, body):
|
|||
|
||||
|
||||
class TestModeDetection:
|
||||
def test_off_by_default(self, monkeypatch):
|
||||
def test_default_on_when_cli_available(self, monkeypatch):
|
||||
"""Backend unset: Browser Use mode is the default when the CLI runs."""
|
||||
monkeypatch.setattr("hermes_cli.config.read_raw_config", lambda: {})
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: ["/usr/bin/browser-use"])
|
||||
assert bu_cli.is_browser_use_cli_mode() is True
|
||||
|
||||
def test_default_off_when_cli_unavailable(self, monkeypatch):
|
||||
"""Backend unset + no runnable CLI: keep the built-in browser tools."""
|
||||
monkeypatch.setattr("hermes_cli.config.read_raw_config", lambda: {})
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_explicit_off_wins_over_default(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.read_raw_config",
|
||||
lambda: {"browser": {"backend": bu_cli.BACKEND_DISABLED}},
|
||||
)
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: ["/usr/bin/browser-use"])
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_yaml_bool_off_means_disabled(self, monkeypatch):
|
||||
"""YAML 1.1 parses unquoted `off` as False — must mean disabled."""
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.read_raw_config",
|
||||
lambda: {"browser": {"backend": False}},
|
||||
)
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: ["/usr/bin/browser-use"])
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_config_opt_in(self, monkeypatch):
|
||||
|
|
@ -56,11 +81,12 @@ class TestModeDetection:
|
|||
)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_config_read_failure_fails_safe(self, monkeypatch):
|
||||
def test_config_read_failure_uses_default(self, monkeypatch):
|
||||
def boom():
|
||||
raise RuntimeError("config unreadable")
|
||||
|
||||
monkeypatch.setattr("hermes_cli.config.read_raw_config", boom)
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
|
||||
|
|
@ -119,23 +145,26 @@ class TestToolSurfaceSwap:
|
|||
|
||||
|
||||
class TestFindCli:
|
||||
"""The tests/tools conftest pins _find_cli to None (host isolation);
|
||||
exercise the real function via the preserved _find_cli_unpatched."""
|
||||
|
||||
def test_prefers_installed_binary(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
bu_cli.shutil, "which",
|
||||
lambda name: "/usr/local/bin/browser-use" if name == "browser-use" else "/usr/local/bin/uvx",
|
||||
)
|
||||
assert bu_cli._find_cli() == ["/usr/local/bin/browser-use"]
|
||||
assert bu_cli._find_cli_unpatched() == ["/usr/local/bin/browser-use"]
|
||||
|
||||
def test_falls_back_to_uvx(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
bu_cli.shutil, "which",
|
||||
lambda name: "/usr/local/bin/uvx" if name == "uvx" else None,
|
||||
)
|
||||
assert bu_cli._find_cli() == ["/usr/local/bin/uvx", "browser-use"]
|
||||
assert bu_cli._find_cli_unpatched() == ["/usr/local/bin/uvx", "browser-use"]
|
||||
|
||||
def test_none_when_neither_available(self, monkeypatch):
|
||||
monkeypatch.setattr(bu_cli.shutil, "which", lambda name: None)
|
||||
assert bu_cli._find_cli() is None
|
||||
assert bu_cli._find_cli_unpatched() is None
|
||||
|
||||
|
||||
class TestLegacyCloudMigration:
|
||||
|
|
@ -156,10 +185,12 @@ class TestLegacyCloudMigration:
|
|||
lambda: {"browser": {"cloud_provider": "browser-use", "use_gateway": True}},
|
||||
)
|
||||
monkeypatch.setenv("BROWSER_USE_API_KEY", "bu-key")
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_no_api_key_stays_on_legacy_path(self, monkeypatch):
|
||||
monkeypatch.setattr("hermes_cli.config.read_raw_config", lambda: self._LEGACY)
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_camofox_user_does_not_migrate(self, monkeypatch):
|
||||
|
|
@ -201,6 +232,7 @@ class TestLegacyCloudMigration:
|
|||
lambda: {"browser": {"cloud_provider": "browserbase"}},
|
||||
)
|
||||
monkeypatch.setenv("BROWSER_USE_API_KEY", "bu-key")
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_explicit_local_does_not_migrate(self, monkeypatch):
|
||||
|
|
@ -209,6 +241,7 @@ class TestLegacyCloudMigration:
|
|||
lambda: {"browser": {"cloud_provider": "local"}},
|
||||
)
|
||||
monkeypatch.setenv("BROWSER_USE_API_KEY", "bu-key")
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_auto_detect_with_key_migrates(self, monkeypatch):
|
||||
|
|
@ -222,7 +255,9 @@ class TestLegacyCloudMigration:
|
|||
assert bu_cli.is_browser_use_cli_mode() is True
|
||||
|
||||
def test_auto_detect_without_key_does_not_migrate(self, monkeypatch):
|
||||
"""No key, no CLI: nothing to migrate and no default flip."""
|
||||
monkeypatch.setattr("hermes_cli.config.read_raw_config", lambda: {})
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert bu_cli.is_browser_use_cli_mode() is False
|
||||
|
||||
def test_migrated_config_gets_bu_autospawn(self, tmp_path, monkeypatch):
|
||||
|
|
@ -388,7 +423,7 @@ class TestProviderPickerIntegration:
|
|||
assert config["browser"]["backend"] == "browser-use"
|
||||
assert config["browser"]["cloud_provider"] == "local"
|
||||
|
||||
def test_provider_row_stays_active_alongside_cli_mode(self):
|
||||
def test_provider_row_stays_active_alongside_cli_mode(self, monkeypatch):
|
||||
from hermes_cli.tools_config import _is_provider_active
|
||||
|
||||
cli_row = next(r for r in self._rows() if r.get("browser_backend"))
|
||||
|
|
@ -401,9 +436,20 @@ class TestProviderPickerIntegration:
|
|||
# driver attaches to.
|
||||
assert _is_provider_active(local_row, cli_config) is True
|
||||
|
||||
local_config = {"browser": {"cloud_provider": "local"}}
|
||||
assert _is_provider_active(cli_row, local_config) is False
|
||||
assert _is_provider_active(local_row, local_config) is True
|
||||
# Explicit off: the CLI row must not highlight even with the CLI
|
||||
# installed (default-on only applies while backend is unset).
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: ["/usr/bin/browser-use"])
|
||||
off_config = {"browser": {"cloud_provider": "local", "backend": "off"}}
|
||||
assert _is_provider_active(cli_row, off_config) is False
|
||||
assert _is_provider_active(local_row, off_config) is True
|
||||
|
||||
# Backend unset: default-on — the CLI row highlights when the CLI
|
||||
# is runnable, and not when it isn't.
|
||||
default_config = {"browser": {"cloud_provider": "local"}}
|
||||
assert _is_provider_active(cli_row, default_config) is True
|
||||
assert _is_provider_active(local_row, default_config) is True
|
||||
monkeypatch.setattr(bu_cli, "_find_cli", lambda: None)
|
||||
assert _is_provider_active(cli_row, default_config) is False
|
||||
|
||||
|
||||
class TestBrowserUseSlashCommand:
|
||||
|
|
|
|||
|
|
@ -68,8 +68,18 @@ def _read_browser_cfg() -> dict:
|
|||
|
||||
|
||||
def get_browser_backend() -> str:
|
||||
"""Return the configured browser backend key ("" = legacy stack)."""
|
||||
return str(_read_browser_cfg().get("backend") or "").strip().lower()
|
||||
"""Return the configured browser backend key ("" = unset → default).
|
||||
|
||||
YAML 1.1 parses an unquoted ``off`` as boolean False — a hand-edited
|
||||
``backend: off`` must mean BACKEND_DISABLED, not "unset". (True has no
|
||||
sensible backend meaning; normalize it to unset.)
|
||||
"""
|
||||
raw = _read_browser_cfg().get("backend")
|
||||
if raw is False:
|
||||
return BACKEND_DISABLED
|
||||
if raw is True:
|
||||
return ""
|
||||
return str(raw or "").strip().lower()
|
||||
|
||||
|
||||
def is_legacy_browser_use_cloud_config(browser_cfg: dict) -> bool:
|
||||
|
|
@ -98,6 +108,11 @@ def is_legacy_browser_use_cloud_config(browser_cfg: dict) -> bool:
|
|||
def is_browser_use_cli_mode() -> bool:
|
||||
"""True when the Browser Use CLI replaces the built-in browser stack.
|
||||
|
||||
Browser Use mode is the DEFAULT: an unset ``browser.backend`` ("") enables
|
||||
it whenever the browser-use CLI is runnable (installed binary or uvx).
|
||||
Set ``browser.backend: off`` (or ``/browser use off``) for the built-in
|
||||
browser_* tools.
|
||||
|
||||
Camofox always falls back to the built-in tools regardless of
|
||||
``browser.backend`` — it is Firefox-based with a custom HTTP API and no
|
||||
CDP surface, so the CDP-only browser-use harness cannot drive it.
|
||||
|
|
@ -112,7 +127,11 @@ def is_browser_use_cli_mode() -> bool:
|
|||
backend = get_browser_backend()
|
||||
if backend:
|
||||
return backend == _BACKEND_KEY
|
||||
return is_legacy_browser_use_cloud_config(_read_browser_cfg())
|
||||
if is_legacy_browser_use_cloud_config(_read_browser_cfg()):
|
||||
return True
|
||||
# Default (backend unset): Browser Use mode when the CLI can run at all;
|
||||
# otherwise keep the built-in tools so browsing never silently breaks.
|
||||
return _find_cli() is not None
|
||||
|
||||
|
||||
def _find_cli() -> Optional[List[str]]:
|
||||
|
|
|
|||
|
|
@ -61,20 +61,24 @@ BROWSER_USE_API_KEY=***
|
|||
|
||||
Get your API key at [browser-use.com](https://browser-use.com).
|
||||
|
||||
### Browser Use mode
|
||||
### Browser Use mode (default)
|
||||
|
||||
Browser Use mode uses the [Browser Use CLI 3.0](https://github.com/browser-use/browser-use) — a new browser harness that is state-of-the-art at web tasks — instead of the default browser tools. The agent writes and executes Python in the browser to click, type, drag, scrape, and interact with webpages.
|
||||
Browser Use mode uses the [Browser Use CLI 3.0](https://github.com/browser-use/browser-use) — a new browser harness that is state-of-the-art at web tasks — instead of the built-in browser tools. The agent writes and executes Python in the browser to click, type, drag, scrape, and interact with webpages.
|
||||
|
||||
The mode is a **driver** that composes with your configured browser backend: it drives your local Chrome, a Nous-subscription cloud browser, Browserbase, Firecrawl, or Browser Use cloud browsers — whichever browser source is selected in `hermes tools` → Browser Automation. The one exception is Camofox, which has no CDP endpoint for the harness to attach to; Camofox setups automatically keep the built-in browser tools even when Browser Use mode is enabled.
|
||||
**This is the default browser mode**: when `browser.backend` is unset and the `browser-use` CLI is runnable (installed, or available through `uvx`), the agent gets the single `browser_exec` tool. If the CLI can't run, Hermes falls back to the built-in browser tools automatically.
|
||||
|
||||
Enable it with `hermes tools` → **Browser Automation → Browser Use** (free · local · cloud), or directly:
|
||||
The mode is a **driver** that composes with your configured browser backend: it drives your local Chrome, a Nous-subscription cloud browser, Browserbase, Firecrawl, or Browser Use cloud browsers — whichever browser source is selected in `hermes tools` → Browser Automation. The one exception is Camofox, which has no CDP endpoint for the harness to attach to; Camofox setups automatically keep the built-in browser tools.
|
||||
|
||||
To opt out and force the built-in browser tools, use `/browser use off`, or:
|
||||
|
||||
```yaml
|
||||
# Add to ~/.hermes/config.yaml
|
||||
browser:
|
||||
backend: "browser-use"
|
||||
backend: "off"
|
||||
```
|
||||
|
||||
(`backend: "browser-use"` remains valid to force the mode explicitly.)
|
||||
|
||||
Browser Use's own cloud browsers need `browser-use auth login` or `BROWSER_USE_API_KEY`; other browser sources use their existing credentials unchanged.
|
||||
|
||||
:::note
|
||||
|
|
|
|||
Loading…
Reference in New Issue