cdpilot 0.9.2 → 0.9.4
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.
- package/README.md +523 -7
- package/bin/cdpilot.js +207 -13
- package/package.json +3 -2
- package/src/cdpilot.py +7098 -597
package/README.md
CHANGED
|
@@ -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
|
|
60
|
-
(no error, no warning)
|
|
61
|
-
-
|
|
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,37 @@ 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
|
+
### MCP Bundle (.mcpb)
|
|
145
|
+
|
|
146
|
+
For MCP clients that support one-click local server installation (Claude
|
|
147
|
+
Desktop and others), download the `.mcpb` file attached to the
|
|
148
|
+
[latest release](https://github.com/mehmetnadir/cdpilot/releases/latest) and
|
|
149
|
+
open it, or build it yourself:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm run build:mcpb # writes cdpilot.mcpb at the repo root
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The bundle still needs the same runtime on the machine that runs it: Node.js
|
|
156
|
+
18+, Python 3.10+, and Brave/Chrome/Chromium installed. See
|
|
157
|
+
[`manifest.json`](manifest.json) for the bundle's MCP Bundle manifest
|
|
158
|
+
([spec](https://github.com/modelcontextprotocol/mcpb)).
|
|
159
|
+
|
|
89
160
|
### Upgrading from 0.4.x → 0.5.0 — read this first
|
|
90
161
|
|
|
91
162
|
**One breaking change**, the rest is additive.
|
|
@@ -144,6 +215,86 @@ cdpilot scroll-to <selector> # Scroll element into view
|
|
|
144
215
|
cdpilot drag <from> <to> # Drag and drop
|
|
145
216
|
```
|
|
146
217
|
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
### Element targeting inside iframes
|
|
221
|
+
|
|
222
|
+
Card forms (Stripe/iyzico-style), embedded login widgets and the reCAPTCHA
|
|
223
|
+
checkbox live inside `<iframe>`s. Name the frame chain before the element
|
|
224
|
+
selector with `>>>`, or pass `--frame`:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Payment-style page: the card fields are inside <iframe id="card" title="Secure card payment">
|
|
228
|
+
cdpilot fill "iframe#card >>> input[name=cardnumber]" "4242424242424242"
|
|
229
|
+
cdpilot type --frame "#card" "input[name=cvc]" "123"
|
|
230
|
+
cdpilot click "iframe[title='Secure card payment'] >>> button#pay-btn"
|
|
231
|
+
|
|
232
|
+
# Nested frames: outermost first
|
|
233
|
+
cdpilot click "iframe.checkout >>> iframe.card >>> button"
|
|
234
|
+
|
|
235
|
+
# --frame <selector|index|url-substring>, nestable with >>>
|
|
236
|
+
cdpilot click --frame 0 "button.submit" # first iframe (same order as `frame list`)
|
|
237
|
+
cdpilot fill --frame "url=js.stripe.com" "input[name=cardnumber]" "4242424242424242"
|
|
238
|
+
cdpilot frame list --frame "#card" # iframes inside that frame
|
|
239
|
+
cdpilot frame eval --frame "#card" "document.title" # runs in the frame's own page context
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- Works for `click`, `fill`, `type`, `submit`, `hover`, `dblclick`, `rightclick`,
|
|
243
|
+
`smart-click`, `smart-fill`, `smart-select` and `frame list|eval|shadow`.
|
|
244
|
+
- A frame hop is a CSS selector, a 0-based index, a `name`/`id`, or
|
|
245
|
+
`url=<src substring>`. `--frame` also takes a bare `src` substring.
|
|
246
|
+
`>>>` inside quotes or `[...]` is literal. A selector that matches an
|
|
247
|
+
element *wrapping* an iframe (Stripe's `#card-element`) enters the iframe
|
|
248
|
+
inside it and says so on stderr:
|
|
249
|
+
`note: '#card-element' is not an iframe; using the iframe inside it`.
|
|
250
|
+
- Selectors and texts that already contain `>>>` keep working: when the part
|
|
251
|
+
before the first `>>>` finds no iframe in the page, or a segment is empty
|
|
252
|
+
(`click "Next >>>"`), the whole string is used as written, exactly as
|
|
253
|
+
before. If that fails too, one stderr line explains:
|
|
254
|
+
`note: 'Home' matched no iframe; used the selector as written`.
|
|
255
|
+
`--frame` never falls back: a frame it cannot find is an error (exit 1).
|
|
256
|
+
- For `smart-click` / `smart-fill` / `smart-select` the part before the first
|
|
257
|
+
`>>>` is a frame only if it looks like a selector (it has `#`, `.` or `[`,
|
|
258
|
+
or starts with an `iframe`/`frame` tag) or is `url=…`; words stay text.
|
|
259
|
+
`smart-click "Main >>> Settings"` clicks the page's "Main >>> Settings"
|
|
260
|
+
link even when a `<main>` element holds an iframe. For a frame index or a
|
|
261
|
+
bare name use `--frame`.
|
|
262
|
+
- Same-origin **and cross-origin** frames work, including out-of-process
|
|
263
|
+
iframes (site isolation): cdpilot resolves each hop over CDP
|
|
264
|
+
(`DOM.describeNode` → frame id → the frame's execution context, or
|
|
265
|
+
`Target.attachToTarget` with a flat session for an out-of-process frame),
|
|
266
|
+
never through `contentDocument`, which the browser blocks cross-origin.
|
|
267
|
+
- Inside a frame, `click` and `smart-click` are real mouse input
|
|
268
|
+
(`isTrusted: true`), like `hover`, `dblclick`, `rightclick` and
|
|
269
|
+
`--entropy=on` clicks; an element with no box falls back to a script click.
|
|
270
|
+
Mouse input is dispatched at page coordinates: each frame's offset and scale
|
|
271
|
+
(`transform: scale()`, `zoom`) in the page is applied automatically, so the
|
|
272
|
+
browser hit-tests into the right frame. Before pressing, cdpilot checks the
|
|
273
|
+
point (`elementFromPoint` in each document on the way); if something covers
|
|
274
|
+
the target, such as a cookie banner, it clicks the target by script instead
|
|
275
|
+
and says so on one stderr line (`note: … is covered by div#cookie at the
|
|
276
|
+
click point; used a script click`). cdpilot's own input blocker (`show on`,
|
|
277
|
+
MCP sessions) lets only cdpilot's click through, for the click itself.
|
|
278
|
+
- `smart-click` / `smart-fill` / `smart-select` look in the page first and
|
|
279
|
+
compare texts with whitespace collapsed (` ` and line breaks count as
|
|
280
|
+
one space). An enabled page element whose text or label contains the whole
|
|
281
|
+
query is used. If the page's only such elements are disabled, the command
|
|
282
|
+
fails as before (`no enabled element matches …`) without looking in frames.
|
|
283
|
+
Otherwise the visible frames are searched breadth-first for a whole-query
|
|
284
|
+
match (at most 20 frames, and 2 s for all of the search's CDP calls) and the
|
|
285
|
+
match is reported on stderr:
|
|
286
|
+
`smart-click: matched inside frame iframe#card (https://…)`. The search only
|
|
287
|
+
looks (no click, no typing); the command then acts once, in the chosen
|
|
288
|
+
frame. With no frame match, the page's partial (word) match is used as
|
|
289
|
+
before. When the 2 s run out:
|
|
290
|
+
`smart-click: frame search stopped after 2s (3 of 7 frames)`; a frame that
|
|
291
|
+
had not answered by then has not been touched.
|
|
292
|
+
- `smart-fill` and `smart-select` search automatically only frames of the
|
|
293
|
+
page's own origin, so a typed value never lands in a third-party frame (an
|
|
294
|
+
ad, chat or payment widget) by accident; reach a cross-origin frame with
|
|
295
|
+
`>>>` or `--frame`. `smart-click` searches all visible frames.
|
|
296
|
+
- `frame list` without `--frame` prints the same list as before.
|
|
297
|
+
|
|
147
298
|
### Debugging
|
|
148
299
|
|
|
149
300
|
```bash
|
|
@@ -308,7 +459,8 @@ cdpilot storage # localStorage contents
|
|
|
308
459
|
cdpilot upload <sel> <file> # Upload file to input
|
|
309
460
|
cdpilot multi-eval <js> # Execute JS in all tabs
|
|
310
461
|
cdpilot headless [on|off] # Toggle headless mode
|
|
311
|
-
cdpilot frame list # List iframes
|
|
462
|
+
cdpilot frame list # List iframes (index = --frame <index>)
|
|
463
|
+
cdpilot frame eval --frame <f> <js> # Run JS inside an iframe (cross-origin too)
|
|
312
464
|
cdpilot dialog auto-accept # Auto-accept dialogs
|
|
313
465
|
cdpilot permission grant geo # Grant geolocation
|
|
314
466
|
```
|
|
@@ -391,6 +543,8 @@ At launch, cdpilot also passes `--disable-blink-features=AutomationControlled`,
|
|
|
391
543
|
which closes the Blink runtime flag that Cloudflare and DataDome probe to detect
|
|
392
544
|
an automated browser.
|
|
393
545
|
|
|
546
|
+
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).
|
|
547
|
+
|
|
394
548
|
#### Three-tier stealth mode
|
|
395
549
|
|
|
396
550
|
`cdpilot mode` is the recommended entry point — one switch that sets how much
|
|
@@ -434,6 +588,145 @@ cdpilot adaptive clear # Drop the stealth host memory entirely
|
|
|
434
588
|
> when it sees one — adds the host to a persistent list, retries once with
|
|
435
589
|
> stealth on. Never auto-demotes. Conservative by design.
|
|
436
590
|
|
|
591
|
+
### Web Bot Auth (signed agent)
|
|
592
|
+
|
|
593
|
+
The opposite of stealth. Stealth tries to make the browser look like a person;
|
|
594
|
+
Web Bot Auth says "this is an automated agent, and here is proof of which one".
|
|
595
|
+
Every request the browser makes carries an Ed25519
|
|
596
|
+
[HTTP Message Signature (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421)
|
|
597
|
+
that a site, or Cloudflare in front of it, checks against a key directory you
|
|
598
|
+
publish on your own domain. The format follows
|
|
599
|
+
[draft-meunier-web-bot-auth-architecture](https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/)
|
|
600
|
+
and [draft-meunier-http-message-signatures-directory](https://datatracker.ietf.org/doc/draft-meunier-http-message-signatures-directory/),
|
|
601
|
+
and reproduces the Ed25519 test vectors of Cloudflare's reference
|
|
602
|
+
implementation ([cloudflare/web-bot-auth](https://github.com/cloudflare/web-bot-auth)).
|
|
603
|
+
|
|
604
|
+
**Optional dependency.** Signing needs the `cryptography` package, for the Python cdpilot runs
|
|
605
|
+
on (`CDPILOT_PYTHON` if you set it):
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
pip install cryptography
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Without it, the `bot-auth` commands and `launch --bot-auth` print that hint and exit 2. Every
|
|
612
|
+
other command works as before.
|
|
613
|
+
|
|
614
|
+
**1. Generate the key** (once):
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
cdpilot bot-auth init --agent-url https://your-domain.com
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
This writes an Ed25519 private key to `CDPILOT_HOME/bot-auth/ed25519.key` (default
|
|
621
|
+
`~/.cdpilot`), created with mode `0600`. cdpilot never prints or logs the key, and it warns
|
|
622
|
+
when the file is readable by others. The agent URL must be an `https://` origin (no path). A
|
|
623
|
+
second `init` refuses to replace the key unless you pass `--force`, because the new key would no
|
|
624
|
+
longer match the directory you published.
|
|
625
|
+
|
|
626
|
+
**2. Host the directory on your own domain, signed.** `cdpilot bot-auth directory` prints the
|
|
627
|
+
public key as a JWKS, where `kid` is the key's RFC 7638 JWK thumbprint. Cloudflare also requires
|
|
628
|
+
the directory *response* to be signed, so `--headers` prints the headers to serve with it:
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
cdpilot bot-auth directory --headers # headers, a blank line, then the JSON body
|
|
632
|
+
cdpilot bot-auth directory --headers --json # the same as {"authority", "headers", "body"}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
Content-Type: application/http-message-signatures-directory+json
|
|
637
|
+
Signature-Input: sig1=("@authority";req);created=…;keyid="<thumbprint>";alg="ed25519";expires=…;nonce="…";tag="http-message-signatures-directory"
|
|
638
|
+
Signature: sig1=:<base64 Ed25519 signature>:
|
|
639
|
+
|
|
640
|
+
{ "keys": [ … ] }
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Serve that body at `https://your-domain.com/.well-known/http-message-signatures-directory` with
|
|
644
|
+
those three headers. The signature covers `("@authority";req)`, the host the directory is fetched
|
|
645
|
+
from: by default the agent URL's host, or `--authority <host>`. There is one signature per key in
|
|
646
|
+
the directory. It is valid for 24 hours (`--ttl <seconds>`), so a static host needs it refreshed
|
|
647
|
+
daily, from cron or an edge function that runs the same signing. A cron example that rewrites the
|
|
648
|
+
headers file and the body your web server serves:
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
# m h dom mon dow — every day at 03:17, 48 h validity so one missed run does not break it
|
|
652
|
+
17 3 * * * cdpilot bot-auth directory --headers --ttl 172800 --json > /var/www/wba/directory.json.tmp \
|
|
653
|
+
&& mv /var/www/wba/directory.json.tmp /var/www/wba/directory.json
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Your server (or an edge function) reads `headers` and `body` from that JSON and serves them at
|
|
657
|
+
the well-known path. `--content-digest` also covers the body (`Content-Digest`, RFC 9530), as
|
|
658
|
+
in Cloudflare's reference vector; then the body must be served byte for byte as printed.
|
|
659
|
+
|
|
660
|
+
**3. Launch a signing browser:**
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
cdpilot launch --bot-auth # or: CDPILOT_BOT_AUTH=1 cdpilot launch
|
|
664
|
+
cdpilot status # ... bot-auth: on (keyid <thumbprint>)
|
|
665
|
+
cdpilot go https://example.com # this request is signed, and so is everything after it
|
|
666
|
+
cdpilot stop # stops the browser and the signer
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`launch --bot-auth` starts a small detached signer next to the browser. It uses the same
|
|
670
|
+
self-fork as the idle-close watcher and lives exactly as long as the browser. It holds one CDP
|
|
671
|
+
connection and auto-attaches to every page, popup, new tab, out-of-process iframe and worker. It
|
|
672
|
+
signs each request at the Fetch "Request" stage, including every redirect hop. So requests the page makes on its own are
|
|
673
|
+
signed as well: timers, `fetch()` after your cdpilot command has returned, a tab the page opens.
|
|
674
|
+
`data:`, `blob:` and `chrome-extension:` requests never leave the browser, so they pass through
|
|
675
|
+
unchanged. If signing a request fails, that request goes out unsigned, one line is written to
|
|
676
|
+
`CDPILOT_HOME/bot-auth/signers/<port>.log` (host only), and the page is never held up. Running
|
|
677
|
+
`launch --bot-auth` against a browser that is already up adds the signer to it.
|
|
678
|
+
|
|
679
|
+
Each request gets three headers:
|
|
680
|
+
|
|
681
|
+
| Header | Value |
|
|
682
|
+
|--------|-------|
|
|
683
|
+
| `Signature-Agent` | `"https://your-domain.com"` |
|
|
684
|
+
| `Signature-Input` | `sig1=("@authority" "signature-agent");created=…;keyid="<thumbprint>";alg="ed25519";expires=…;nonce="…";tag="web-bot-auth"` |
|
|
685
|
+
| `Signature` | `sig1=:<base64 Ed25519 signature>:` |
|
|
686
|
+
|
|
687
|
+
`expires` is `created` + 5 minutes. The `nonce` is 64 random bytes, base64-encoded, and new for
|
|
688
|
+
every request.
|
|
689
|
+
|
|
690
|
+
This is the **legacy** `Signature-Agent` form (draft-05 A.2.3), and it is the default because it
|
|
691
|
+
is the form Cloudflare's verifier accepts: Cloudflare rejects the dictionary form of later drafts.
|
|
692
|
+
To send the dictionary form instead (`Signature-Agent: sig1="https://your-domain.com"`, covered as
|
|
693
|
+
`"signature-agent";key="sig1"`), run `cdpilot bot-auth format dict` (`bot-auth format legacy`
|
|
694
|
+
switches back, `bot-auth format` shows it, `bot-auth init --agent-format dict` sets it at
|
|
695
|
+
creation). It applies from the next `launch --bot-auth`.
|
|
696
|
+
|
|
697
|
+
**What is not signed.** WebSocket handshakes are not signed: the browser does not pass them
|
|
698
|
+
through CDP's `Fetch` domain, so a `wss://` connection the page opens goes out without the
|
|
699
|
+
headers. Service-worker-internal cache hits and `data:`/`blob:` URLs never reach a server.
|
|
700
|
+
|
|
701
|
+
**If the signer dies or hangs.** While it runs, the signer holds every request of the browser at
|
|
702
|
+
the Fetch stage until it has added the headers. If it is killed, requests go out unsigned; `cdpilot
|
|
703
|
+
status` and `cdpilot go` then print one stderr line (signer not running, requests go out unsigned,
|
|
704
|
+
run `cdpilot launch --bot-auth` again) until you do, or `stop`. If it is frozen (a
|
|
705
|
+
stopped process, a debugger), requests hang: `cdpilot stop` and `launch --bot-auth` again.
|
|
706
|
+
cdpilot trusts its signer state only for the process whose command line is that port's signer and
|
|
707
|
+
only for the browser it attached to, so a stale state file whose pid was reused by another program
|
|
708
|
+
is dropped, never signalled.
|
|
709
|
+
|
|
710
|
+
**Idle close** still works while the signer runs: the signer is attached to every page, so instead
|
|
711
|
+
of "a page is attached" cdpilot counts the CDP clients other than the signer (by socket owner:
|
|
712
|
+
`netstat` on macOS and Windows, `ss` on Linux), and a `watch` daemon or a Playwright session keeps
|
|
713
|
+
the browser open. On Linux, `ss -tnp` shows only your own processes' sockets, so a client running
|
|
714
|
+
as another user is not counted and idle close may close the browser under it.
|
|
715
|
+
|
|
716
|
+
**Stealth conflict.** Signing says "I am an agent", and stealth says "I am not". If you pass
|
|
717
|
+
`--bot-auth` together with `--stealth`/`--undetected`, or while `cdpilot mode` is `stealth` or
|
|
718
|
+
`undetected`, cdpilot prints one warning line and applies bot-auth. While the signer runs, no
|
|
719
|
+
stealth script or user-agent override is injected, and adaptive escalation does not retry at a
|
|
720
|
+
stealth tier.
|
|
721
|
+
|
|
722
|
+
**Register with Cloudflare.** Once the directory is live, submit it through the Bot Submission
|
|
723
|
+
Form in the Cloudflare dashboard as a *signed agent* (an agent acting for its users) or a
|
|
724
|
+
*verified bot*; see Cloudflare's
|
|
725
|
+
[signed agents](https://blog.cloudflare.com/signed-agents/) and
|
|
726
|
+
[verified bots with cryptography](https://blog.cloudflare.com/verified-bots-with-cryptography/)
|
|
727
|
+
posts. Sites behind Cloudflare then see your requests as coming from your agent. Sites that
|
|
728
|
+
don't check Web Bot Auth ignore the headers.
|
|
729
|
+
|
|
437
730
|
### Friction Ladder (progressive anti-bot detection)
|
|
438
731
|
|
|
439
732
|
Real sites don't just throw a CAPTCHA — they stack defenses incrementally.
|
|
@@ -562,7 +855,8 @@ Earlier figures on this page (sannysoft 24/24, intoli 6/6) were measured on v0.4
|
|
|
562
855
|
|
|
563
856
|
```bash
|
|
564
857
|
cdpilot browser [name|auto] # workload-aware browser selection
|
|
565
|
-
cdpilot
|
|
858
|
+
cdpilot browser install chrome-for-testing # for extension development (see above)
|
|
859
|
+
cdpilot health # JSON: alive, port, tabs, browser, today's crashes, idle close
|
|
566
860
|
```
|
|
567
861
|
|
|
568
862
|
`cdpilot health` is designed for shell watchdogs:
|
|
@@ -584,6 +878,33 @@ Lifecycle, status and configuration commands (`launch`, `stop`, `close`,
|
|
|
584
878
|
`mcp`, `serve`, …) never launch. `CDPILOT_NO_AUTOLAUNCH=1` restores the old
|
|
585
879
|
"CDP connection error. Is the browser running?" error and exit code 1.
|
|
586
880
|
|
|
881
|
+
**Idle auto-close.** A browser that a page command auto-launched, or that the
|
|
882
|
+
MCP server launched (`browser_launch` included), closes itself after
|
|
883
|
+
**15 minutes** without a cdpilot command or a visible page change, so a
|
|
884
|
+
finished agent task does not leave it holding memory. An explicit CLI
|
|
885
|
+
`cdpilot launch` stays open (you may be browsing in it by hand) unless you ask:
|
|
886
|
+
`cdpilot launch --idle-close 30` or `CDPILOT_IDLE_CLOSE=30`. The env var also
|
|
887
|
+
sets the auto-launch delay (read when the browser starts; fractions allowed);
|
|
888
|
+
`0` turns idle close off. A browser you started yourself, or one cdpilot merely
|
|
889
|
+
attached to, is never closed.
|
|
890
|
+
|
|
891
|
+
What counts as use: any cdpilot command except the read-only checks (`status`,
|
|
892
|
+
`health`, `projects`, `version` — a `cdpilot health` watchdog loop does not keep
|
|
893
|
+
the browser alive), MCP tool calls, `serve` requests, a CDP client still
|
|
894
|
+
attached to a page (a long-running command, `watch`, Playwright via
|
|
895
|
+
`connectOverCDP`), and any change in the open pages' URLs or in the set of
|
|
896
|
+
tabs (someone navigating, a tab opened or closed). Title changes do not count,
|
|
897
|
+
so a page that rewrites its own title (a clock, an unread counter) cannot keep
|
|
898
|
+
the browser alive. `cdpilot status` and
|
|
899
|
+
`cdpilot health` show `idle close in 12m` or `idle close off`.
|
|
900
|
+
|
|
901
|
+
How: every command stamps `~/.cdpilot/projects/<id>/last-activity`; the launch
|
|
902
|
+
starts a small detached watcher (one per port, no console window on Windows)
|
|
903
|
+
that checks every ≤30 s, stops the browser like `cdpilot stop`, marks it
|
|
904
|
+
stopped in the registry and exits — it also exits as soon as the browser is
|
|
905
|
+
gone for any other reason. Browsers started by `serve --api` are managed by the
|
|
906
|
+
server and have no idle close.
|
|
907
|
+
|
|
587
908
|
**Timeouts.** Any command takes `--timeout <seconds>`, before or after the
|
|
588
909
|
command name, or a default from `CDPILOT_TIMEOUT` (the flag wins; `0`
|
|
589
910
|
disables). It bounds the whole command's wall-clock: on expiry cdpilot prints
|
|
@@ -600,6 +921,198 @@ CDPILOT_TIMEOUT=60 cdpilot run flow.cdp # default for every command (and each
|
|
|
600
921
|
For `mcp` and `serve` the value is not applied to the long-running server
|
|
601
922
|
itself; it is passed on to every tool call / request it runs.
|
|
602
923
|
|
|
924
|
+
### Session log
|
|
925
|
+
|
|
926
|
+
Every command (and every MCP tool call) appends one JSON line to a local,
|
|
927
|
+
per-project log, so when a browser task is done there is a record of what was
|
|
928
|
+
done and found. Nothing leaves the machine.
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
cdpilot log # today's commands: time, exit, command, url, result
|
|
932
|
+
cdpilot log --md # Markdown report: pages visited, actions, errors, files produced
|
|
933
|
+
cdpilot log --json # raw lines (one JSON object per command)
|
|
934
|
+
cdpilot log --days 3 # include the last 3 days
|
|
935
|
+
cdpilot log --path # where the files are
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
Each line has `ts`, `cmd`, `args`, `exit`, `duration_ms`, the page `url` and
|
|
939
|
+
`title` after the command (when the command already had them), a ~200-char
|
|
940
|
+
`summary` of its output, the `error` line, and `files` it wrote (screenshots,
|
|
941
|
+
PDFs). Files live in `~/.cdpilot/projects/<project-id>/log/<YYYY-MM-DD>.jsonl`.
|
|
942
|
+
|
|
943
|
+
Redaction happens before anything is written: values given to `fill`, `type`,
|
|
944
|
+
`smart-fill`, `smart-select`, `assert-value` and `dialog prompt` become
|
|
945
|
+
`«redacted:N chars»`; so do values of password/token/key/secret/cookie/auth
|
|
946
|
+
flags and headers, token-shaped arguments, and URL query/fragment values whose
|
|
947
|
+
names contain token, key, secret, password, auth, code or session. `cookies`
|
|
948
|
+
and `storage` output is never logged; `eval` source is, with string literals
|
|
949
|
+
over 40 chars cut and secret-looking ones replaced. Logging is best effort: it
|
|
950
|
+
never changes a command's output or exit code, and a failed write costs one
|
|
951
|
+
stderr line. `CDPILOT_LOG=0` turns it off; `CDPILOT_LOG_DAYS` (default 14)
|
|
952
|
+
sets how many days are kept. The MCP server exposes it as `browser_log`.
|
|
953
|
+
|
|
954
|
+
### Use your own browser (human-in-the-loop)
|
|
955
|
+
|
|
956
|
+
An agent hits a CAPTCHA or a login wall it cannot get past. A person solves it,
|
|
957
|
+
and the agent carries on in the same browser.
|
|
958
|
+
|
|
959
|
+
**The simplest way needs no `connect`.** cdpilot's own browser is a normal,
|
|
960
|
+
visible window unless you launch it headless (`cdpilot headless on` /
|
|
961
|
+
`CHROME_HEADLESS=1`). The person solves the CAPTCHA or logs in right there, and
|
|
962
|
+
the agent continues:
|
|
963
|
+
|
|
964
|
+
```bash
|
|
965
|
+
cdpilot go https://example.com/dashboard # agent: "captcha detected" / login wall
|
|
966
|
+
cdpilot captcha-wait 300 # agent waits while the person solves it
|
|
967
|
+
cdpilot go https://example.com/dashboard # agent continues in the same window
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
That window uses cdpilot's own profile, so the login stays for later runs.
|
|
971
|
+
|
|
972
|
+
**`connect` is for a browser you start yourself**, with a debugging port and
|
|
973
|
+
your own profile directory (log into it once; it keeps its cookies):
|
|
974
|
+
|
|
975
|
+
```bash
|
|
976
|
+
# macOS
|
|
977
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
978
|
+
--remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome"
|
|
979
|
+
# Linux
|
|
980
|
+
google-chrome --remote-debugging-port=9450 --user-data-dir="$HOME/cdpilot-chrome"
|
|
981
|
+
# Windows
|
|
982
|
+
"%ProgramFiles%\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9450 --user-data-dir="%USERPROFILE%\cdpilot-chrome"
|
|
983
|
+
|
|
984
|
+
cdpilot connect 9450 # or: cdpilot connect ws://127.0.0.1:9450/devtools/browser/<id>
|
|
985
|
+
cdpilot connect --auto # or: find it via a DevToolsActivePort file
|
|
986
|
+
cdpilot go https://example.com/dashboard
|
|
987
|
+
cdpilot disconnect # forget it; the browser keeps running
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Since Chrome 136, `--remote-debugging-port` is ignored on the default profile,
|
|
991
|
+
so the separate `--user-data-dir` is required. `connect --auto` reads the
|
|
992
|
+
DevToolsActivePort file of each known profile directory (`~/cdpilot-chrome`
|
|
993
|
+
from the command above, Chrome stable/beta/dev/canary, Chromium, Brave,
|
|
994
|
+
Vivaldi, Edge), skips files left over from a browser that is gone or was
|
|
995
|
+
replaced on that port (the browser id in the file must match), and connects to
|
|
996
|
+
the first live one. It asks over HTTP only; a WebSocket probe (which Chrome's
|
|
997
|
+
chrome://inspect mode may answer with a permission prompt) is sent only when no
|
|
998
|
+
live browser was found.
|
|
999
|
+
|
|
1000
|
+
**Not supported yet: Chrome's `chrome://inspect/#remote-debugging` toggle**
|
|
1001
|
+
(Chrome 144+, your everyday logged-in profile). In that mode Chrome answers
|
|
1002
|
+
only on its WebSocket (`/json/version` and `/json` return 404) and may ask you
|
|
1003
|
+
to allow every new debugging connection, while cdpilot lists tabs over `/json`
|
|
1004
|
+
and opens a new connection per command. `connect --auto` recognises the mode
|
|
1005
|
+
and exits with code 2 and the start command above instead of half-working.
|
|
1006
|
+
|
|
1007
|
+
**What cdpilot does with a connected browser:**
|
|
1008
|
+
|
|
1009
|
+
- Page commands (`go`, `click`, `fill`, `shot`, …) run in a tab cdpilot opens
|
|
1010
|
+
for itself in a new window that does not take the focus
|
|
1011
|
+
(`Target.createTarget` with `newWindow` + `background`, so your window and
|
|
1012
|
+
tab keep it) on the first page command, and keep using that tab. `close`,
|
|
1013
|
+
`session-close` and the idle session cleanup close only such tabs, the ones
|
|
1014
|
+
cdpilot opened in this browser run, never the last one. Your own tabs are
|
|
1015
|
+
never navigated or typed into; only a command that names a tab touches it
|
|
1016
|
+
(`switch-tab`, `close-tab <index|id>`, `CDPILOT_TARGET`, and `multi-eval`,
|
|
1017
|
+
which runs its script in every open tab).
|
|
1018
|
+
- It never closes or kills the browser. `close`, `close --force`, `stop`,
|
|
1019
|
+
`stop --smart [--force]`, `project-stop`, `stop-all`, `session-close` and MCP
|
|
1020
|
+
`browser_close` close at most cdpilot's own tabs (above) and print
|
|
1021
|
+
"connected browser left running; run `cdpilot disconnect` to forget it".
|
|
1022
|
+
The idle auto-close never touches it,
|
|
1023
|
+
`tabs --reap` closes nothing, `wipe`, `permission`, `cookies load` and `cf-replay` are refused (add `--allow-external` to load/replay cookies anyway),
|
|
1024
|
+
`context close` only destroys a context `context create` made, and `launch` /
|
|
1025
|
+
MCP `browser_launch` start nothing.
|
|
1026
|
+
- `close-tab` is an explicit command and **can close your tabs**: the active
|
|
1027
|
+
(cdpilot) tab with no argument, or any tab you name by index or id. It never
|
|
1028
|
+
closes the last tab, which would quit the browser on Windows and Linux.
|
|
1029
|
+
- It injects nothing into it: no stealth patch or user-agent override (whatever
|
|
1030
|
+
`cdpilot mode` / `stealth` says), no glow, no input blocker, no dev-extension
|
|
1031
|
+
scripts, no auto-cookie restore.
|
|
1032
|
+
- The registration stays until you run `cdpilot disconnect`. If the browser is
|
|
1033
|
+
closed, page commands exit 1 with "your connected browser is gone; run
|
|
1034
|
+
`cdpilot connect` again or `cdpilot disconnect`", and cdpilot never launches a
|
|
1035
|
+
browser in its place.
|
|
1036
|
+
- `connect` refuses when this project already has a browser cdpilot launched
|
|
1037
|
+
("stop it first (`cdpilot stop`) or use another project"), and only
|
|
1038
|
+
`127.0.0.1` / `localhost` is accepted (anything else exits 2). `disconnect`
|
|
1039
|
+
also stops this project's `watch` daemon.
|
|
1040
|
+
- cdpilot treats a browser as its own only with proof: the browser id it
|
|
1041
|
+
recorded at `launch` (or, for older entries, a live recorded pid that holds
|
|
1042
|
+
the debug port). A busy port is not proof, so a leftover registry entry never
|
|
1043
|
+
makes `stop` / `close` touch your own Chrome on port 9222.
|
|
1044
|
+
|
|
1045
|
+
**Risks:**
|
|
1046
|
+
|
|
1047
|
+
- Every cdpilot command, and any agent that can run cdpilot, can read and act
|
|
1048
|
+
with the cookies and sessions of that profile (mail, banking, admin panels).
|
|
1049
|
+
Use a profile that is logged into only what the task needs.
|
|
1050
|
+
- The debugging port is open to every local program for as long as that browser
|
|
1051
|
+
runs, not just to cdpilot. Close the browser when you are done.
|
|
1052
|
+
- Sites can tell automation more easily: cdpilot applies no stealth to a
|
|
1053
|
+
connected browser.
|
|
1054
|
+
- cdpilot's tab sits next to yours in the same window, and an agent may
|
|
1055
|
+
still act in one of your tabs when it names it (`switch-tab`,
|
|
1056
|
+
`close-tab <n>`, `multi-eval`).
|
|
1057
|
+
|
|
1058
|
+
### WebMCP tools (opt-in)
|
|
1059
|
+
|
|
1060
|
+
[WebMCP](https://webmachinelearning.github.io/webmcp/) lets a page register
|
|
1061
|
+
"tools" on `document.modelContext`: imperatively with `registerTool()`, or
|
|
1062
|
+
declaratively with `<form toolname tooldescription>` (inputs described by
|
|
1063
|
+
`toolparamdescription`). cdpilot lists and calls them through the browser's own
|
|
1064
|
+
API, `getTools()` and `executeTool()`. In Chrome the API is behind
|
|
1065
|
+
`chrome://flags/#enable-webmcp-testing` (Chromium 146+), so start the browser
|
|
1066
|
+
with it:
|
|
1067
|
+
|
|
1068
|
+
```bash
|
|
1069
|
+
cdpilot launch --webmcp # saved for this project: later launches (auto-launch too) keep it
|
|
1070
|
+
cdpilot go https://example.com/shop
|
|
1071
|
+
cdpilot tools list # name, title, description, annotations, input schema
|
|
1072
|
+
cdpilot tools list --json
|
|
1073
|
+
cdpilot tools call add_to_cart '{"sku":"A1","qty":2}'
|
|
1074
|
+
cdpilot tools call add_to_cart --arg sku=A1 --arg qty=2
|
|
1075
|
+
cdpilot launch --no-webmcp # turn it off again (applies at the next start)
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
- `launch --webmcp` adds `--enable-features=WebMCP` to the browser's command
|
|
1079
|
+
line and saves the mode next to the stealth mode (`webmcp.json` in the
|
|
1080
|
+
project profile); `CDPILOT_WEBMCP=1|0` overrides it. `cdpilot status` shows a
|
|
1081
|
+
WebMCP line while the mode is on.
|
|
1082
|
+
Chrome reads feature flags only at startup: a browser that is already running
|
|
1083
|
+
keeps its flags until `cdpilot stop`.
|
|
1084
|
+
- `tools list` shows the tools of the page and of its same-origin iframes
|
|
1085
|
+
(with the frame URL), marks form tools and whether the form submits itself
|
|
1086
|
+
(`toolautosubmit`), and shows `title` and the `readOnlyHint` /
|
|
1087
|
+
`consequentialHint` / `untrustedContentHint` annotations. Nothing is injected
|
|
1088
|
+
into the page: the list is the browser's registry at that moment, so it
|
|
1089
|
+
follows reloads and navigations. Both commands run in cdpilot's own isolated
|
|
1090
|
+
world (no `Runtime.enable`), so a page script that wraps `getTools()` /
|
|
1091
|
+
`executeTool()` cannot add tools, change results or see the calls; only if
|
|
1092
|
+
that world cannot see `document.modelContext` does cdpilot use the page's
|
|
1093
|
+
main world, and it says so on stderr. With no tools it says why (WebMCP not
|
|
1094
|
+
enabled in the running browser, not a secure context, browser too old,
|
|
1095
|
+
document not origin-keyed, or the page registered none) and exits 0.
|
|
1096
|
+
- `tools call` checks the arguments against the tool's `inputSchema` first
|
|
1097
|
+
(`required`, `type` — `true` is not an integer —, `enum`, `const`, nested
|
|
1098
|
+
`properties` and `items`; other keywords are left to the page) and exits 1
|
|
1099
|
+
on a mismatch, an unknown tool or a tool error. It passes an `AbortSignal`
|
|
1100
|
+
and aborts it when `--timeout` (default 20 s) runs out, then exits 124
|
|
1101
|
+
with `timed out after <N>s (--timeout); its execution was aborted`. The
|
|
1102
|
+
signal fires at the `--timeout` deadline; the watchdog waits 3 s more so
|
|
1103
|
+
that report is printed.
|
|
1104
|
+
A form tool without `toolautosubmit` waits for a person to submit the form.
|
|
1105
|
+
When several frames register the same name, `--frame <url-part>` picks one
|
|
1106
|
+
(an exact frame URL wins); without it the top document's tool is used, and
|
|
1107
|
+
if there is none the call stops and lists the frames.
|
|
1108
|
+
- MCP: `browser_site_tools` and `browser_site_tool_call` are listed only while
|
|
1109
|
+
the project's WebMCP mode is on (or `CDPILOT_WEBMCP=1` for the server).
|
|
1110
|
+
|
|
1111
|
+
> **Security**: `tools call` runs the page's own code, and tool names,
|
|
1112
|
+
> descriptions and results are page content: treat them as untrusted.
|
|
1113
|
+
> Arguments and results are written to the session log with secret-named
|
|
1114
|
+
> fields (password, token, key, secret, auth, ...) redacted at any depth.
|
|
1115
|
+
|
|
603
1116
|
### Scaling & Workstation Use
|
|
604
1117
|
|
|
605
1118
|
```bash
|
|
@@ -663,6 +1176,9 @@ print(result.stdout)
|
|
|
663
1176
|
| `CDPILOT_OFFSCREEN` | `0` | Headed but render off-screen — no window steals focus |
|
|
664
1177
|
| `CDPILOT_TIMEOUT` | unset | Default `--timeout` in seconds for every command (flag wins, `0` disables); expiry exits 124 |
|
|
665
1178
|
| `CDPILOT_NO_AUTOLAUNCH` | `0` | `1` = page commands fail with the old "Is the browser running?" error instead of launching the browser |
|
|
1179
|
+
| `CDPILOT_IDLE_CLOSE` | `15` | Minutes without a cdpilot command or page change before a browser cdpilot launched closes itself (fractions allowed, `0` = never; read at launch). Default applies to auto-launch and MCP launches; an explicit CLI `launch` is off unless this is set or `--idle-close <min>` is given |
|
|
1180
|
+
| `CDPILOT_LOG` | `1` | `0` = do not write the session log (`cdpilot log`) |
|
|
1181
|
+
| `CDPILOT_LOG_DAYS` | `14` | Days of session log to keep; older day files are deleted on the first write of a day (`0` keeps all) |
|
|
666
1182
|
|
|
667
1183
|
## How It Works
|
|
668
1184
|
|
|
@@ -823,10 +1339,10 @@ The only browser MCP with built-in test assertions. Here's what we've shipped an
|
|
|
823
1339
|
- [x] **Data extraction** (`extract`) — structured DOM data in text, JSON, or list format
|
|
824
1340
|
- [x] **Page observation** (`observe`) — list all interactive elements with available actions
|
|
825
1341
|
- [x] **Script runner** (`run`) — execute `.cdp` script files with pass/fail reporting
|
|
1342
|
+
- [x] **iframe targeting** — `"iframe#card >>> input"` / `--frame` for element commands, same-origin and cross-origin (out-of-process) frames; smart commands search frames automatically
|
|
826
1343
|
|
|
827
1344
|
### Coming Soon
|
|
828
1345
|
|
|
829
|
-
- [ ] **iframe** support — interact with elements inside iframes (Shadow DOM traversal already shipped in smart commands)
|
|
830
1346
|
- [ ] **Multi-instance pool** (`CDPILOT_POOL_SIZE`) — N independent browser processes with least-loaded dispatch
|
|
831
1347
|
- [ ] **Session recording & replay** — record browser sessions and replay them deterministically
|
|
832
1348
|
- [ ] **Stealth mode** *(Pro)* — human-like mouse/typing, anti-fingerprint, CAPTCHA solving
|