* 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>
Cut ipc.py from 127 to 68 lines (-46%) by removing restated docstrings and
keeping only load-bearing inline comments (path-traversal guard, uv-Python
AF_UNIX gating, Windows .port-file role). Same logic, same call sites.
Rename ipc -> _ipc per Python convention for internal modules. The IPC
plumbing is only called by daemon/admin/helpers; agents reading helpers.py
should not be pulled into transport details. Callers do 'import _ipc as ipc'
so internal ipc.foo references stay unchanged.
The harness was Linux/macOS-only because daemon IPC hardcoded AF_UNIX sockets
at /tmp/bu-*.sock paths and asyncio.start_unix_server, all of which are
unavailable or invalid on Windows. Worse, uv-managed Python on Windows
(python-build-standalone) ships without socket.AF_UNIX entirely (#124).
New ipc.py centralizes the platform fork:
- POSIX: AF_UNIX socket at <tempdir>/bu-<NAME>.sock (chmod 0600), unchanged
semantics from the prior /tmp-hardcoded path.
- Windows: TCP loopback on 127.0.0.1:<ephemeral>, with the chosen port
written to <tempdir>/bu-<NAME>.port so clients can find the daemon.
Uses asyncio.start_server (stdlib, no obscure APIs, no third-party deps).
Path discipline: log/pid/port files all sit under tempfile.gettempdir() so
they land in /tmp on Linux, $TMPDIR on macOS, %TEMP% on Windows. helpers.py
screenshot() default also moves from /tmp/shot.png to tempfile.gettempdir().
subprocess detach uses start_new_session=True on POSIX and
DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP on Windows via ipc.spawn_kwargs().
run.py reconfigures stdout to UTF-8 on Windows so print(page_info()) doesn't
UnicodeEncodeError on the 🟢 marker that helpers prepend to tab titles
(#124 item 4). cp1252 (PowerShell default) can't encode it.
Verified end-to-end on Windows 11 with Chrome remote debugging:
- daemon spawns, allocates port, writes .port file
- goto + page_info + screenshot round-trip through TCP loopback
- restart_daemon cleans up .port and .pid
POSIX path is logically equivalent to the prior code (same AF_UNIX call,
same socket-file semantics, same chmod 0600), routed through ipc.py.
Closes#124 items 1, 2, 4. Item 3 (Chrome 147 user-data-dir) is a separate
concern not addressed here.
Bot review caught two issues:
1. capture_screenshot's max_dim path imports PIL but Pillow wasn't in
pyproject.toml dependencies, so a clean install would ImportError as
soon as the option was used.
2. NamedTemporaryFile + writing to f.name while the handle is open is
not portable on Windows (file lock). Switched the screenshot tests to
tempfile.TemporaryDirectory and folded the three near-identical bodies
into a small _run() helper.
- Add --version, --doctor, --setup, --update[-y] commands in run.py;
logic lives in admin.py (install-mode detection, GitHub-releases cache
with 24h TTL, dirty-worktree guard, interactive Chrome-attach flow).
- Print a once-per-day startup banner telling agents to run
`browser-harness --update -y` when a newer release is available.
- Rename project to browser-harness in pyproject.toml so PyPI installs
(uv tool install browser-harness) work via the public package name.
- Add .github/workflows/release.yml: on v* tag push, verify the tag
matches pyproject.toml, uv build, and publish to PyPI via trusted
publishing.
- Wire helpers.http_get through fetch_use.fetch_sync(...).text when
BROWSER_USE_API_KEY is set; falls back to the original urllib path
otherwise, preserving the existing str return contract.
- Document the new commands and the agent's self-update duty in
SKILL.md and install.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pyproject.toml used >= ranges for browser-harness, cdp-use, and
websockets, so a resolver could pull newer minors/patches on any
fresh install. Move them to == at the currently-resolved versions so
bumps have to be explicit and show up in review.
uv.lock stays gitignored; transitives float by design.
* add remote browser support via Browser Use cloud + multi-daemon
HARNESLESS_NAME suffixes socket/pid/log — daemons are independent, no
supervisor. start_remote_daemon() creates a Browser Use cloud browser and
launches a daemon attached to it; kill_daemon() stops both. Local Chrome
path is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* rename env vars to BU_ prefix (shorter, less noisy in tool calls)
HARNESLESS_NAME → BU_NAME
HARNESLESS_CDP_WS → BU_CDP_WS
HARNESLESS_REMOTE_BROWSER_ID → BU_BROWSER_ID
Socket/pid/log files keep the harnesless- prefix on disk so they're
recognizable in /tmp. BROWSER_USE_API_KEY unchanged (external convention).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* rename project from harnesless to bu
Socket/pid/log paths now /tmp/bu-<name>.{sock,pid,log}. pyproject package
name updated, uv.lock regenerated. Slash command now /bu.
Note: the repo directory itself is still named harnesless on disk. Rename
manually (mv harnesless bu) so the absolute paths in docs line up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-process architecture: daemon.py holds one persistent CDP WebSocket
to the user's running Chrome (via chrome://inspect), short-lived run.py
processes talk to it over a Unix socket, helpers.py is the transparent
layer the LLM reads and edits at will.
Philosophy: no CLI, no fixed API surface. The LLM writes Python blocks
against ~13 tiny helpers (cdp, click, type_text, screenshot, get_dom,
etc.) and edits helpers.py on the fly when a pattern repeats. Coordinate
clicks default because they pass through iframes/shadow DOM/cross-origin
at the compositor level.
Uses cdp-use internally for send_raw only (ignores its 36k lines of
typed wrappers — raw CDP strings tokenize better than typed calls).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>