cdpilot 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +451 -22
  2. package/bin/cdpilot.js +160 -15
  3. package/package.json +3 -2
  4. package/src/cdpilot.py +6885 -511
package/README.md CHANGED
@@ -28,14 +28,14 @@ No config files. No boilerplate. Just `npx` and go.
28
28
  AI agents and developers need browser control that **just works**:
29
29
 
30
30
  - **Zero config** — `npx cdpilot launch` starts an isolated browser session
31
- - **Zero dependency** — No Puppeteer, no Playwright, no Selenium. Pure CDP over HTTP
31
+ - **Zero npm dependencies** — No Puppeteer, no Playwright, no Selenium. Pure CDP over HTTP/WebSocket; the Python side needs one package, `websockets` (the setup wizard installs it)
32
32
  - **70+ commands** — Navigate, click, type, screenshot, network, console, accessibility, video understanding, progressive anti-bot resilience, and more
33
33
  - **AI-agent friendly** — Designed for Claude, GPT, Gemini, and any LLM tool-use workflow
34
34
  - **Isolated sessions** — Your personal browser stays untouched. cdpilot runs in its own profile
35
35
  - **Visual feedback** — Green glow overlay, cursor visualization, click ripples, and keystroke display keep you informed during automation
36
36
  - **Multi-project isolation** — Each project gets its own browser instance and port automatically, no conflicts
37
37
  - **AI control warning** — Red toast notification appears when you hover during active automation
38
- - **Privacy-first** — Everything runs locally. No data leaves your machine
38
+ - **Privacy-first** — Everything runs locally; nothing leaves your machine unless you opt in (CAPTCHA solver keys, `tls-check`, proxies)
39
39
 
40
40
  ### Browser Selection (Workload-Aware Auto-Pick)
41
41
 
@@ -44,7 +44,7 @@ two-axis policy — extension workload × platform stability:
44
44
 
45
45
  | Your workload | Auto-pick order |
46
46
  |---|---|
47
- | Has extensions registered (`ext-install`) | vivaldi → brave → edge → chromium → chrome |
47
+ | Has extensions registered (`ext-install`) | vivaldi → brave → edge → chromium → chrome (→ Chrome for Testing, if installed) |
48
48
  | No extensions (pure automation) | chrome → vivaldi → edge → chromium → brave |
49
49
 
50
50
  Override anytime:
@@ -52,13 +52,16 @@ Override anytime:
52
52
  ```bash
53
53
  cdpilot browser # show current pick + reason
54
54
  cdpilot browser vivaldi # pin to Vivaldi
55
+ cdpilot browser chrome-for-testing # pin to Chrome for Testing (after installing it, below)
55
56
  cdpilot browser auto # restore smart default
56
57
  ```
57
58
 
58
59
  **Why the split?**
59
- - **Chrome 147+ silently drops `--load-extension`** for unpacked extensions
60
- (no error, no warning). Verified — `chrome://extensions` shows 0 items.
61
- - **Vivaldi, Brave, Edge, Chromium** honor `--load-extension` (tested).
60
+ - **Chrome 137+ silently drops `--load-extension`** for unpacked extensions
61
+ (no error, no warning) — Google removed it from branded Chrome builds
62
+ ([PSA](https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ)).
63
+ Verified — `chrome://extensions` shows 0 items.
64
+ - **Vivaldi, Brave, Edge, Chromium and Chrome for Testing** honor `--load-extension` (tested).
62
65
  - On **macOS 26 (Tahoe)** Brave 1.89 crashes deterministically at ~7min
63
66
  uptime (SIGTRAP in ThreadPoolForegroundWorker). cdpilot detects the OS
64
67
  and demotes Brave automatically until a fixed Brave release ships.
@@ -66,6 +69,43 @@ cdpilot browser auto # restore smart default
66
69
  Each browser gets its own isolated profile (`~/.cdpilot/.../profile-vivaldi`
67
70
  etc.) so switching never causes prefs corruption.
68
71
 
