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 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 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,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
- - 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.
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 (`&nbsp;` 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