Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Each upstream has its own append-only table. Add a row every time you pull.
| 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. |
| 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. |
| 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. |
| 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. |

---

Expand Down
14 changes: 12 additions & 2 deletions packages/bcode-browser/harness/src/browser_harness/daemon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""CDP WS holder + IPC relay (Unix socket on POSIX, TCP loopback on Windows). One daemon per BU_NAME."""
import asyncio, json, os, socket, sys, time, urllib.request
import asyncio, json, os, socket, sys, time, urllib.error, urllib.request
from collections import deque
from pathlib import Path

Expand Down Expand Up @@ -93,9 +93,13 @@ def get_ws_url():
raise RuntimeError(f"BU_CDP_URL={url} unreachable after 30s: {last_err} -- is the dedicated automation Chrome running?")
for base in PROFILES:
try:
port = (base / "DevToolsActivePort").read_text().strip().split("\n", 1)[0].strip()
active = (base / "DevToolsActivePort").read_text().splitlines()
except (FileNotFoundError, NotADirectoryError):
continue
port = active[0].strip() if active else ""
ws_path = active[1].strip() if len(active) > 1 else ""
if not port:
continue
# Resolve the live WS URL via /json/version instead of trusting the path stored
# alongside the port in DevToolsActivePort: if Chrome was previously launched
# with a different --user-data-dir on the same port, that file is left behind
Expand All @@ -104,6 +108,12 @@ def get_ws_url():
while time.time() < deadline:
try:
return json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=1).read())["webSocketDebuggerUrl"]
except urllib.error.HTTPError as e:
# Chrome 147+ disables /json/* HTTP discovery on the default user-data-dir;
# the ws path Chrome wrote to DevToolsActivePort still works.
if e.code == 404 and ws_path:
return f"ws://127.0.0.1:{port}{ws_path}"
time.sleep(1)
except (OSError, KeyError, ValueError):
time.sleep(1)
raise RuntimeError(
Expand Down
18 changes: 17 additions & 1 deletion packages/bcode-browser/harness/src/browser_harness/run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os, sys
import os, sys, urllib.request

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

from .admin import (
_version,
NAME,
daemon_alive,
ensure_daemon,
list_cloud_profiles,
list_local_profiles,
Expand Down Expand Up @@ -44,6 +46,18 @@
"""


# Probe /json/version (not a bare TCP connect) so a non-Chrome process bound to
# 9222/9223 doesn't masquerade as Chrome and skip the cloud bootstrap. Mirrors
# daemon.py's fallback probe.
def _local_chrome_listening():
for port in (9222, 9223):
try:
urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3).close()
return True
except OSError: pass
return False


def main():
args = sys.argv[1:]
if args and args[0] in {"-h", "--help"}:
Expand Down Expand Up @@ -71,6 +85,8 @@ def main():
if len(args) < 2:
sys.exit("Usage: browser-harness -c \"print(page_info())\"")
print_update_banner()
if not daemon_alive() and not _local_chrome_listening() and os.environ.get("BROWSER_USE_API_KEY"):
start_remote_daemon(NAME)
ensure_daemon()
exec(args[1], globals())

Expand Down
23 changes: 23 additions & 0 deletions packages/bcode-browser/harness/tests/unit/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ def test_c_flag_executes_code():
assert stdout.getvalue().strip() == "hello from -c"


def test_cloud_bootstrap_on_headless_server(monkeypatch):
"""No daemon, no local Chrome, API key set -> auto-provision cloud daemon."""
monkeypatch.setenv("BROWSER_USE_API_KEY", "test-key")
with patch.object(sys, "argv", ["browser-harness", "-c", "x = 1"]), \
patch("browser_harness.run.daemon_alive", return_value=False), \
patch("browser_harness.run._local_chrome_listening", return_value=False), \
patch("browser_harness.run.start_remote_daemon") as mock_start, \
patch("browser_harness.run.ensure_daemon"), \
patch("browser_harness.run.print_update_banner"):
run.main()
mock_start.assert_called_once()


def test_local_chrome_listening_rejects_non_chrome():
"""A bare TCP listener on 9222/9223 must not fool the probe — only a real
/json/version response counts as Chrome."""
with patch("browser_harness.run.urllib.request.urlopen", side_effect=OSError):
assert run._local_chrome_listening() is False
with patch("browser_harness.run.urllib.request.urlopen") as mock_open:
assert run._local_chrome_listening() is True
mock_open.assert_called_once()


def test_c_flag_does_not_read_stdin():
stdin_read = []
fake_stdin = StringIO("should not be read")
Expand Down
Loading