提交

提交图

228 次代码提交

作者 SHA1 备注 提交日期
Alezander9 db41106662 docs: replace remaining heredoc examples with -c flag
PR #229 (src layout refactor) updated SKILL.md and run.py to the -c form
when the CLI dropped stdin support, but missed install.md (steps 2 and 7)
and interaction-skills/profile-sync.md. Sync them up.

Picks up the still-relevant hunks from #215 (closes #215, refs #213).

Co-authored-by: Andres Gonzalez <62394570+FVTVLIX@users.noreply.github.com>
2026-04-29 13:55:44 -07:00
Alexander Yue 7ce5ba7ca4 Merge pull request #255 from browser-use/docs/readme-operator-framing-rebase
docs: lead README with first-buyer job (rebased from #170)
2026-04-29 13:53:11 -07:00
Alexander Yue c95a512891 Merge pull request #254 from browser-use/fix/connection-status-error-on-cdp-failure
fix(daemon): report error on stale CDP in connection_status
2026-04-29 13:51:13 -07:00
Matt Van Horn 9e609f2674 docs: lead README with first-buyer job
Rebased from #170 (authored by @mvanhorn) onto current main.
The original branch was based on a stale README; only the two
intentional lines from that PR are reapplied here.
2026-04-29 13:51:12 -07:00
Alezander9 e52992ad80 fix(daemon): report cdp_disconnected on stale CDP probe in connection_status
When the browser dies but the daemon process keeps running, target_id and session stay cached on the Bridge. Pre-fix, connection_status swallowed the Target.getTargetInfo failure and returned the cached IDs with page=null, so admin counted the daemon as healthy. Now it returns {error: cdp_disconnected} and admin's existing error-check skips it. Also returns {error: not_attached} when target_id is unset.
2026-04-29 13:45:16 -07:00
FuLin 1c5a5681bb domain-skills: add Xiaohongshu scraping skill (web search, sorting, and note-opening) (#246)
* Add Xiaohongshu search and note-opening skill

* Clarify Xiaohongshu search URL encoding
2026-04-29 11:46:32 -07:00
playjing 2a478a8ff9 domain-skills/shopify-admin: KB FAQs, embedded apps, Polaris inputs (#247)
Three patterns discovered while populating the Shopify Knowledge Base App
for JING (silent training football brand). Each pattern saved 30+ minutes
of rediscovery; documenting so future agents on any Shopify store benefit.

- README.md: when to use the harness for Shopify admin vs Storefront API
  / Admin API / Shopify CLI; auth notes; Polaris React vs Web Components
  detection.

- embedded-apps.md: every Shopify app surfaces in an iframe at
  *-app.shopify.prod.shopifyapps.com. Use iframe_target() and pass
  target_id to js() calls. Known iframe slugs table (extend as
  discovered).

- polaris-inputs.md: Polaris React text inputs reject the standard
  React-friendly synthetic value setter pattern — fields fill but Save
  button stays disabled. Working pattern is .focus() via JS + CDP-native
  keystrokes via type_text(). Includes full add-FAQ implementation +
  notes on Polaris's web-component migration (polaris-react archived
  Jan 2026).

- knowledge-base.md: canonical automation for the Shopify Knowledge
  Base App (Winter '26 Edition). FAQ-pair flow, batch loop pattern,
  /pairs/<id> URL signal for save success, brand-voice guidance,
  query-log review cadence. App is currently English-only with no
  public API — browser automation is the only path until Shopify
  exposes endpoints.

Tested with 19 successful FAQ saves on the JING store on 2026-04-29.

Co-authored-by: Claude (JING build agent) <build@playjing.local>
2026-04-29 11:31:14 -07:00
BitToby d2410828ec Fix remote browser cleanup when daemon startup fails (#251)
* Fix remote startup cleanup

* Fix cloud browser cleanup on startup interruption
2026-04-29 11:09:30 -07:00
Alexander Yue 997ee457b4 Merge pull request #245 from browser-use/add-comet-arc-profiles
daemon: discover DevToolsActivePort in Comet and Arc profiles on macOS
2026-04-28 22:46:07 -07:00
Alezander9 e4a4478a10 daemon: discover DevToolsActivePort in Comet and Arc profiles on macOS 2026-04-28 22:43:06 -07:00
Alexander Yue 2e0f7ec3a8 Merge pull request #241 from cryptoshrine/fix/windows-os-kill-systemerror
fix(admin): catch SystemError raised by os.kill on Windows
2026-04-28 22:34:29 -07:00
Alexander Yue b1ec1346a2 Merge pull request #244 from browser-use/fix/bh-tmp-dir-short-filenames
fix(_ipc): drop bu-<NAME> filename prefix when BH_TMP_DIR is set
2026-04-28 22:32:57 -07:00
Alezander9 51d62fbbe7 fix(_ipc): drop bu-<NAME> filename prefix when BH_TMP_DIR is set
When BH_TMP_DIR is set, the caller owns the directory and is expected to
give each daemon its own — so the bu-<NAME> filename prefix is redundant.
Dropping it cuts ~30 chars off the AF_UNIX path on POSIX, which matters
because sun_path is 104 bytes on macOS / 108 on Linux and a long BU_NAME
(e.g. an opencode-style session id) can blow past it.

Concretely the failing path on macOS for a 30-char session id under
~/.local/share/bcode/sessions/<sessionID>/bu-<sessionID>.sock was 117
bytes; with this change it becomes ~83 bytes.

When BH_TMP_DIR is unset, _TMP is the shared default (/tmp on POSIX,
gettempdir() on Windows) and the bu-<NAME> prefix is kept to disambiguate
multiple daemons sharing the dir. Backward compatible by default.

admin._daemon_endpoint_names: when BH_TMP_DIR is set, the dir is per-daemon
by construction, so glob discovery doesn't apply — just check whether our
local endpoint exists. Without BH_TMP_DIR, fall back to the existing
shared-dir glob.

Validation (_check on BU_NAME) still runs in both modes to catch garbage
names early.
2026-04-28 22:30:57 -07:00
Alexander Yue 2125cea3a5 Merge pull request #243 from browser-use/fix/ipc-tmp-dir-mkdir
fix(_ipc): mkdir -p _TMP at module load
2026-04-28 21:25:36 -07:00
Alezander9 225abcc9d1 fix(_ipc): mkdir -p _TMP at module load so BH_TMP_DIR can point at a non-existent dir
Without this, a caller passing BH_TMP_DIR=<custom dir> to a directory that
doesn't exist yet causes the first write (sock/port/pid/log/screenshot) to
fail with FileNotFoundError. Default _TMP (/tmp on POSIX, gettempdir() on
Windows) always exists, so this is latent today; it bites the per-session
scratch-dir use case browsercode is about to introduce.

Single root cause -> single fix: cover screenshots, sock, port, pid, and
log paths uniformly.
2026-04-28 21:21:05 -07:00
Alexander Yue 04f7716630 Merge pull request #242 from browser-use/claude/slack-session-sdKmR
Rename skill command from /browser-harness to /browser
2026-04-28 15:48:45 -07:00
Alexander Yue 036bfaf69c Merge pull request #240 from browser-use/fix/ensure-daemon-windows-probe
fix(admin): route ensure_daemon CDP probe through ipc.connect (Windows)
2026-04-28 15:48:30 -07:00
Alezander9 6dc41c1a5b fix(_ipc): suppress empty python.exe console window on Windows daemon spawn
DETACHED_PROCESS overrides CREATE_NO_WINDOW per Win32 docs, so combining
them caused Windows to allocate a fresh console for the daemon. Closing
that window killed the daemon and forced Chrome to re-prompt for remote
debugging permission. Drop DETACHED_PROCESS, keep CREATE_NEW_PROCESS_GROUP
for terminal-close survival.
2026-04-28 15:32:35 -07:00
Alezander9 82ef2a13ab feat(_ipc): BH_TMP_DIR overrides sock/port/pid/log + screenshot dir
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.
2026-04-28 15:21:48 -07:00
Claude ee1ff81eac Rename skill command from /browser-harness to /browser
Shortens the invocation to a cleaner /browser command.

https://claude.ai/code/session_014BWe8AkViicHviYPP843t5
2026-04-28 22:19:53 +00:00
cryptoshrine 06aad12d91 fix(admin): catch SystemError raised by os.kill on Windows
On Windows, `os.kill(pid, 0)` does not behave like its POSIX
counterpart. Instead of returning silently when the process
exists or raising ProcessLookupError when it doesn't, CPython
on Windows raises:

    SystemError: <built-in function kill> returned a result
                 with an exception set

This happens because the underlying Win32 TerminateProcess API
does not accept signal 0 as an "is alive?" probe — Python's C
implementation hits an internal error path that doesn't set a
proper exception, and the interpreter surfaces SystemError.

`restart_daemon()` already catches `(ProcessLookupError, OSError)`
around both `os.kill(pid, 0)` and `os.kill(pid, signal.SIGTERM)`,
but SystemError isn't a subclass of either, so the harness crashes
on every second invocation on Windows: the daemon's stale pid file
points at a no-longer-running pid, the probe raises SystemError,
and the whole `browser-harness -c '...'` call dies with a stack
trace.

Repro on Windows 11 (Python 3.12, browser-harness 0.1.0):

    browser-harness --reload      # ok
    browser-harness -c 'print(page_info())'   # ok
    browser-harness -c 'print(page_info())'   # CRASH

Fix: add SystemError to the existing except clauses in
restart_daemon(). One-word change in two places, matches the
intent of the existing handlers (treat any "couldn't probe/signal
the pid" failure as "process is gone, move on").
2026-04-28 22:56:48 +01:00
Alezander9 1dd4f60252 fix(admin): route ensure_daemon CDP probe through ipc.connect (Windows)
The stale-daemon probe in ensure_daemon used raw socket.AF_UNIX, which:
1. Doesn't exist on uv-bundled Python on Windows (AttributeError).
2. Would point at a TCP display string '127.0.0.1:<port>', not a socket
   path, even if AF_UNIX existed.

Either way the probe always raised, was swallowed by 'except Exception',
and fell through to restart_daemon — killing and respawning the daemon
on every warm call. Symptom is most visible on Windows where the warm
path is the common case.

Fix: use ipc.connect(name) which already does the right thing per
platform (AF_UNIX on POSIX, TCP loopback on Windows). Same call
daemon_alive and restart_daemon already use.

Also drop the now-unused _paths() helper; restart_daemon only needed
the pid_path half, inline that.
2026-04-28 13:16:28 -07:00
Vlad Rafeev ac71315eda fix: prevent chrome://inspect tab flooding during setup (#232)
When remote-debugging is not enabled, `run_setup` opens chrome://inspect
once, then retries `ensure_daemon` in a loop. But `ensure_daemon` itself
also calls `_open_chrome_inspect()` on failure — opening a new tab every
~7 seconds and flooding the browser.

Add `_open_inspect` parameter to `ensure_daemon` so the retry loop in
`run_setup` can suppress redundant tab opens after the first one.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Saurav Panda <sgp65@cornell.edu>
2026-04-28 12:01:13 -07:00
Sarath S Menon fefca43ab5 feat(doctor): show live browser connections and attached pages in run_doctor (#234)
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>
2026-04-28 17:42:47 +05:30
Sarath S Menon 64dafa2805 refactor(js): proper return-statement parsing, unserializable value decoding, unified eval helpers (#231)
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>
2026-04-28 16:33:28 +05:30
Sarath S Menon 5a77ea5525 fix(js): raise RuntimeError on JS evaluation errors instead of returning None (#230)
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>
2026-04-28 16:04:33 +05:30
Sarath S Menon fb1a51dd9b refactor: move to src layout, agent-workspace, and fix SKILL.md invocation format (#229)
* 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>
2026-04-28 15:38:11 +05:30
Sarath S Menon cae52ce916 refactor(tests): reorganize into tests/unit and tests/integration (#228)
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>
2026-04-28 14:42:52 +05:30
Alexander Yue f5f0564757 Merge pull request #225 from browser-use/fix/windows-ipc
Windows support: route IPC through ipc.py (TCP on Windows, AF_UNIX on POSIX)
2026-04-28 01:04:34 -07:00
Alezander9 51ff305bfa Use /tmp on POSIX to stay under AF_UNIX sun_path limit
tempfile.gettempdir() on macOS returns /var/folders/xx/yy.../T/ (~49 chars).
Combined with bu-{64-char-name}.sock that exceeds the 104-byte sun_path
limit on macOS (108 on Linux), causing daemon startup to fail.

Pre-PR upstream hardcoded /tmp; this restores that for POSIX. Windows is
unaffected (uses TCP, not AF_UNIX).
2026-04-28 00:24:18 -07:00
Matt Van Horn a5c973f333 fix(daemon): fire-and-forget mark-title eval so load events don't stall (#171)
* fix(daemon): fire-and-forget mark-title eval so load events don't stall

* fix(daemon): bound mark-title eval to 2s so stalled V8 can't pile up tasks

Fire-and-forget kept the perf win, but dropped the 2s upper bound. On a
discarded renderer or hard-hung V8, the Runtime.evaluate task would
never resolve, leaking one task per navigation. Restore the timeout
inside the _silent wrapper.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Saurav Panda <sgp65@cornell.edu>
2026-04-27 18:44:19 -07:00
Alezander9 b05be93d60 Slim ipc.py and rename to _ipc.py to signal internal
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.
2026-04-27 18:01:11 -07:00
Alezander9 71cf3a87a0 Merge remote-tracking branch 'origin/main' into fix/windows-ipc
# Conflicts:
#	SKILL.md
#	admin.py
#	helpers.py
2026-04-27 17:41:08 -07:00
Alezander9 b749f82350 Address PR review (cubic): docs, serve-task crash detection, name sanitization
- daemon.py:1, SKILL.md:151 — docstring/diagram said named pipe on Windows;
  the implementation is TCP loopback. Updated.
- daemon.py:209 — if ipc.serve() crashes (e.g. bind failure), the prior
  shutdown path could miss it and leave the daemon waiting forever on
  d.stop without a listening endpoint. Now race serve_task and stop.wait()
  via asyncio.wait(FIRST_COMPLETED): if serve finishes first it must have
  raised, so await it to surface the exception. Cleanup cancels both tasks
  unconditionally.
- ipc.py:45 — BU_NAME flowed straight into f-strings building filesystem
  paths, allowing path traversal outside tempdir. Validate via
  ^[A-Za-z0-9_-]{1,64}$ in a single _check() helper that all path
  builders (log_path/pid_path/port_path/_sock_path) call. Bad names raise
  ValueError early with a clear message.
2026-04-27 17:36:08 -07:00
Alezander9 991ab21f80 Windows support: route IPC through ipc.py (TCP on Windows, AF_UNIX on POSIX)
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.
2026-04-27 16:40:56 -07:00
Saurav Panda 953aebd07f Merge pull request #221 from browser-use/feat/screenshot-max-dim
feat(helpers): add max_dim to capture_screenshot
2026-04-27 12:18:43 -07:00
Saurav Panda 7d314748ee Merge branch 'main' into feat/screenshot-max-dim 2026-04-27 12:11:01 -07:00
Saurav Panda ef491eae69 Merge pull request #209 from everlastconsulting/loom-folder-enumeration
domain-skills/loom: library folder enumeration
2026-04-27 12:09:55 -07:00
Saurav Panda ffadfe6c51 fix: declare pillow dependency and use TemporaryDirectory in tests
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.
2026-04-27 12:08:40 -07:00
Saurav Panda a2acd7a3d2 Merge pull request #214 from samtucker/add-bu-cdp-url
daemon: add BU_CDP_URL for HTTP DevTools endpoint resolution
2026-04-27 12:06:44 -07:00
Saurav Panda 1886023e8f Merge pull request #173 from drichman1-maker/fix/macos-brave-and-port-fallback
Add macOS Brave profile path + port-9222/9223 CDP probe fallback
2026-04-27 12:04:50 -07:00
Saurav Panda fca4d792ec Merge pull request #211 from hobostay/fix/missing-c-argument-validation
Fix IndexError when -c flag is passed without code argument
2026-04-27 12:03:09 -07:00
Saurav Panda e3685fd667 Merge pull request #172 from mvanhorn/fix/106-websockets-pin-chrome-147
fix(deps): pin websockets==15.0.1 to unblock Chrome 147 CDP handshake
2026-04-27 12:00:56 -07:00
Saurav Panda 71cb1f3581 feat(helpers): add max_dim to capture_screenshot
Long agent sessions on 2× displays bust the 2000px-per-side limit some
image-aware LLMs enforce — a 2296×1143 CSS viewport produces a 4592×2286
PNG. Passing max_dim=1800 downscales the file before save (only when the
image actually exceeds max_dim), keeping callers that don't pass it
unchanged.
2026-04-27 12:00:36 -07:00
Saurav Panda aa70de78e9 Merge pull request #217 from sontianye/feat/substack-domain-skill
feat(domain-skills): add Substack scraping skill
2026-04-27 11:34:43 -07:00
Saurav Panda 767ed0cd57 Merge pull request #218 from NandiniMurali/domain-skill/expedia
Add Expedia domain skill: hotel search automation
2026-04-27 11:16:54 -07:00
Nandini Muralidharan d0ebf8637c Add Expedia domain skill: hotel search automation
Covers URL-based search (dates, destination, travellers), traveller widget
JS interaction, child age dropdowns, and price filter usage. Documents that
the date picker is unreliable with coordinate clicks and should be bypassed
via URL parameters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-27 14:06:41 +05:30
Tianye Song 190fe686fd fix: remove contradictory body_html=null claims for paywalled posts
Three places still described paywalled post body_html as null/None:
- TL;DR limitations list
- Approach 2 section header
- substack_get_post docstring

All updated to correctly state that body_html is a truncated HTML preview
(not null) for paywalled posts, consistent with the Gotchas section and
empirical verification.
2026-04-27 15:08:56 +08:00
Tianye Song c58bdc37f2 feat(domain-skills): add Substack scraping skill
Adds domain-skills/substack/scraping.md with four field-tested approaches
for extracting data from any Substack publication without authentication
or a browser session. All approaches verified live on 2026-04-27.

Approach 1 — /api/v1/posts: paginated post list with title, slug, audience,
wordcount, reactions, and post_id. Supports offset pagination.

Approach 2 — /api/v1/posts/{slug}: full post content. Returns complete
body_html (~40KB) for free posts; truncated HTML preview for paywalled posts.
Use audience == "everyone" as the reliable signal for full content.

Approach 3 — /api/v1/post/{id}/comments: comment list with author, body,
date, and reaction counts. Uses integer post_id (not slug).

Approach 4 — /feed: lightweight RSS metadata (title/link/pubDate/description)
without JSON parsing overhead.

Covers both URL formats (native subdomain and custom domain), pagination,
paywalled post handling, and gotchas: reactions is an emoji-keyed dict not
an integer, comments endpoint uses post_id not slug, body_html is a truncated
preview (not null) for paid posts, no unauthenticated cross-publication search.

Co-Authored-By: Tianye Song <songtianye1997@gmail.com>
2026-04-27 14:59:22 +08:00
Sam Tucker 94897c7ae3 daemon: add BU_CDP_URL for HTTP DevTools endpoint resolution
BU_CDP_URL takes a Chrome DevTools HTTP endpoint (e.g.
http://127.0.0.1:9333) and resolves it to the WS URL via /json/version,
mirroring how start_remote_daemon already handles cloud browsers.

The motivating use case is running a dedicated automation Chrome on a
non-default --user-data-dir to avoid both the Chrome 136 default-profile
lockdown and the Chrome 144+ "Allow remote debugging" per-connection
consent dialog. Pointing BU_CDP_URL at that instance lets the harness
attach without prompting the user.

Falls back to the existing default-profile DevToolsActivePort discovery
when neither BU_CDP_WS nor BU_CDP_URL is set, so existing setups are
unaffected.
2026-04-26 19:30:44 +01:00