fix(browser): discover browser-use from user-level tool directories

Desktop/TUI workers can spawn with a minimal PATH that omits
~/.local/bin, the default location where uv tool install links the
browser-use binary. _find_cli() then failed to resolve an installed
CLI and Browser Use mode silently fell back to the built-in tools.

Probe the user-level tool dir (~/.local/bin on POSIX, APPDATA/uv/bin
on Windows) between PATH and the managed HERMES_HOME/bin, for both
the browser-use binary and the uvx fallback.

Salvaged from PR #83788 by @kimyxx onto current main; tests adapted
and extended with precedence and uvx coverage.
This commit is contained in:
kimyxx
2026-08-14 13:19:18 -07:00
committed by Teknium
parent c59e30f14c
commit d29abb7e6b
2 changed files with 69 additions and 14 deletions

View File

@@ -706,7 +706,15 @@ class TestBrowserExec:
class TestFindCliManagedBin:
"""_find_cli probes $HERMES_HOME/bin after PATH (managed uv/uvx/browser-use)."""
"""_find_cli probes ~/.local/bin and $HERMES_HOME/bin after PATH."""
@pytest.fixture(autouse=True)
def _hermetic_home(self, tmp_path, monkeypatch):
"""Pin HOME so the ~/.local/bin probe can't leak the host's real
user-level installs into these real-PATH-probing tests."""
monkeypatch.setenv("HOME", str(tmp_path / "userhome"))
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
def test_managed_bin_browser_use_found(self, tmp_path, monkeypatch):
bin_dir = tmp_path / "home" / "bin"
@@ -714,8 +722,6 @@ class TestFindCliManagedBin:
bu = bin_dir / "browser-use"
bu.write_text("#!/bin/sh\n")
bu.chmod(bu.stat().st_mode | stat.S_IXUSR)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
assert bu_cli._find_cli_unpatched() == [str(bu)]
def test_managed_bin_uvx_fallback(self, tmp_path, monkeypatch):
@@ -724,15 +730,45 @@ class TestFindCliManagedBin:
uvx = bin_dir / "uvx"
uvx.write_text("#!/bin/sh\n")
uvx.chmod(uvx.stat().st_mode | stat.S_IXUSR)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
assert bu_cli._find_cli_unpatched() == [str(uvx), "browser-use"]
def test_nothing_found(self, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
assert bu_cli._find_cli_unpatched() is None
def test_user_local_bin_browser_use_found(self, tmp_path, monkeypatch):
"""#83788: Desktop/TUI workers spawn with a minimal PATH that omits
~/.local/bin, where `uv tool install browser-use` links the binary
by default — _find_cli must probe it explicitly."""
cli_dir = tmp_path / "userhome" / ".local" / "bin"
cli_dir.mkdir(parents=True)
cli = cli_dir / "browser-use"
cli.write_text("#!/bin/sh\n")
cli.chmod(cli.stat().st_mode | stat.S_IXUSR)
assert bu_cli._find_cli_unpatched() == [str(cli)]
def test_user_local_bin_precedes_managed_bin(self, tmp_path, monkeypatch):
"""A user-level install wins over Hermes' managed copy — mirrors the
PATH-first preference (user intent beats bootstrap)."""
user_dir = tmp_path / "userhome" / ".local" / "bin"
user_dir.mkdir(parents=True)
user_cli = user_dir / "browser-use"
user_cli.write_text("#!/bin/sh\n")
user_cli.chmod(user_cli.stat().st_mode | stat.S_IXUSR)
managed_dir = tmp_path / "home" / "bin"
managed_dir.mkdir(parents=True)
managed_cli = managed_dir / "browser-use"
managed_cli.write_text("#!/bin/sh\n")
managed_cli.chmod(managed_cli.stat().st_mode | stat.S_IXUSR)
assert bu_cli._find_cli_unpatched() == [str(user_cli)]
def test_user_local_bin_uvx_fallback(self, tmp_path, monkeypatch):
cli_dir = tmp_path / "userhome" / ".local" / "bin"
cli_dir.mkdir(parents=True)
uvx = cli_dir / "uvx"
uvx.write_text("#!/bin/sh\n")
uvx.chmod(uvx.stat().st_mode | stat.S_IXUSR)
assert bu_cli._find_cli_unpatched() == [str(uvx), "browser-use"]
class TestInstallCli:
def test_already_installed_on_path(self, tmp_path, monkeypatch):

View File

@@ -203,21 +203,40 @@ def _managed_bin_dir() -> Optional[str]:
return None
def _user_local_bin_dir() -> Optional[str]:
"""The standard user-level tool dir (~/.local/bin on POSIX; uv's default
tool bin dir on Windows). Desktop/TUI workers may start with a minimal
PATH that omits it even when `uv tool install browser-use` put the
binary there."""
try:
if os.name == "nt":
base = os.environ.get("APPDATA")
if base:
return str(Path(base) / "uv" / "bin")
return None
return str(Path(os.path.expanduser("~")) / ".local" / "bin")
except Exception as e: # pragma: no cover — defensive
logger.debug("Could not resolve user-local bin dir: %s", e)
return None
def _find_cli() -> Optional[List[str]]:
"""Locate the browser-use CLI, or None when it can't be run.
Prefers an installed browser-use binary (PATH, then Hermes' managed
$HERMES_HOME/bin); falls back to running it through uvx (PATH, then
managed). The managed probes matter because Hermes bootstraps its own
uv into $HERMES_HOME/bin, which is not on the user's PATH.
Prefers an installed browser-use binary (PATH, then the user-level tool
dir, then Hermes' managed $HERMES_HOME/bin); falls back to running it
through uvx across the same probe set. The extra probes matter because
Hermes bootstraps its own uv into $HERMES_HOME/bin (not on the user's
PATH), and Desktop/TUI workers can spawn with a minimal PATH that omits
~/.local/bin where `uv tool install` links binaries by default.
"""
bin_dir = _managed_bin_dir()
for probe_path in (None, bin_dir):
probe_paths = (None, _user_local_bin_dir(), _managed_bin_dir())
for probe_path in probe_paths:
if probe_path is None or probe_path:
direct = shutil.which("browser-use", path=probe_path)
if direct:
return [direct]
for probe_path in (None, bin_dir):
for probe_path in probe_paths:
if probe_path is None or probe_path:
uvx = shutil.which("uvx", path=probe_path)
if uvx: