Files
hermes-agent/tests/tools/test_browser_use_harness.py
teknium1 1d287d5375 fix(browser): ship the Browser Use CLI engine in every install, Desktop included
The default browser_exec tool ran the `browser-use` CLI from a PM side
environment (browser-use==0.13.10 in <home>/environments/browser-use),
provisioned by the installers and `hermes update`. Sealed Desktop payloads
skip that step, so the Desktop app never had it and silently fell back to
the built-in tools; the side env was also per-profile and 225 MB.

The CLI's execution path is only `browser_harness.run.main()`; the
browser-use agent framework (anthropic/openai/google-api pins, 93 MB of
googleapiclient) is never imported. browser-harness itself is 2.6 MB of
pure Python whose pins (Pillow 12.3.0, websockets 15.0.1) already match
Hermes's own, so it becomes a core dependency and runs on sys.executable:

- pyproject/uv.lock: browser-harness==0.1.13 (+ cdp-use, fetch-use).
- _find_cli() returns [sys.executable, -m, browser_harness.run]; the child
  env points PYTHONPATH at the harness site dir (the Desktop store
  interpreter boots without a venv and the harness daemon re-runs
  sys.executable), replacing whatever the agent inherited.
- The side-env provisioning (install_cli, the update/installer step) goes.
2026-09-27 23:53:40 -07:00

68 lines
3.6 KiB
Python

"""Browser Use caller -> browser-harness on Hermes's own interpreter -> actual child."""
import importlib.metadata
import json
import subprocess
import sys
import pytest
from tools import browser_use_cli as bu
@pytest.mark.platforms("posix", "windows")
def test_harness_runs_on_an_interpreter_without_its_own_site_packages(monkeypatch):
"""The Desktop bundle boots its store interpreter with no venv to activate (``-S`` emulates
that); the harness and its daemon must import from the PYTHONPATH the tool env supplies,
never from what the agent process inherited."""
monkeypatch.setattr(bu, "_find_cli", bu._find_cli_unpatched)
monkeypatch.setenv("PYTHONPATH", "/wrong-abi")
monkeypatch.setenv("PYTHONHOME", "/wrong-python")
cmd = bu._find_cli()
assert cmd == [sys.executable, "-m", "browser_harness.run"]
result = subprocess.run([cmd[0], "-S", *cmd[1:], "--version"], env=bu._base_subprocess_env(),
capture_output=True, text=True, timeout=120)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == importlib.metadata.version("browser-harness")
@pytest.mark.platforms("posix", "windows")
def test_browser_exec_child_environment(tmp_path, monkeypatch):
from tools import browser_tool_session, browser_supervisor
probe = tmp_path / "probe.py"
probe.write_text("import json, os, sys\n"
"print(json.dumps({'argv': sys.argv, 'stdin': sys.stdin.read(), 'env': dict(os.environ)}))\n",
encoding="utf-8")
monkeypatch.setattr(bu, "_find_cli", lambda: [sys.executable, str(probe)])
monkeypatch.setattr("hermes_cli.config.read_raw_config", lambda: {"browser": {"backend": "browser-use"}})
monkeypatch.setattr("tools.browser_tool_cdp._get_cdp_override", lambda: "")
monkeypatch.setattr("tools.browser_tool_cdp._resolve_cdp_override", lambda url: url)
monkeypatch.setattr("tools.browser_tool_cloud._get_cloud_provider", lambda: None)
monkeypatch.setattr("tools.browser_tool_lightpanda_fallback._using_lightpanda_engine", lambda: False)
def local_browser(task_id, command, args, **kwargs):
assert (task_id, command, args) == ("bu-named-research", "get", ["cdp-url"])
return {"success": True, "data": {"cdpUrl": "ws://127.0.0.1:47000/private"}}
monkeypatch.setattr(browser_tool_session, "_run_browser_command", local_browser)
attached = []
monkeypatch.setattr(browser_supervisor.SUPERVISOR_REGISTRY, "get_or_start",
lambda task_id, cdp_url, **kw: attached.append((task_id, cdp_url)))
monkeypatch.setenv("PYTHONPATH", "/wrong-abi")
monkeypatch.setenv("PYTHONHOME", "/wrong-python")
monkeypatch.setenv("OPENAI_API_KEY", "must-not-leak")
monkeypatch.setenv("BROWSERBASE_API_KEY", "browser-key")
monkeypatch.setenv("KEEP_BROWSER_PROBE", "kept")
monkeypatch.delenv("ANONYMIZED_TELEMETRY", raising=False)
result = json.loads(bu.browser_exec("print('payload')", session="research", task_id="owner"))
assert result["success"], result
child = json.loads(result["output"])
assert child["stdin"] == "print('payload')"
for key in ("PYTHONHOME", "OPENAI_API_KEY", "_HERMES_BU_PRIVATE_BROWSER"):
assert key not in child["env"]
assert child["env"].get("PYTHONPATH") == bu._harness_site_dir()
assert child["env"]["BU_NAME"] == "research"
assert child["env"]["BU_CDP_WS"] == "ws://127.0.0.1:47000/private"
assert child["env"]["ANONYMIZED_TELEMETRY"] == "false"
assert child["env"]["BROWSERBASE_API_KEY"] == "browser-key"
assert child["env"]["KEEP_BROWSER_PROBE"] == "kept"
assert attached == [("owner", "ws://127.0.0.1:47000/private")]