Skip to content

Commit c6b2441

Browse files
author
bcode
committed
sync: harness 997ee45
1 parent 50cbcee commit c6b2441

5 files changed

Lines changed: 50 additions & 19 deletions

File tree

UPSTREAM.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Each upstream has its own append-only table. Add a row every time you pull.
8686
| 2026-04-28 | `216a2c9` | `fefca43` | bcode | 41 upstream commits. **Major restructure** (PR #229): src-layout reorg (`*.py``src/browser_harness/*.py`), `domain-skills/``agent-workspace/domain-skills/`, agent-editable surface moved from root `helpers.py` to `agent-workspace/agent_helpers.py`, new `_ipc.py` for Windows TCP / POSIX AF_UNIX support, tests moved to `tests/{unit,integration}/`. Also: Expedia/Substack/Loom/Gmail domain skills, screenshot max-dim, helpers.switch_tab dict-accept, websockets pin 15.0.1, BU_CDP_URL, doctor improvements, JS eval refactor. Adapted our integration: `browser-execute.ts` invokes `browser-harness` console-script (not `python run.py`); `harness.ts` `PRESERVED_PATHS` updated to `agent-workspace/agent_helpers.py`; smoke test now imports from `browser_harness` package; `browser-execute.txt` prompt updated to point at new helper paths. Divergences touched: none (still just `.gitignore` + `.venv/`). |
8787
| 2026-04-28 | `fefca43` | `04f7716` | bcode | 7 upstream commits. Windows fixes (PRs #232, #240) + skill rename (PR #242). Files: `src/browser_harness/_ipc.py` (BH_TMP_DIR override for sock/port/pid/log/screenshot dir; drop DETACHED_PROCESS to suppress empty Windows console window), `src/browser_harness/admin.py` (route `ensure_daemon` warm probe through `ipc.connect` so Windows TCP loopback works; new `_open_inspect=False` flag on `ensure_daemon` used by `run_setup` to prevent chrome://inspect tab flooding; drop unused `_paths()` helper), `src/browser_harness/helpers.py` (`capture_screenshot` and click-debug overlay route through `ipc._TMP` instead of `tempfile.gettempdir()` so BH_TMP_DIR covers them too), `SKILL.md` (`name: browser-harness` → `name: browser`), `install.md` (`name: browser-harness-install` → `name: browser-install`). All in protected `src/browser_harness/*.py` zone — taken verbatim. SKILL/install frontmatter rename only affects how end-users invoke the skill (`/browser` vs `/browser-harness`); our `browser-execute.txt` references SKILL.md by file path, so no integration code changes. Divergences touched: none. PR #240 e2e tested separately on Linux against headless Chrome before sync. |
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. |
89+
| 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. |
8990

9091
---
9192

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from pathlib import Path
44

55
IS_WINDOWS = sys.platform == "win32"
6-
# Override via BH_TMP_DIR for sock/port/pid/log + screenshot output (e.g. per-session
7-
# scratch dir). Default keeps AF_UNIX paths under sun_path limits (104 macOS, 108 Linux):
8-
# /tmp on POSIX (gettempdir() returns long /var/folders/... on macOS); tempdir on Windows.
9-
# Caller picking BH_TMP_DIR is responsible for keeping <dir>/bu-<NAME>.sock under 104 chars.
10-
_TMP = Path(os.environ.get("BH_TMP_DIR") or (tempfile.gettempdir() if IS_WINDOWS else "/tmp"))
11-
_TMP.mkdir(parents=True, exist_ok=True) # caller-supplied BH_TMP_DIR may not exist yet
6+
# BH_TMP_DIR set → caller-isolated dir, bare filenames (avoids AF_UNIX sun_path
7+
# overrun: 104 macOS / 108 Linux). Unset → shared tmpdir, "bu-<NAME>" prefix
8+
# disambiguates daemons. POSIX default is /tmp (gettempdir() returns long
9+
# /var/folders/... on macOS); Windows uses TCP so any tempdir is fine.
10+
BH_TMP_DIR = os.environ.get("BH_TMP_DIR")
11+
_TMP = Path(BH_TMP_DIR or (tempfile.gettempdir() if IS_WINDOWS else "/tmp"))
12+
_TMP.mkdir(parents=True, exist_ok=True)
1213
_NAME_RE = re.compile(r"\A[A-Za-z0-9_-]{1,64}\Z")
1314

1415

@@ -18,16 +19,21 @@ def _check(name): # path-traversal guard for BU_NAME
1819
return name
1920

2021

21-
def log_path(name): return _TMP / f"bu-{_check(name)}.log"
22-
def pid_path(name): return _TMP / f"bu-{_check(name)}.pid"
23-
def port_path(name): return _TMP / f"bu-{_check(name)}.port" # Windows-only: holds the daemon's TCP port
24-
def _sock_path(name): return _TMP / f"bu-{_check(name)}.sock"
22+
def _stem(name): # "bu" when BH_TMP_DIR isolates us, else "bu-<NAME>"
23+
_check(name)
24+
return "bu" if BH_TMP_DIR else f"bu-{name}"
25+
26+
27+
def log_path(name): return _TMP / f"{_stem(name)}.log"
28+
def pid_path(name): return _TMP / f"{_stem(name)}.pid"
29+
def port_path(name): return _TMP / f"{_stem(name)}.port" # Windows-only: holds the daemon's TCP port
30+
def _sock_path(name): return _TMP / f"{_stem(name)}.sock"
2531

2632

2733
def sock_addr(name): # display-only, used in log lines
2834
if not IS_WINDOWS: return str(_sock_path(name))
2935
try: return f"127.0.0.1:{port_path(name).read_text().strip()}"
30-
except FileNotFoundError: return f"tcp:bu-{_check(name)}"
36+
except FileNotFoundError: return f"tcp:{_stem(name)}"
3137

3238

3339
def spawn_kwargs(): # subprocess.Popen flags so the daemon detaches from this terminal

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,16 @@ def daemon_alive(name=None):
7676

7777

7878
def _daemon_endpoint_names():
79-
pattern = "bu-*.port" if ipc.IS_WINDOWS else "bu-*.sock"
79+
# BH_TMP_DIR isolates one daemon per dir → no filename-prefix discovery,
80+
# just check whether our local endpoint exists. Without BH_TMP_DIR, _TMP
81+
# is the shared default (`/tmp` etc.) and we glob `bu-*.<suffix>` to find
82+
# every daemon on the machine.
8083
suffix = ".port" if ipc.IS_WINDOWS else ".sock"
84+
if ipc.BH_TMP_DIR:
85+
return [NAME] if (ipc._TMP / f"bu{suffix}").exists() else []
8186
names = []
82-
for p in sorted(ipc._TMP.glob(pattern)):
83-
name = p.name
84-
if not name.startswith("bu-") or not name.endswith(suffix):
85-
continue
86-
raw = name[3:-len(suffix)]
87+
for p in sorted(ipc._TMP.glob(f"bu-*{suffix}")):
88+
raw = p.name[3:-len(suffix)]
8789
try:
8890
ipc._check(raw)
8991
except ValueError:
@@ -219,12 +221,12 @@ def restart_daemon(name=None):
219221
try:
220222
os.kill(pid, 0)
221223
time.sleep(0.2)
222-
except (ProcessLookupError, OSError):
224+
except (ProcessLookupError, OSError, SystemError):
223225
break
224226
else:
225227
try:
226228
os.kill(pid, signal.SIGTERM)
227-
except (ProcessLookupError, OSError):
229+
except (ProcessLookupError, OSError, SystemError):
228230
pass
229231
ipc.cleanup_endpoint(name or NAME)
230232
try:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def _load_env_file(p):
3434
BUF = 500
3535
PROFILES = [
3636
Path.home() / "Library/Application Support/Google/Chrome",
37+
Path.home() / "Library/Application Support/Comet",
38+
Path.home() / "Library/Application Support/Arc/User Data",
3739
Path.home() / "Library/Application Support/Microsoft Edge",
3840
Path.home() / "Library/Application Support/Microsoft Edge Beta",
3941
Path.home() / "Library/Application Support/Microsoft Edge Dev",

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def test_stale_websocket_does_not_open_chrome_inspect():
4848

4949
def test_daemon_endpoint_names_discovers_valid_socket_names(tmp_path, monkeypatch):
5050
monkeypatch.setattr(admin.ipc, "IS_WINDOWS", False)
51+
monkeypatch.setattr(admin.ipc, "BH_TMP_DIR", None) # shared-tmpdir mode
5152
monkeypatch.setattr(admin.ipc, "_TMP", tmp_path)
5253
(tmp_path / "bu-default.sock").touch()
5354
(tmp_path / "bu-remote_1.sock").touch()
@@ -57,6 +58,25 @@ def test_daemon_endpoint_names_discovers_valid_socket_names(tmp_path, monkeypatc
5758
assert admin._daemon_endpoint_names() == ["default", "remote_1"]
5859

5960

61+
def test_daemon_endpoint_names_with_bh_tmp_dir_returns_local_name_when_sock_exists(tmp_path, monkeypatch):
62+
monkeypatch.setattr(admin.ipc, "IS_WINDOWS", False)
63+
monkeypatch.setattr(admin.ipc, "BH_TMP_DIR", str(tmp_path))
64+
monkeypatch.setattr(admin.ipc, "_TMP", tmp_path)
65+
monkeypatch.setattr(admin, "NAME", "session-xyz")
66+
(tmp_path / "bu.sock").touch()
67+
68+
assert admin._daemon_endpoint_names() == ["session-xyz"]
69+
70+
71+
def test_daemon_endpoint_names_with_bh_tmp_dir_returns_empty_when_sock_missing(tmp_path, monkeypatch):
72+
monkeypatch.setattr(admin.ipc, "IS_WINDOWS", False)
73+
monkeypatch.setattr(admin.ipc, "BH_TMP_DIR", str(tmp_path))
74+
monkeypatch.setattr(admin.ipc, "_TMP", tmp_path)
75+
monkeypatch.setattr(admin, "NAME", "session-xyz")
76+
77+
assert admin._daemon_endpoint_names() == []
78+
79+
6080
def test_active_browser_connections_counts_only_healthy_daemons(monkeypatch):
6181
monkeypatch.setattr(admin, "_daemon_endpoint_names", lambda: ["default", "stale", "remote"])
6282

0 commit comments

Comments
 (0)