codex flagged a P2 follow-on to the just-added Network.enable: my fix
reliably enables Network on every fresh session, but old sessions
never get Network disabled (helpers.switch_tab attaches without
detaching), and wait_for_network_idle reads from the daemon's global
drain_events stream without filtering by session_id. Net effect: an
agent that visited a polling/SSE tab and switched away would observe
that background tab's traffic in a later wait, either timing out or
waiting on the wrong tab's requests.
Two narrow fixes:
1. helpers.wait_for_network_idle now captures the active session at
the start of the wait and skips events whose session_id doesn't
match. That's the consumer-side root-cause fix.
2. daemon set_session now calls Network.disable on the previous
session before enabling on the new one. Defense in depth — keeps
the daemon's event buffer from filling with background-tab noise
in the first place. Best-effort with its own 2s timeout; failure
doesn't abort the rest of the handler.
Tests added:
- tests/unit/test_helpers.py: events from a background session are
ignored; the active session can reach idle even when the
background session is busy.
- tests/unit/test_daemon.py: set_session disables Network on the old
session; first set_session call (no prior session) does not call
Network.disable.
Full suite: 63 passed (60 -> 63).
press_key emits a Input.dispatchKeyEvent('char', text='a') after every
single-character keyDown. With Cmd/Ctrl held, that char event makes Chrome
treat the input as a printable letter "a" rather than firing the
select-all shortcut, so the field never gets cleared (Backspace would
then delete the literal "a" and leave the original value untouched).
Dispatch the rawKeyDown/keyUp pair directly with the modifier set and no
text/char event. Tightened the test to assert (a) the modifier is on the
'a' event with the platform-correct value and (b) no char event with
text='a' is emitted.
The computed-style check measures the element itself, so an element nested
inside a display:none or visibility:hidden ancestor is reported as visible
(getComputedStyle returns the descendant's own non-none value, not the
inherited "is rendered" state). checkVisibility walks the ancestor chain
and is the right primitive on modern Chrome. Kept the per-element CSS
check as a fallback for older Chrome that lacks checkVisibility.
Three improvements to the cross-platform IPC layer, lifted from #104:
- meta:'ping' handshake replaces bare TCP connect in daemon_alive() and already_running(). A connect-only check on Windows can succeed against an unrelated process that grabbed our ephemeral port after a daemon crash; the ping/pong response confirms the listener is actually our daemon.
- Per-daemon random token (secrets.token_hex(32)) gates every request on Windows. AF_UNIX + chmod 600 is the boundary on POSIX, but TCP loopback has no chmod-equivalent; without a token any local process could connect and issue CDP commands.
- Atomic .port write (write .port.tmp, os.replace) so a concurrent reader never sees a half-written file.
Adds rohitdutt108 to VOUCHED.td.
Co-authored-by: Rohit Dutt <rohit.dutt@iyc.ishafoundation.org>
Domain skills auto-injected by goto_url() are community-contributed and quality varies; defaulting them off avoids polluting the average run while preserving the contribution loop. Set BH_DOMAIN_SKILLS=1 to opt in.
Lets callers (e.g. browsercode per-session scratch) redirect all harness
file output via one env var. Default behavior unchanged when unset:
/tmp on POSIX, gettempdir() on Windows.
helpers.capture_screenshot and the debug-click overlay now route through
ipc._TMP so the same knob covers screenshots.
Add browser_connections() / active_browser_connections() to admin, which probe
each daemon socket via a new connection_status meta message. The daemon tracks
target_id (set on attach and on set_session) and returns the live page title/URL
via Target.getTargetInfo. switch_tab now forwards target_id in set_session so the
daemon stays in sync after tab switches. run_doctor shows the count, each daemon
name, and the truncated active-page title and URL.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace naive `"return " in expression` check with a character-level parser that
ignores strings, line comments, and block comments. Add `_decode_unserializable_js_value`
to handle NaN, ±Infinity, -0, and BigInt. Extract `_runtime_value` and
`_runtime_evaluate` to unify error handling across `js()` and `page_info()`.
Wrap Runtime.evaluate TimeoutErrors in RuntimeError with expression context.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously js() silently returned None when Chrome reported a syntax or
runtime error. Now it raises RuntimeError with the error description,
line/column location, and a snippet of the failing expression.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(tests): reorganize into tests/unit and tests/integration
Moves all root-level test_*.py files into a structured tests/ directory:
- tests/unit/ — admin, helpers (was test_screenshot), run
- tests/integration/ — js expression tests
- tests/conftest.py — shared fake_png pytest fixture, eliminating duplication
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: move to src layout, agent-workspace, and fix SKILL.md invocation format
- Move package to src/browser_harness/ and domain-skills/interaction-skills to agent-workspace/
- Fix all browser-harness <<'PY' heredoc examples in SKILL.md and run.py HELP string to use the correct -c '...' flag format (heredoc was never supported by the CLI)
- Update SKILL.md path references from domain-skills/ to agent-workspace/domain-skills/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>