72
+ ### Extension development: Chrome for Testing
73
+
74
+ If you develop an extension against Chrome itself, install
75
+ [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing) (CfT) —
76
+ the Chrome team's versioned, non-auto-updating Chrome build for automation, which
77
+ still loads unpacked extensions:
78
+
79
+ ```bash
80
+ cdpilot browser install chrome-for-testing # latest Stable
81
+ cdpilot browser install chrome-for-testing --channel beta # stable | beta | dev | canary
82
+ cdpilot browser install chrome-for-testing --version 154 # a milestone, or a full version (154.0.8037.57)
83
+ cdpilot ext-install ./my-extension/ # register your unpacked extension
84
+ cdpilot browser status # shows the CfT version and path
85
+ ```
86
+
87
+ - **Download only on that command.** It is a one-time ~150-190 MB zip (mac-arm64 at
88
+ 154.0.8037.57: 191,429,663 bytes); cdpilot never fetches it by itself. The build comes
89
+ from the official [CfT JSON endpoints](https://googlechromelabs.github.io/chrome-for-testing/)
90
+ (`last-known-good-versions-with-downloads.json`, per-version and per-milestone files).
91
+ Those list URLs only, no hash, so the download is checked against the server's
92
+ `Content-Length` and its `x-goog-hash` md5; the zip's SHA-256 is recorded.
93
+ - It is extracted to `$CDPILOT_HOME/browsers/chrome-for-testing/<version>/` (on macOS the
94
+ quarantine attribute is cleared on that directory only) and recorded in `installed.json`.
95
+ - **Used automatically for extension work:** while dev extensions are registered, a
96
+ branded Chrome pick (`cdpilot browser chrome`, or `auto` with Chrome the only
97
+ extension-capable choice) becomes Chrome for Testing, with one stderr line saying so.
98
+ Without CfT installed, `ext-install` on Chrome prints the install command and keeps
99
+ Vivaldi/Brave as the alternative. `cdpilot browser chrome-for-testing` selects it
100
+ always. An explicit `CHROME_BIN` is never replaced.
101
+ - It runs with cdpilot's usual per-project isolated profile (`profile-cft`).
102
+ - **It is for extension development and testing — not a stealth browser.** cdpilot
103
+ makes no anti-bot claims for it; use your regular browser setup for that.
104
+ - **Linux (Ubuntu 23.10+):** AppArmor blocks unprivileged user namespaces, and CfT
105
+ (unlike the distro's Chrome package) has no profile allowing them, so it exits at
106
+ start. cdpilot prints this hint; allow them with
107
+ `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`.
108
+
69
109
  ## Installation
70
110
 
71
111
  ```bash
@@ -86,6 +126,46 @@ npx cdpilot launch # Start browser with CDP enabled
86
126
  npx cdpilot status # Check connection
87
127
  ```
88
128
 
129
+ ### Claude Code plugin marketplace
130
+
131
+ Install cdpilot's MCP server and its skill straight into Claude Code, no
132
+ `.mcp.json` editing required:
133
+
134
+ ```bash
135
+ /plugin marketplace add mehmetnadir/cdpilot
136
+ /plugin install cdpilot@cdpilot
137
+ ```
138
+
139
+ This registers the `cdpilot` MCP server (`npx cdpilot mcp`) and a short skill
140
+ that teaches Claude Code when and how to reach for cdpilot's commands. See
141
+ [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json) and
142
+ [`plugins/cdpilot/`](plugins/cdpilot/).
143
+
144
+ ### Smaller MCP tool list
145
+
146
+ `cdpilot mcp --tools core` (or `CDPILOT_MCP_TOOLS=core`; the flag wins) lists 10 core
147
+ tools (navigate, click, fill, content, screenshot, eval, wait_for, assert, tabs, connect)
148
+ plus `browser_tools`. The agent calls `browser_tools` with `list` to see the other
149
+ tools, or `enable` to add some (the server sends `tools/list_changed`). `--tools a,b,c`
150
+ picks tools by name; `all` (default) is unchanged. Schema size of `tools/list`
151
+ (tiktoken `o200k_base`): 45 tools = 5,769 tokens, `core` = 11 tools = 1,342 tokens.
152
+
153
+ ### MCP Bundle (.mcpb)
154
+
155
+ For MCP clients that support one-click local server installation (Claude
156
+ Desktop and others), download the `.mcpb` file attached to the
157
+ [latest release](https://github.com/mehmetnadir/cdpilot/releases/latest) and
158
+ open it, or build it yourself:
159
+
160
+ ```bash
161
+ npm run build:mcpb # writes cdpilot.mcpb at the repo root
162
+ ```
163
+
164
+ The bundle still needs the same runtime on the machine that runs it: Node.js
165
+ 18+, Python 3.10+, and Brave/Chrome/Chromium installed. See
166
+ [`manifest.json`](manifest.json) for the bundle's MCP Bundle manifest
167
+ ([spec](https://github.com/modelcontextprotocol/mcpb)).
168
+
89
169
  ### Upgrading from 0.4.x → 0.5.0 — read this first
90
170
 
91
171
  **One breaking change**, the rest is additive.
@@ -144,6 +224,8 @@ cdpilot scroll-to <selector> # Scroll element into view
144
224
  cdpilot drag <from> <to> # Drag and drop
145
225
  ```
146
226
 
227
+ A real mouse click (inside a frame, or `--entropy=on`) whose target moves or gets covered while the button is held is not clicked again by script: `click` prints `Pressed (released elsewhere, not clicked): …` and ends with **exit 3** (pressed, release missed the target, not clicked; `1` stays "error"). A target the page replaced or navigated away from on mousedown prints `Pressed (the page replaced or left it, not clicked): …` and exits 0. MCP (`browser_click`, `browser_smart_click`) returns `isError: false` with a first line `{"clicked": false, "reason": "moved"|"gone"|"unknown"}`. `batch` and `run` finish every step, then exit 1 if a step failed, else 3 if a click was not clicked, else 0.
228
+
147
229
  ### Element targeting inside iframes
148
230
 
149
231
  Card forms (Stripe/iyzico-style), embedded login widgets and the reCAPTCHA
@@ -175,11 +257,19 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
175
257
  inside it and says so on stderr:
176
258
  `note: '#card-element' is not an iframe; using the iframe inside it`.
177
259
  - Selectors and texts that already contain `>>>` keep working: when the part
178
- before the first `>>>` finds no iframe in the page, or a segment is empty
260
+ before the first `>>>` finds no iframe or shadow host in the page, or a segment is empty
179
261
  (`click "Next >>>"`), the whole string is used as written, exactly as
180
262
  before. If that fails too, one stderr line explains:
181
- `note: 'Home' matched no iframe; used the selector as written`.
263
+ `note: 'Home' matched no iframe or shadow host; used the selector as written`.
182
264
  `--frame` never falls back: a frame it cannot find is an error (exit 1).
265
+ - **Shadow DOM support**: `>>>` hops also pierce open shadow roots (`host.shadowRoot.querySelector(next)`).
266
+ Chains mix freely with iframes: `cdpilot click "iframe#pay >>> my-card >>> button.inner"` or
267
+ `cdpilot click "my-app >>> my-panel >>> input"`. A closed shadow root is unreachable by design
268
+ and fails with `'X' has a closed shadow root; cdpilot cannot reach inside it`.
269
+ When a plain CSS selector matches nothing at document level, cdpilot automatically falls back
270
+ to a depth-first search across all open shadow roots, printing one stderr note:
271
+ `note: '<sel>' found inside the shadow root of <host tag#id>`. Explicit `>>>` hops do not print this note.
272
+ `wait` and `wait-for` poll shadow roots every 200 ms alongside the document MutationObserver.
183
273
  - For `smart-click` / `smart-fill` / `smart-select` the part before the first
184
274
  `>>>` is a frame only if it looks like a selector (it has `#`, `.` or `[`,
185
275
  or starts with an `iframe`/`frame` tag) or is `url=…`; words stay text.
@@ -191,9 +281,17 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
191
281
  (`DOM.describeNode` → frame id → the frame's execution context, or
192
282
  `Target.attachToTarget` with a flat session for an out-of-process frame),
193
283
  never through `contentDocument`, which the browser blocks cross-origin.
194
- - Real mouse input (`hover`, `dblclick`, `rightclick`, `--entropy=on` clicks)
195
- is dispatched at page coordinates: the frame's offset in the page is added
196
- automatically, so the browser hit-tests into the right frame.
284
+ - Inside a frame, `click` and `smart-click` are real mouse input
285
+ (`isTrusted: true`), like `hover`, `dblclick`, `rightclick` and
286
+ `--entropy=on` clicks; an element with no box falls back to a script click.
287
+ Mouse input is dispatched at page coordinates: each frame's offset and scale
288
+ (`transform: scale()`, `zoom`) in the page is applied automatically, so the
289
+ browser hit-tests into the right frame. Before pressing, cdpilot checks the
290
+ point (`elementFromPoint` in each document on the way); if something covers
291
+ the target, such as a cookie banner, it clicks the target by script instead
292
+ and says so on one stderr line (`note: … is covered by div#cookie at the
293
+ click point; used a script click`). cdpilot's own input blocker (`show on`,
294
+ MCP sessions) lets only cdpilot's click through, for the click itself.
197
295
  - `smart-click` / `smart-fill` / `smart-select` look in the page first and
198
296
  compare texts with whitespace collapsed (`&nbsp;` and line breaks count as
199
297
  one space). An enabled page element whose text or label contains the whole
@@ -217,14 +315,22 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
217
315
  ### Debugging
218
316
 
219
317
  ```bash
220
- cdpilot console [url] # Capture console logs
221
- cdpilot network [url] # Monitor network requests
222
- cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
223
- cdpilot perf # Performance metrics
224
- cdpilot eval <js> # Execute JavaScript
225
- cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
318
+ cdpilot console [url] # Capture console logs
319
+ cdpilot network [url] # Monitor network requests
320
+ cdpilot network <url> --har trace.har # Export navigation traffic as HAR 1.2 JSON (--har - for stdout)
321
+ cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
322
+ cdpilot perf # Performance metrics
323
+ cdpilot eval <js> # Execute JavaScript
324
+ cdpilot eval-batch <json> # Run N JS expressions in 1 roundtrip (5-30x faster)
226
325
  ```
227
326
 
327
+ #### Network monitoring & HAR export
328
+
329
+ `cdpilot network <url> --har <file>` exports all HTTP requests of a navigation as standard HAR 1.2 JSON (`--har -` writes to stdout).
330
+ - **Included:** URLs, methods, headers, status codes, query strings, and CDP timing breakdowns (DNS, connect, SSL, send, wait, receive).
331
+ - **Omitted:** Response bodies are not fetched or stored (avoids memory bloat and round-trip overhead); content.size reflects transfer length; bodySize is -1.
332
+ - **Masking:** Sensitive header values matching security patterns (`Cookie`, `Set-Cookie`, `Authorization`, `X-Api-Key`, token, secret) are replaced with `"«masked»"`. URLs are masked the same way as the session log: userinfo passwords and query/fragment parameters named like secrets (`token`, `key`, `code`, `session`, …) in `request.url`, `queryString`, `redirectURL` and URL-bearing header values (`Location`, `Referer`, `Link`); a `data:` URL keeps only its media type.
333
+
228
334
  ```bash
229
335
  # Example: read 4 DOM values in a single CDP roundtrip instead of 4
230
336
  cdpilot eval-batch '["document.title","location.href","document.links.length","document.images.length"]'
@@ -462,6 +568,8 @@ At launch, cdpilot also passes `--disable-blink-features=AutomationControlled`,
462
568
  which closes the Blink runtime flag that Cloudflare and DataDome probe to detect
463
569
  an automated browser.
464
570
 
571
+ Real mouse clicks (frames, `--entropy=on`, `click @ref`, `dblclick`, `rightclick`) hold the button 40-120 ms like a person's press; `CDPILOT_PRESS_MS=min-max` changes the range (`0-0` = instant).
572
+
465
573
  #### Three-tier stealth mode
466
574
 
467
575
  `cdpilot mode` is the recommended entry point — one switch that sets how much
@@ -505,6 +613,145 @@ cdpilot adaptive clear # Drop the stealth host memory entirely
505
613
  > when it sees one — adds the host to a persistent list, retries once with
506
614
  > stealth on. Never auto-demotes. Conservative by design.
507
615
 
616
+ ### Web Bot Auth (signed agent)
617
+
618
+ The opposite of stealth. Stealth tries to make the browser look like a person;
619
+ Web Bot Auth says "this is an automated agent, and here is proof of which one".
620
+ Every request the browser makes carries an Ed25519
621
+ [HTTP Message Signature (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421)
622
+ that a site, or Cloudflare in front of it, checks against a key directory you
623
+ publish on your own domain. The format follows
624
+ [draft-meunier-web-bot-auth-architecture](https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/)
625
+ and [draft-meunier-http-message-signatures-directory](https://datatracker.ietf.org/doc/draft-meunier-http-message-signatures-directory/),
626
+ and reproduces the Ed25519 test vectors of Cloudflare's reference
627
+ implementation ([cloudflare/web-bot-auth](https://github.com/cloudflare/web-bot-auth)).
628
+
629
+ **Optional dependency.** Signing needs the `cryptography` package, for the Python cdpilot runs
630
+ on (`CDPILOT_PYTHON` if you set it):
631
+
632
+ ```bash
633
+ pip install cryptography
634
+ ```
635
+
636
+ Without it, the `bot-auth` commands and `launch --bot-auth` print that hint and exit 2. Every
637
+ other command works as before.
638
+
639
+ **1. Generate the key** (once):
640
+
641
+ ```bash
642
+ cdpilot bot-auth init --agent-url https://your-domain.com
643
+ ```
644
+
645
+ This writes an Ed25519 private key to `CDPILOT_HOME/bot-auth/ed25519.key` (default
646
+ `~/.cdpilot`), created with mode `0600`. cdpilot never prints or logs the key, and it warns
647
+ when the file is readable by others. The agent URL must be an `https://` origin (no path). A
648
+ second `init` refuses to replace the key unless you pass `--force`, because the new key would no
649
+ longer match the directory you published.
650
+
651
+ **2. Host the directory on your own domain, signed.** `cdpilot bot-auth directory` prints the
652
+ public key as a JWKS, where `kid` is the key's RFC 7638 JWK thumbprint. Cloudflare also requires
653
+ the directory *response* to be signed, so `--headers` prints the headers to serve with it:
654
+
655
+ ```bash
656
+ cdpilot bot-auth directory --headers # headers, a blank line, then the JSON body
657
+ cdpilot bot-auth directory --headers --json # the same as {"authority", "headers", "body"}
658
+ ```
659
+
660
+ ```
661
+ Content-Type: application/http-message-signatures-directory+json
662
+ Signature-Input: sig1=("@authority";req);created=…;keyid="<thumbprint>";alg="ed25519";expires=…;nonce="…";tag="http-message-signatures-directory"
663
+ Signature: sig1=:<base64 Ed25519 signature>:
664
+
665
+ { "keys": [ … ] }
666
+ ```
667
+
668
+ Serve that body at `https://your-domain.com/.well-known/http-message-signatures-directory` with
669
+ those three headers. The signature covers `("@authority";req)`, the host the directory is fetched
670
+ from: by default the agent URL's host, or `--authority <host>`. There is one signature per key in
671
+ the directory. It is valid for 24 hours (`--ttl <seconds>`), so a static host needs it refreshed
672
+ daily, from cron or an edge function that runs the same signing. A cron example that rewrites the
673
+ headers file and the body your web server serves:
674
+
675
+ ```bash
676
+ # m h dom mon dow — every day at 03:17, 48 h validity so one missed run does not break it
677
+ 17 3 * * * cdpilot bot-auth directory --headers --ttl 172800 --json > /var/www/wba/directory.json.tmp \
678
+ && mv /var/www/wba/directory.json.tmp /var/www/wba/directory.json
679
+ ```
680
+
681
+ Your server (or an edge function) reads `headers` and `body` from that JSON and serves them at
682
+ the well-known path. `--content-digest` also covers the body (`Content-Digest`, RFC 9530), as
683
+ in Cloudflare's reference vector; then the body must be served byte for byte as printed.
684
+
685
+ **3. Launch a signing browser:**
686
+
687
+ ```bash
688
+ cdpilot launch --bot-auth # or: CDPILOT_BOT_AUTH=1 cdpilot launch
689
+ cdpilot status # ... bot-auth: on (keyid <thumbprint>)
690
+ cdpilot go https://example.com # this request is signed, and so is everything after it
691
+ cdpilot stop # stops the browser and the signer
692
+ ```
693
+
694
+ `launch --bot-auth` starts a small detached signer next to the browser. It uses the same
695
+ self-fork as the idle-close watcher and lives exactly as long as the browser. It holds one CDP
696
+ connection and auto-attaches to every page, popup, new tab, out-of-process iframe and worker. It
697
+ signs each request at the Fetch "Request" stage, including every redirect hop. So requests the page makes on its own are
698
+ signed as well: timers, `fetch()` after your cdpilot command has returned, a tab the page opens.
699
+ `data:`, `blob:` and `chrome-extension:` requests never leave the browser, so they pass through
700
+ unchanged. If signing a request fails, that request goes out unsigned, one line is written to
701
+ `CDPILOT_HOME/bot-auth/signers/<port>.log` (host only), and the page is never held up. Running
702
+ `launch --bot-auth` against a browser that is already up adds the signer to it.
703
+
704
+ Each request gets three headers:
705
+
706
+ | Header | Value |
707
+ |--------|-------|
708
+ | `Signature-Agent` | `"https://your-domain.com"` |
709
+ | `Signature-Input` | `sig1=("@authority" "signature-agent");created=…;keyid="<thumbprint>";alg="ed25519";expires=…;nonce="…";tag="web-bot-auth"` |
710
+ | `Signature` | `sig1=:<base64 Ed25519 signature>:` |
711
+
712
+ `expires` is `created` + 5 minutes. The `nonce` is 64 random bytes, base64-encoded, and new for
713
+ every request.
714
+
715
+ This is the **legacy** `Signature-Agent` form (draft-05 A.2.3), and it is the default because it
716
+ is the form Cloudflare's verifier accepts: Cloudflare rejects the dictionary form of later drafts.
717
+ To send the dictionary form instead (`Signature-Agent: sig1="https://your-domain.com"`, covered as
718
+ `"signature-agent";key="sig1"`), run `cdpilot bot-auth format dict` (`bot-auth format legacy`
719
+ switches back, `bot-auth format` shows it, `bot-auth init --agent-format dict` sets it at
720
+ creation). It applies from the next `launch --bot-auth`.
721
+
722
+ **What is not signed.** WebSocket handshakes are not signed: the browser does not pass them
723
+ through CDP's `Fetch` domain, so a `wss://` connection the page opens goes out without the
724
+ headers. Service-worker-internal cache hits and `data:`/`blob:` URLs never reach a server.
725
+
726
+ **If the signer dies or hangs.** While it runs, the signer holds every request of the browser at
727
+ the Fetch stage until it has added the headers. If it is killed, requests go out unsigned; `cdpilot
728
+ status` and `cdpilot go` then print one stderr line (signer not running, requests go out unsigned,
729
+ run `cdpilot launch --bot-auth` again) until you do, or `stop`. If it is frozen (a
730
+ stopped process, a debugger), requests hang: `cdpilot stop` and `launch --bot-auth` again.
731
+ cdpilot trusts its signer state only for the process whose command line is that port's signer and
732
+ only for the browser it attached to, so a stale state file whose pid was reused by another program
733
+ is dropped, never signalled.
734
+
735
+ **Idle close** still works while the signer runs: the signer is attached to every page, so instead
736
+ of "a page is attached" cdpilot counts the CDP clients other than the signer (by socket owner:
737
+ `netstat` with an `lsof` fallback on macOS, `netstat` on Windows, `ss` on Linux), and a `watch` daemon or a Playwright session keeps
738
+ the browser open. On Linux, `ss -tnp` shows only your own processes' sockets, so a client running
739
+ as another user is not counted and idle close may close the browser under it.
740
+
741
+ **Stealth conflict.** Signing says "I am an agent", and stealth says "I am not". If you pass
742
+ `--bot-auth` together with `--stealth`/`--undetected`, or while `cdpilot mode` is `stealth` or
743
+ `undetected`, cdpilot prints one warning line and applies bot-auth. While the signer runs, no
744
+ stealth script or user-agent override is injected, and adaptive escalation does not retry at a
745
+ stealth tier.
746
+
747
+ **Register with Cloudflare.** Once the directory is live, submit it through the Bot Submission
748
+ Form in the Cloudflare dashboard as a *signed agent* (an agent acting for its users) or a
749
+ *verified bot*; see Cloudflare's
750
+ [signed agents](https://blog.cloudflare.com/signed-agents/) and
751
+ [verified bots with cryptography](https://blog.cloudflare.com/verified-bots-with-cryptography/)
752
+ posts. Sites behind Cloudflare then see your requests as coming from your agent. Sites that
753
+ don't check Web Bot Auth ignore the headers.
754
+
508
755
  ### Friction Ladder (progressive anti-bot detection)
509
756
 
510
757
  Real sites don't just throw a CAPTCHA — they stack defenses incrementally.
@@ -633,6 +880,7 @@ Earlier figures on this page (sannysoft 24/24, intoli 6/6) were measured on v0.4
633
880
 
634
881
  ```bash
635
882
  cdpilot browser [name|auto] # workload-aware browser selection
883
+ cdpilot browser install chrome-for-testing # for extension development (see above)
636
884
  cdpilot health # JSON: alive, port, tabs, browser, today's crashes, idle close
637
885
  ```
638
886
 
@@ -728,6 +976,187 @@ never changes a command's output or exit code, and a failed write costs one
728
976
  stderr line. `CDPILOT_LOG=0` turns it off; `CDPILOT_LOG_DAYS` (default 14)
729
977
  sets how many days are kept. The MCP server exposes it as `browser_log`.
730
978
 
979
+ ### Record & replay
980
+
981
+ Record sequences of CLI commands into a portable flow JSON file and replay them with variable substitution:
982
+
983
+ ```bash
984
+ cdpilot record start [--name <n>]
985
+ cdpilot record status
986
+ cdpilot record cancel
987
+ cdpilot go https://example.com
988
+ cdpilot click "#login"
989
+ cdpilot fill "#email" test@test.com
990
+ cdpilot record stop [--save login-flow.json]
991
+ cdpilot replay login-flow.json [--var k=v ...] [--delay real|none|<ms>] [--dry-run] [--continue]
992
+ ```
993
+
994
+ Commands that run while recording is active are appended to `$CDPILOT_HOME/record/active.json`; secrets are replaced by `{{name}}` placeholders before a step is written to active.json. `CDPILOT_LOG=0` does not turn recording off, and steps run by `replay` are not recorded. Failed steps (`exit != 0`) are dropped at `record stop`. A value becomes a placeholder `{{name}}` cataloged under `vars` if the selector/label is secret-shaped (`#password`, `input[type=password]`, API key), or the value matches a token/bearer pattern or secret URL parameter. Other typed values (emails, search terms, anything the rules do not recognise) are stored verbatim, so review a flow before sharing it; `--save` files are created with mode 0600. In `replay`, `--delay real` reproduces recorded inter-step timing (capped at 10 s per gap), while `--delay none` or a fixed millisecond delay skips waiting. Variables are supplied via `--var k=v` or `CDPILOT_VAR_<NAME>`; missing variables halt replay before any step runs.
995
+
996
+ ### Use your own browser (human-in-the-loop)
997
+
998
+ An agent hits a CAPTCHA or a login wall it cannot get past. A person solves it,
999
+ and the agent carries on in the same browser.
1000
+
1001
+ **The simplest way needs no `connect`.** cdpilot's own browser is a normal,
1002
+ visible window unless you launch it headless (`cdpilot headless on` /
1003
+ `CHROME_HEADLESS=1`). The person solves the CAPTCHA or logs in right there, and
1004
+ the agent continues:
1005
+
1006
+ ```bash
1007
+ cdpilot go https://example.com/dashboard # agent: "captcha detected" / login wall
1008
+ cdpilot captcha-wait 300 # agent waits while the person solves it
1009
+ cdpilot go https://example.com/dashboard # agent continues in the same window
1010
+ ```
1011
+
1012
+ That window uses cdpilot's own profile, so the login stays for later runs.
1013
+
1014
+ **`connect` is for a browser you start yourself**, with a debugging port and
1015
+ your own profile directory (log into it once; it keeps its cookies):
1016
+
1017
+ ```bash
1018
+ # macOS
1019
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
1020
+ --remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome"
1021
+ # Linux
1022
+ google-chrome --remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome"
1023
+ # Windows
1024
+ "%ProgramFiles%\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9450 --user-data-dir="%USERPROFILE%\cdpilot-chrome"
1025
+
1026
+ cdpilot connect 9450 # or: cdpilot connect ws://127.0.0.1:9450/devtools/browser/<id>
1027
+ cdpilot connect --auto # or: find it via a DevToolsActivePort file
1028
+ cdpilot go https://example.com/dashboard
1029
+ cdpilot disconnect # forget it; the browser keeps running
1030
+ ```
1031
+
1032
+ Since Chrome 136, `--remote-debugging-port` is ignored on the default profile,
1033
+ so the separate `--user-data-dir` is required. `connect --auto` reads the
1034
+ DevToolsActivePort file of each known profile directory (`~/cdpilot-chrome`
1035
+ from the command above, Chrome stable/beta/dev/canary, Chromium, Brave,
1036
+ Vivaldi, Edge), skips files left over from a browser that is gone or was
1037
+ replaced on that port (the browser id in the file must match), and connects to
1038
+ the first live one. It asks over HTTP only; a WebSocket probe (which Chrome's
1039
+ chrome://inspect mode may answer with a permission prompt) is sent only when no
1040
+ live browser was found.
1041
+
1042
+ **Not supported yet: Chrome's `chrome://inspect/#remote-debugging` toggle**
1043
+ (Chrome 144+, your everyday logged-in profile). In that mode Chrome answers
1044
+ only on its WebSocket (`/json/version` and `/json` return 404) and may ask you
1045
+ to allow every new debugging connection, while cdpilot lists tabs over `/json`
1046
+ and opens a new connection per command. `connect --auto` recognises the mode
1047
+ and exits with code 2 and the start command above instead of half-working.
1048
+
1049
+ **What cdpilot does with a connected browser:**
1050
+
1051
+ - Page commands (`go`, `click`, `fill`, `shot`, …) run in a tab cdpilot opens
1052
+ for itself in a new window that does not take the focus
1053
+ (`Target.createTarget` with `newWindow` + `background`, so your window and
1054
+ tab keep it) on the first page command, and keep using that tab. `close`,
1055
+ `session-close` and the idle session cleanup close only such tabs, the ones
1056
+ cdpilot opened in this browser run, never the last one. Your own tabs are
1057
+ never navigated or typed into; only a command that names a tab touches it
1058
+ (`switch-tab`, `close-tab <index|id>`, `CDPILOT_TARGET`, and `multi-eval`,
1059
+ which runs its script in every open tab).
1060
+ - It never closes or kills the browser. `close`, `close --force`, `stop`,
1061
+ `stop --smart [--force]`, `project-stop`, `stop-all`, `session-close` and MCP
1062
+ `browser_close` close at most cdpilot's own tabs (above) and print
1063
+ "connected browser left running; run `cdpilot disconnect` to forget it".
1064
+ The idle auto-close never touches it,
1065
+ `tabs --reap` closes nothing, `wipe`, `permission`, `cookies load` and `cf-replay` are refused (add `--allow-external` to load/replay cookies anyway),
1066
+ `context close` only destroys a context `context create` made, and `launch` /
1067
+ MCP `browser_launch` start nothing.
1068
+ - `close-tab` is an explicit command and **can close your tabs**: the active
1069
+ (cdpilot) tab with no argument, or any tab you name by index or id. It never
1070
+ closes the last tab, which would quit the browser on Windows and Linux.
1071
+ - It injects nothing into it: no stealth patch or user-agent override (whatever
1072
+ `cdpilot mode` / `stealth` says), no glow, no input blocker, no dev-extension
1073
+ scripts, no auto-cookie restore.
1074
+ - The registration stays until you run `cdpilot disconnect`. If the browser is
1075
+ closed, page commands exit 1 with "your connected browser is gone; run
1076
+ `cdpilot connect` again or `cdpilot disconnect`", and cdpilot never launches a
1077
+ browser in its place.
1078
+ - `connect` refuses when this project already has a browser cdpilot launched
1079
+ ("stop it first (`cdpilot stop`) or use another project"), and only
1080
+ `127.0.0.1` / `localhost` is accepted (anything else exits 2). `disconnect`
1081
+ also stops this project's `watch` daemon.
1082
+ - cdpilot treats a browser as its own only with proof: the browser id it
1083
+ recorded at `launch` (or, for older entries, a live recorded pid that holds
1084
+ the debug port). A busy port is not proof, so a leftover registry entry never
1085
+ makes `stop` / `close` touch your own Chrome on port 9222.
1086
+
1087
+ **Risks:**
1088
+
1089
+ - Every cdpilot command, and any agent that can run cdpilot, can read and act
1090
+ with the cookies and sessions of that profile (mail, banking, admin panels).
1091
+ Use a profile that is logged into only what the task needs.
1092
+ - The debugging port is open to every local program for as long as that browser
1093
+ runs, not just to cdpilot. Close the browser when you are done.
1094
+ - Sites can tell automation more easily: cdpilot applies no stealth to a
1095
+ connected browser.
1096
+ - cdpilot's tab sits next to yours in the same window, and an agent may
1097
+ still act in one of your tabs when it names it (`switch-tab`,
1098
+ `close-tab <n>`, `multi-eval`).
1099
+
1100
+ ### WebMCP tools (opt-in)
1101
+
1102
+ [WebMCP](https://webmachinelearning.github.io/webmcp/) lets a page register
1103
+ "tools" on `document.modelContext`: imperatively with `registerTool()`, or
1104
+ declaratively with `<form toolname tooldescription>` (inputs described by
1105
+ `toolparamdescription`). cdpilot lists and calls them through the browser's own
1106
+ API, `getTools()` and `executeTool()`. In Chrome the API is behind
1107
+ `chrome://flags/#enable-webmcp-testing` (Chromium 146+), so start the browser
1108
+ with it:
1109
+
1110
+ ```bash
1111
+ cdpilot launch --webmcp # saved for this project: later launches (auto-launch too) keep it
1112
+ cdpilot go https://example.com/shop
1113
+ cdpilot tools list # name, title, description, annotations, input schema
1114
+ cdpilot tools list --json
1115
+ cdpilot tools call add_to_cart '{"sku":"A1","qty":2}'
1116
+ cdpilot tools call add_to_cart --arg sku=A1 --arg qty=2
1117
+ cdpilot launch --no-webmcp # turn it off again (applies at the next start)
1118
+ ```
1119
+
1120
+ - `launch --webmcp` adds `--enable-features=WebMCP` to the browser's command
1121
+ line and saves the mode next to the stealth mode (`webmcp.json` in the
1122
+ project profile); `CDPILOT_WEBMCP=1|0` overrides it. `cdpilot status` shows a
1123
+ WebMCP line while the mode is on.
1124
+ Chrome reads feature flags only at startup: a browser that is already running
1125
+ keeps its flags until `cdpilot stop`.
1126
+ - `tools list` shows the tools of the page and of its same-origin iframes
1127
+ (with the frame URL; every readable frame is asked for its own tools, so a
1128
+ frame tool missing from the top document's `getTools()` is still listed and
1129
+ runs in its frame), marks form tools and whether the form submits itself
1130
+ (`toolautosubmit`), and shows `title` and the `readOnlyHint` /
1131
+ `consequentialHint` / `untrustedContentHint` annotations. Nothing is injected
1132
+ into the page: the list is the browser's registry at that moment, so it
1133
+ follows reloads and navigations. Both commands run in cdpilot's own isolated
1134
+ world (no `Runtime.enable`), so a page script that wraps `getTools()` /
1135
+ `executeTool()` cannot add tools, change results or see the calls; only if
1136
+ that world cannot see `document.modelContext` does cdpilot use the page's
1137
+ main world, and it says so on stderr. With no tools it says why (WebMCP not
1138
+ enabled in the running browser, not a secure context, browser too old,
1139
+ document not origin-keyed, or the page registered none) and exits 0.
1140
+ - `tools call` checks the arguments against the tool's `inputSchema` first
1141
+ (`required`, `type` — `true` is not an integer —, `enum`, `const`, nested
1142
+ `properties` and `items`; other keywords are left to the page) and exits 1
1143
+ on a mismatch, an unknown tool or a tool error. It passes an `AbortSignal`
1144
+ and aborts it when `--timeout` (default 20 s) runs out, then exits 124
1145
+ with `timed out after <N>s (--timeout); its execution was aborted`. The
1146
+ signal fires at the `--timeout` deadline; the watchdog waits 3 s more so
1147
+ that report is printed.
1148
+ A form tool without `toolautosubmit` waits for a person to submit the form.
1149
+ When several frames register the same name, `--frame <url-part>` picks one
1150
+ (an exact frame URL wins); without it the top document's tool is used, and
1151
+ if there is none the call stops and lists the frames.
1152
+ - MCP: `browser_site_tools` and `browser_site_tool_call` are listed only while
1153
+ the project's WebMCP mode is on (or `CDPILOT_WEBMCP=1` for the server).
1154
+
1155
+ > **Security**: `tools call` runs the page's own code, and tool names,
1156
+ > descriptions and results are page content: treat them as untrusted.
1157
+ > Arguments and results are written to the session log with secret-named
1158
+ > fields (password, token, key, secret, auth, ...) redacted at any depth.
1159
+
731
1160
  ### Scaling & Workstation Use
732
1161
 
733
1162
  ```bash
@@ -845,9 +1274,9 @@ print(result.stdout)
845
1274
  | v0.6.0 | + captcha solver + cookies-auto (regression) | 15 / 80 | 18.75% |
846
1275
  | **v0.8.0** | **Full — cookies safe-host scoped + per-task wipe (no proxy, no TLS fork)** | **29 / 80** | **36.25%** |
847
1276
  | v0.7.0 (slot) | + named proxy pools | _depends on user proxy_ | — |
848
- | v0.8.0 (slot) | + TLS-aware launcher (camoufox/undetected-chrome) | _depends on browser choice_ | — |
1277
+ | v0.8.0 (slot) | TLS-aware launcher — withdrawn: camoufox / undetected-chrome do not speak CDP (CHANGELOG 0.8.0) | — | — |
849
1278
 
850
- **What cdpilot does not do:** `cdpilot` is an avoidance engine, not a CAPTCHA solver. We prioritize structural stealth (JS fingerprinting, behavioral entropy) to prevent challenges from appearing. When a CAPTCHA blocks progress and cannot be bypassed, the task fails — that is the honest definition of our success rate. PerimeterX (2/18), GeeTest (0/4) and Akamai (1/6) are known weaknesses; v0.7+ (residential proxy) and v0.8+ (TLS fingerprint correction via camoufox) target these directly.
1279
+ **What cdpilot does not do:** `cdpilot` is an avoidance engine first; the solver plugins above (`cdpilot captcha`, bring your own key, off by default) are a fallback, not the strategy. We prioritize structural stealth (JS fingerprinting, behavioral entropy) to prevent challenges from appearing. When a CAPTCHA blocks progress and cannot be avoided, the task fails — that is the honest definition of our success rate. PerimeterX (2/18), GeeTest (0/4) and Akamai (1/6) are known weaknesses; named proxy pools (v0.7) help where the block is IP-based, while TLS-level fingerprinting cannot be fixed from inside a CDP-driven browser and remains open.
851
1280
 
852
1281
  ## Recommended Configuration
853
1282
 
@@ -886,7 +1315,7 @@ Future paid offerings:
886
1315
  - **No arbitrary file access** — MCP screenshot filenames are sanitized and restricted to the screenshots directory. Path traversal is blocked.
887
1316
  - **Safe CSS selectors** — All selectors passed to `querySelector` are JSON-escaped to prevent injection.
888
1317
  - **No network exposure** — CDP listens on `127.0.0.1` only. Remote connections are not possible by default.
889
- - **No dependencies** — Zero npm/Python runtime dependencies means zero supply-chain attack surface.
1318
+ - **One dependency** — zero npm packages; the Python side needs only `websockets`. A one-package supply chain is small enough to audit.
890
1319
 
891
1320
  Found a vulnerability? Please email the maintainer directly instead of opening a public issue.
892
1321
 
@@ -924,13 +1353,13 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
924
1353
  - [x] **10 built-in test assertions** — assert, assert-url, assert-title, assert-count, assert-value, assert-attr, assert-visible/hidden, wait-for, check (batch), screenshot-diff
925
1354
  - [x] **`test` runner + `trace` viewer** — `cdpilot test [--watch]` runs `*.cdpt.js` files; `cdpilot trace open <run>` opens a time-travel trace viewer for a run
926
1355
  - [x] **Accessibility tree snapshot** (`a11y-snapshot`) — structured text with @ref handles the agent can act on directly, no vision model needed; 1.4–42× smaller than the page's raw HTML on the four pages we measured (2026-09-27). Not always cheaper than a screenshot: link-heavy pages produce more text than a small screenshot costs
927
- - [x] **Token-efficient screenshots** — element-level crop (13x smaller), JPEG quality control, format selection
1356
+ - [x] **Token-efficient screenshots** — element-level crop, JPEG quality control, format selection
928
1357
  - [x] **Vision fallback** (`describe`) — a11y + screenshot + text in one call
929
1358
  - [x] **Annotated screenshots** — @N badge overlays on interactive elements
930
1359
  - [x] **Auto-wait** — MutationObserver-based, 5s automatic element waiting
931
1360
  - [x] **`wait-for-text`** — adaptive text-based waiting (subtree + characterData) for streaming AI responses, async toasts, and selector-less synchronization
932
1361
  - [x] **`eval-batch`** — run N JS expressions in 1 CDP roundtrip (5-30x speedup vs sequential `eval`)
933
- - [x] **`block`** — request blocking via `Network.setBlockedURLs` with built-in presets (images/fonts/ads/media), 3-10x faster page loads on opt-in
1362
+ - [x] **`block`** — request blocking via `Network.setBlockedURLs` with built-in presets (images/fonts/ads/media); fewer bytes and faster loads on opt-in (measure on your own pages)
934
1363
  - [x] **`dismiss`** — heuristic auto-click for LLM chat sign-up walls (EN+TR pattern library, destructive-action guards)
935
1364
  - [x] **`adaptive`** — auto-escalate to stealth on CAPTCHA-protected hosts, persistent per-host memory ("run fast, climb walls")
936
1365
  - [x] **`cookies save/load`** — export/import cookies as JSON (replay CF/DataDome clearance across runs)