Skip to content

Commit 91d1b51

Browse files
authored
Merge pull request #28 from browser-use/sync/harness-013097a
sync: harness 013097a
2 parents 763a2ba + 7a11378 commit 91d1b51

4 files changed

Lines changed: 53 additions & 3 deletions

File tree

UPSTREAM.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Each upstream has its own append-only table. Add a row every time you pull.
8888
| 2026-04-28 | `04f7716` | `2125cea` | bcode | 1 upstream commit (PR #243). `src/browser_harness/_ipc.py`: `_TMP.mkdir(parents=True, exist_ok=True)` at module load so a caller-supplied `BH_TMP_DIR` pointing at a non-existent directory no longer fails the first sock/port/pid/log/screenshot write. Prerequisite for browsercode's per-session scratch-dir use case. Protected zone — taken verbatim. Divergences touched: none. |
8989
| 2026-04-29 | `2125cea` | `997ee45` | bcode | 6 upstream commits (PRs #241, #244, #245). `src/browser_harness/_ipc.py`: when `BH_TMP_DIR` is set, drop the `bu-<NAME>` filename prefix (caller-isolated dir means no shared-tmpdir disambiguation needed); without `BH_TMP_DIR` the original `bu-<NAME>` scheme is unchanged. `src/browser_harness/admin.py`: `_daemon_endpoint_names` short-circuits to the local NAME when `BH_TMP_DIR` is set (no glob); plus catch `SystemError` from `os.kill` on Windows during `restart_daemon`. `src/browser_harness/daemon.py`: discover DevToolsActivePort in Comet and Arc profiles on macOS. `tests/unit/test_admin.py`: 2 new tests for the `BH_TMP_DIR` discovery path. All in protected `src/browser_harness/*.py` + tests — taken verbatim. Smoke test + 12 admin unit tests pass. The `_ipc` filename change pairs with our recent per-session BH_TMP_DIR work (browsercode PR #22) — caller isolation now extends to filenames as well as the dir. Divergences touched: none. |
9090
| 2026-04-30 | `997ee45` | `660827d` | bcode | 11 upstream commits (PRs #246, #247, #251, #254, #256, #260). `src/browser_harness/daemon.py`: resolve WS via `/json/version` to avoid stale `DevToolsActivePort` path (PR #260) + report `cdp_disconnected` on stale CDP probe in `connection_status` (PR #254) + cleanup remote browser when daemon startup fails (PR #251). `src/browser_harness/admin.py`: companion changes for the daemon fixes. `tests/unit/test_admin.py`: 7 new tests. New domain skills: `agent-workspace/domain-skills/xiaohongshu/scraping.md` (PR #246), and a top-level `domain-skills/shopify-admin/` tree (PR #247: README, embedded-apps, knowledge-base, polaris-inputs). Note: PR #247 added skills at the top-level `domain-skills/` path, not under `agent-workspace/domain-skills/` as the post-#229 layout would suggest — vendored verbatim to match upstream layout. Doc updates: README operator framing (PR #255), install.md heredoc → `-c` flag (PR #256), profile-sync.md same. All files outside divergences — taken verbatim. Smoke test + 19 admin unit tests pass. Divergences touched: none. |
91+
| 2026-05-01 | `660827d` | `013097a` | bcode | 8 upstream commits (PRs #261, #265, #266). `src/browser_harness/daemon.py` (PR #265): split `DevToolsActivePort` into port + ws-path lines and fall back to `ws://127.0.0.1:<port><ws_path>` when `/json/version` returns 404 (Chrome 147+ disables `/json/*` HTTP discovery on the default user-data-dir). `src/browser_harness/run.py` (PR #266): when no daemon is alive, no local Chrome is listening on 9222/9223 (probed via `/json/version`, not bare TCP), and `BROWSER_USE_API_KEY` is set, auto-bootstrap a cloud daemon. `tests/unit/test_run.py`: 2 new tests for the cloud bootstrap path. PR #261 moved `domain-skills/shopify-admin/` → `agent-workspace/domain-skills/shopify-admin/` upstream — both paths are excluded from the vendored tree per §3, so this rename is a no-op for browsercode (`script/check-harness-diff.sh` filters both via `IGNORED_PATHS_REGEX`). All in protected `src/browser_harness/*.py` + tests — taken verbatim. Smoke test + 23 unit tests pass. Divergences touched: none. |
9192

9293
---
9394

packages/bcode-browser/harness/src/browser_harness/daemon.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""CDP WS holder + IPC relay (Unix socket on POSIX, TCP loopback on Windows). One daemon per BU_NAME."""
2-
import asyncio, json, os, socket, sys, time, urllib.request
2+
import asyncio, json, os, socket, sys, time, urllib.error, urllib.request
33
from collections import deque
44
from pathlib import Path
55

@@ -93,9 +93,13 @@ def get_ws_url():
9393
raise RuntimeError(f"BU_CDP_URL={url} unreachable after 30s: {last_err} -- is the dedicated automation Chrome running?")
9494
for base in PROFILES:
9595
try:
96-
port = (base / "DevToolsActivePort").read_text().strip().split("\n", 1)[0].strip()
96+
active = (base / "DevToolsActivePort").read_text().splitlines()
9797
except (FileNotFoundError, NotADirectoryError):
9898
continue
99+
port = active[0].strip() if active else ""
100+
ws_path = active[1].strip() if len(active) > 1 else ""
101+
if not port:
102+
continue
99103
# Resolve the live WS URL via /json/version instead of trusting the path stored
100104
# alongside the port in DevToolsActivePort: if Chrome was previously launched
101105
# with a different --user-data-dir on the same port, that file is left behind
@@ -104,6 +108,12 @@ def get_ws_url():
104108
while time.time() < deadline:
105109
try:
106110
return json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=1).read())["webSocketDebuggerUrl"]
111+
except urllib.error.HTTPError as e:
112+
# Chrome 147+ disables /json/* HTTP discovery on the default user-data-dir;
113+
# the ws path Chrome wrote to DevToolsActivePort still works.
114+
if e.code == 404 and ws_path:
115+
return f"ws://127.0.0.1:{port}{ws_path}"
116+
time.sleep(1)
107117
except (OSError, KeyError, ValueError):
108118
time.sleep(1)
109119
raise RuntimeError(

packages/bcode-browser/harness/src/browser_harness/run.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os, sys
1+
import os, sys, urllib.request
22

33
# Windows default stdout encoding is cp1252, which can't encode the 🟢 marker
44
# helpers prepend to tab titles (or anything else outside Latin-1). Force UTF-8
@@ -9,6 +9,8 @@
99

1010
from .admin import (
1111
_version,
12+
NAME,
13+
daemon_alive,
1214
ensure_daemon,
1315
list_cloud_profiles,
1416
list_local_profiles,
@@ -44,6 +46,18 @@
4446
"""
4547

4648

49+
# Probe /json/version (not a bare TCP connect) so a non-Chrome process bound to
50+
# 9222/9223 doesn't masquerade as Chrome and skip the cloud bootstrap. Mirrors
51+
# daemon.py's fallback probe.
52+
def _local_chrome_listening():
53+
for port in (9222, 9223):
54+
try:
55+
urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3).close()
56+
return True
57+
except OSError: pass
58+
return False
59+
60+
4761
def main():
4862
args = sys.argv[1:]
4963
if args and args[0] in {"-h", "--help"}:
@@ -71,6 +85,8 @@ def main():
7185
if len(args) < 2:
7286
sys.exit("Usage: browser-harness -c \"print(page_info())\"")
7387
print_update_banner()
88+
if not daemon_alive() and not _local_chrome_listening() and os.environ.get("BROWSER_USE_API_KEY"):
89+
start_remote_daemon(NAME)
7490
ensure_daemon()
7591
exec(args[1], globals())
7692

packages/bcode-browser/harness/tests/unit/test_run.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,29 @@ def test_c_flag_executes_code():
1515
assert stdout.getvalue().strip() == "hello from -c"
1616

1717

18+
def test_cloud_bootstrap_on_headless_server(monkeypatch):
19+
"""No daemon, no local Chrome, API key set -> auto-provision cloud daemon."""
20+
monkeypatch.setenv("BROWSER_USE_API_KEY", "test-key")
21+
with patch.object(sys, "argv", ["browser-harness", "-c", "x = 1"]), \
22+
patch("browser_harness.run.daemon_alive", return_value=False), \
23+
patch("browser_harness.run._local_chrome_listening", return_value=False), \
24+
patch("browser_harness.run.start_remote_daemon") as mock_start, \
25+
patch("browser_harness.run.ensure_daemon"), \
26+
patch("browser_harness.run.print_update_banner"):
27+
run.main()
28+
mock_start.assert_called_once()
29+
30+
31+
def test_local_chrome_listening_rejects_non_chrome():
32+
"""A bare TCP listener on 9222/9223 must not fool the probe — only a real
33+
/json/version response counts as Chrome."""
34+
with patch("browser_harness.run.urllib.request.urlopen", side_effect=OSError):
35+
assert run._local_chrome_listening() is False
36+
with patch("browser_harness.run.urllib.request.urlopen") as mock_open:
37+
assert run._local_chrome_listening() is True
38+
mock_open.assert_called_once()
39+
40+
1841
def test_c_flag_does_not_read_stdin():
1942
stdin_read = []
2043
fake_stdin = StringIO("should not be read")

0 commit comments

Comments
 (0)