The cdp-connector is a local Node CLI (bmc, "Busymate Chrome") that captures a
Chrome instance's already-SSL-decrypted request/response traffic over the Chrome
DevTools Protocol (CDP) and streams it into the Busymate dashboard feed — with no
MITM proxy, no Root CA install, and no cert trust.
It lives at cli/cdp-connector.
Captures appear in the dashboard like any iOS or proxy-server device, tagged
source: "cdp" and rendered with a Chrome badge.
Why CDP instead of MITM
Capturing HTTPS the classic way means installing + trusting a Root CA and routing
through a proxy (the iOS VPN tunnel, or web/proxy-server for non-iOS clients). For a
desktop Chrome user that's heavyweight and brittle. Chrome already exposes
plaintext request/response data over the CDP Network domain — so the connector reads
it directly. The trade-off: CDP is pull (Chrome is the server, clients attach to its
debug port), so bmc must run on the same machine as the Chrome it captures.
Data flow
Device model: daemon-only, devices by name
You sign in once per machine (bmc login, stored at ~/.busymate-cdp/auth.json).
bmc is daemon-only: a background supervisor manages your browsers as named
devices (there is no folder-mode / ./.bmc.json-per-directory model — it was removed).
bmc create <name>provisions a device (auto port + Chrome profile) under~/.busymate-cdp/daemon/devices/<uuid>/, holding its UUID + 365-day device JWT (so capture never needs the user session again).--browser <id>bakes in a browser profile's binary + default flags;--from <base>clones one;--startstarts it.bmc start <name>starts the device — auto-creating it if the name is new. Each device launches its own Chrome on its own free debug port + profile, so devices never collide.--allstarts every device;stop/restartmirror it.bmc edit <name>re-configures a device in place (name / browser / binary / flags);bmc remove <name>deletes it (server-side row + captures + local state).- The supervisor runs each device as a
bmc __run <uuid>child (the capture engine, pointed at the device's state dir).opStartwaits + verifies the child stays alive before reporting success, so a browser that dies on launch surfaces a real error to the CLI and the dashboard. Children are killed on exit (gracefulSIGINT/SIGTERM). - You control everything from the live
bmcconsole (keyboard), the dashboard, or MCP — the daemon pairs one host device (modelbmc-daemon) whose Realtime channel carriesbmc.daemon.*(start/stop/restart/create/edit/remove/list) over the samecdp-command/cdp-resultwire as per-device automation.
Source layout
| File | Responsibility |
|---|---|
index.tsx | CLI routing + the __run <uuid> capture engine (the only in-process capture path; start/<name> route to the daemon). Mounts the Ink TUI. |
daemon.ts / daemonCli.ts / ipc.ts / hostDevice.ts / registry.ts | The supervisor + its dispatcher (start/stop/restart/create/edit/remove/list, --all), the local IPC + remote host channel, and the named-device registry. |
cdp.ts | Minimal CDP WebSocket client. Connects to the browser-level endpoint, Target.setAutoAttach({flatten:true}) to follow every tab/frame, Network.enable per session. Resilient reconnect (retries until the debug port is back — needed after a Chrome relaunch). |
translate.ts | Correlate Network.requestWillBeSent / responseReceived / loadingFinished / loadingFailed into the shared TrafficLogEntry shape; fetch bodies via Network.getResponseBody / getRequestPostData (1 MB cap, text vs base64). |
chrome.ts | Find the browser binary (--chrome / CDP_CHROME_BIN / per-OS auto-detect), launch it with the right flags, findFreePort for multi-instance, waitForCdp readiness. |
proxyForwarder.ts | Local auth-injecting proxy used when the device's external proxy needs credentials (see below). |
settings.ts | effective_settings_for_device RPC → capture gate (SSL-proxy domains / inspect-all / ignore-hosts), breakpoint patterns, and the external proxy. Refreshed live via Realtime. |
breakpoints.ts | Request/response breakpoints via Chrome's Fetch domain (pause → edit raw HTTP → continue/abort), mirroring the proxy-server + dashboard flow. |
device.ts | A device's .bmc.json (in its daemon state dir): pair (via device-pair), load, rename, remove (via device-remove). |
ingestClient.ts | In-memory batch (≈1 s / 100 entries) → POST /functions/v1/ingest; strips NUL is handled server-side. |
realtime.ts | device:<uuid> channel: settings push, live rename, unpair, breakpoint-continue. Mirrors iOS RealtimeSubscriber. |
heartbeat.ts | Keeps device_status fresh (Online + source = "cdp"). |
report.ts | Reports the device's real egress country once (flag in the dashboard). |
auth.ts | Global sign-in (browser loopback or headless CI), refresh-token storage. |
External proxy (with auth)
A device can be configured (in the dashboard) to egress through an upstream HTTP proxy. Chrome's proxy is a launch-time flag, so:
bmc startreads the device's effectiveexternalProxyand launches Chrome with--proxy-server=host:port.- Changing the proxy in the dashboard auto-restarts Chrome with the new route (the CDP client reconnects automatically once the new Chrome is up).
- Authenticated proxies can't carry credentials in
--proxy-server, and we don't want Chrome popping an auth dialog. So when the proxy has a username/password,bmcstarts a tiny local forwarder (proxyForwarder.ts) on127.0.0.1:<ephemeral>, points Chrome at that, and relays to the real proxy with aProxy-Authorization: Basic …header injected. CONNECT (HTTPS) tunnels are authenticated on the CONNECT line then piped opaquely; the capture/breakpoint path is untouched.
Breakpoints
CDP devices support the same request/response breakpoints as proxy-server. Breakpoint
patterns (per-device + global) arm Chrome's Fetch domain; a match pauses the exchange,
inserts a breakpoint_events row (fanned out on device:<uuid>), and the dashboard
Breakpoints view lets you edit the raw HTTP and continue/abort. The global Pause all
requests toggle (a catch-all * pattern) works here too.
Remote browser control
Beyond capture, bmc can be driven remotely: the dashboard (a device's Automation
tab) and BusyBro (chat / Telegram / MCP) issue Chrome DevTools Protocol commands that the
locally-running connector executes against its Chrome and replies to — a full two-way
flow over the device's existing Realtime channel.
Wire contract (device:<uuid>, private):
| Direction | Event | Payload |
|---|---|---|
| caller → device | cdp-command | { id, method, params?, sessionId? } |
| device → caller | cdp-result | { id, ok, result?, error? } |
method is forwarded verbatim to Chrome via the connector's CdpClient.command
(raw CDP passthrough — e.g. Page.navigate, Runtime.evaluate, Page.captureScreenshot),
with two connector-side meta methods handled locally: bmc.targets (list the open page
targets + their sessionId) and bmc.activePage (resolve a page session to drive). The
device JWT may broadcast cdp-result back on its own device:<uuid> topic (Realtime write
policy from migration 0017); a new read policy lets a non-admin owner receive those
replies.
Opt-in (default OFF). The connector refuses every cdp-command unless the per-device
flag settings_device.data.cdpControlEnabled is ON — read live through
effective_settings_for_device by SettingsCache, so toggling it in the dashboard takes
effect with no restart. The RemoteCdpExecutor (src/remoteControl.ts) enforces this
before touching Chrome, bounds concurrency, and times each command out.
Gating (caller side): owner-or-admin + devices:edit, plus the opt-in. The raw
passthrough (browser_cdp tool) is additionally admin-only (arbitrary-code power); the
ergonomic wrappers (browser_targets / browser_open / browser_eval /
browser_screenshot / browser_snapshot) need only devices:edit.
Oversize results. A result over ~180 KB (e.g. a Page.captureScreenshot PNG) would
exceed the ~256 KB Realtime frame, so the executor uploads it to the private cdp-artifacts
Storage bucket (<uuid>/<id>.<ext>, device-write / owner-admin-read RLS) and returns a
1-hour signed { url, storagePath, bytes } instead of inline base64.
The shared wire helper lives at supabase/functions/_shared/cdp.ts (sendCdpAndAwait),
imported by both the BusyBro brain and the MCP server so the protocol never drifts; the
dashboard uses the browser-client equivalent sendDeviceCommand in app/lib/apiFetch.ts.
Distribution + updates
The connector is distributed as source (the repo is private):
- The dashboard serves a one-line installer at
cdp.busymate.net/install.sh(bash) andcdp.busymate.net/install.ps1(PowerShell, Windows), plus a source tarball atcdp.busymate.net/bmc.tar.gz(all Next.js route handlers; the tarball istarofcli/cdp-connectorminusnode_modules/.env*/e2e, and carries both thebin/bmcbash launcher and thebin/bmc.cmdWindows launcher). - macOS/Linux:
curl -fsSL https://cdp.busymate.net/install.sh | bash. Windows:irm https://cdp.busymate.net/install.ps1 | iex— extracts with the built-intar.exeinto%LOCALAPPDATA%\busymate-cdp, drops abmc.cmdshim, and adds it to the user PATH. Both linkbmconto the PATH and runnpm install --omit=dev. bmc updatere-checks/api/version(components["cdp-connector"].build) and re-runs the matching installer (bash on Unix, PowerShell on Windows) when a newer build is out.bmc startreports the running build back to the device (set_device_appRPC) so the dashboard shows the build that's actually running.
Windows specifics. The runtime is otherwise identical (it ships .ts and runs via
tsx). findChromeBinary (src/chrome.ts) probes the standard %ProgramFiles% /
%LOCALAPPDATA% install paths for Chrome, Edge, Brave, and Chromium; Chrome is killed with
taskkill /T /F (killChrome) so its renderer/GPU children don't orphan on shutdown.
Shipping note: a connector release needs BOTH a
cli/cdp-connector/package.jsonbuild bump (the CLI readspackage.json.build) AND a dashboard deploy (the VPSgit pullrefreshes/api/version+ the served/bmc.tar.gz).
Reuse
The connector deliberately mirrors existing patterns: the entry shape +
text/base64/1 MB-cap body handling from web/proxy-server, the URL-pattern matcher
(*.host/path* wildcards) shared with proxy-server + dashboard, device pairing from
supabase/functions/device-pair, and the breakpoint wire protocol from the proxy-server
- dashboard Breakpoints flow.