cdpilot 0.9.3 → 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 +392 -7
- package/bin/cdpilot.js +159 -15
- package/package.json +3 -2
- package/src/cdpilot.py +4660 -308
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,8 @@ 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
|
+
|
|
147
220
|
### Element targeting inside iframes
|
|
148
221
|
|
|
149
222
|
Card forms (Stripe/iyzico-style), embedded login widgets and the reCAPTCHA
|
|
@@ -191,9 +264,17 @@ cdpilot frame eval --frame "#card" "document.title" # runs in the frame's
|
|
|
191
264
|
(`DOM.describeNode` → frame id → the frame's execution context, or
|
|
192
265
|
`Target.attachToTarget` with a flat session for an out-of-process frame),
|
|
193
266
|
never through `contentDocument`, which the browser blocks cross-origin.
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
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.
|
|
197
278
|
- `smart-click` / `smart-fill` / `smart-select` look in the page first and
|
|
198
279
|
compare texts with whitespace collapsed (` ` and line breaks count as
|
|
199
280
|
one space). An enabled page element whose text or label contains the whole
|
|
@@ -462,6 +543,8 @@ At launch, cdpilot also passes `--disable-blink-features=AutomationControlled`,
|
|
|
462
543
|
which closes the Blink runtime flag that Cloudflare and DataDome probe to detect
|
|
463
544
|
an automated browser.
|
|
464
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
|
+
|
|
465
548
|
#### Three-tier stealth mode
|
|
466
549
|
|
|
467
550
|
`cdpilot mode` is the recommended entry point — one switch that sets how much
|
|
@@ -505,6 +588,145 @@ cdpilot adaptive clear # Drop the stealth host memory entirely
|
|
|
505
588
|
> when it sees one — adds the host to a persistent list, retries once with
|
|
506
589
|
> stealth on. Never auto-demotes. Conservative by design.
|
|
507
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
|
+
|
|
508
730
|
### Friction Ladder (progressive anti-bot detection)
|
|
509
731
|
|
|
510
732
|
Real sites don't just throw a CAPTCHA — they stack defenses incrementally.
|
|
@@ -633,6 +855,7 @@ Earlier figures on this page (sannysoft 24/24, intoli 6/6) were measured on v0.4
|
|
|
633
855
|
|
|
634
856
|
```bash
|
|
635
857
|
cdpilot browser [name|auto] # workload-aware browser selection
|
|
858
|
+
cdpilot browser install chrome-for-testing # for extension development (see above)
|
|
636
859
|
cdpilot health # JSON: alive, port, tabs, browser, today's crashes, idle close
|
|
637
860
|
```
|
|
638
861
|
|
|
@@ -728,6 +951,168 @@ never changes a command's output or exit code, and a failed write costs one
|
|
|
728
951
|
stderr line. `CDPILOT_LOG=0` turns it off; `CDPILOT_LOG_DAYS` (default 14)
|
|
729
952
|
sets how many days are kept. The MCP server exposes it as `browser_log`.
|
|
730
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
|
+
|
|
731
1116
|
### Scaling & Workstation Use
|
|
732
1117
|
|
|
733
1118
|
```bash
|