cdp-mcp 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +100 -37
  2. package/dist/contract.d.ts +11 -0
  3. package/dist/contract.js +11 -0
  4. package/dist/contract.js.map +1 -0
  5. package/dist/index.d.ts +18 -0
  6. package/dist/locator.d.ts +108 -0
  7. package/dist/locator.js +176 -0
  8. package/dist/locator.js.map +1 -0
  9. package/dist/server.d.ts +2 -0
  10. package/dist/server.js +4 -0
  11. package/dist/server.js.map +1 -1
  12. package/dist/session/browser.d.ts +29 -0
  13. package/dist/session/browser.js +17 -2
  14. package/dist/session/browser.js.map +1 -1
  15. package/dist/session/buffers.d.ts +48 -0
  16. package/dist/session/pause.d.ts +21 -0
  17. package/dist/session/state.d.ts +53 -0
  18. package/dist/sourcemap/loader.d.ts +4 -0
  19. package/dist/sourcemap/normalize.d.ts +2 -0
  20. package/dist/sourcemap/store.d.ts +57 -0
  21. package/dist/tools/_locator_runtime.d.ts +31 -0
  22. package/dist/tools/_locator_runtime.js +243 -0
  23. package/dist/tools/_locator_runtime.js.map +1 -0
  24. package/dist/tools/_register.d.ts +2 -0
  25. package/dist/tools/breakpoints.d.ts +4 -0
  26. package/dist/tools/console.d.ts +2 -0
  27. package/dist/tools/dom.d.ts +2 -0
  28. package/dist/tools/dom.js +3 -221
  29. package/dist/tools/dom.js.map +1 -1
  30. package/dist/tools/execution.d.ts +29 -0
  31. package/dist/tools/forms.d.ts +8 -0
  32. package/dist/tools/forms.js +256 -0
  33. package/dist/tools/forms.js.map +1 -0
  34. package/dist/tools/inspect.d.ts +2 -0
  35. package/dist/tools/nav.d.ts +2 -0
  36. package/dist/tools/network.d.ts +2 -0
  37. package/dist/tools/session.d.ts +2 -0
  38. package/dist/tools/session.js +1 -1
  39. package/dist/tools/session.js.map +1 -1
  40. package/dist/tools/source.d.ts +2 -0
  41. package/dist/tools/storage.d.ts +2 -0
  42. package/dist/tools/storage.js +296 -0
  43. package/dist/tools/storage.js.map +1 -0
  44. package/dist/util/browser-resolve.d.ts +19 -0
  45. package/dist/util/errors.d.ts +7 -0
  46. package/dist/util/format.d.ts +20 -0
  47. package/dist/util/log.d.ts +6 -0
  48. package/docs/chromium-sandboxing.md +197 -0
  49. package/docs/known-chromium-gaps.md +138 -0
  50. package/docs/launchd-service.md +217 -0
  51. package/docs/local-l3-e2e-setup.md +199 -0
  52. package/docs/systemd-service.md +233 -0
  53. package/package.json +18 -2
@@ -0,0 +1,138 @@
1
+ # Known Chromium gaps
2
+
3
+ Specs in the L3 e2e suite that fail (or fail intermittently) on Chromium but
4
+ pass on Chrome stable. Every entry below is a real coverage gap on Linux
5
+ ARM64 + Chromium (the day-1 primary target) — not a CI-only concession.
6
+
7
+ If the gap is mitigatable in production code, link the fix PR. If it's a CDP
8
+ protocol-version difference, link the Chromium release that includes the fix.
9
+
10
+ | Spec | Skip tag | CDP method missing/changed | Chromium version that fixes it | Tracking |
11
+ |---|---|---|---|---|
12
+ | _none yet — populate as L3 lands_ | | | | |
13
+
14
+ ## Pre-flagged risks (not yet observed; documented for triage)
15
+
16
+ These are known protocol-version-sensitive areas the test+eval plan flagged
17
+ as risky during planning. Add a row above when one of them actually fires
18
+ on the e2e suite.
19
+
20
+ - **`Network.loadNetworkResource`** — Used by `src/sourcemap/loader.ts:113`
21
+ to fetch source maps through the browser's network stack (so cookies/
22
+ origin/auth flow naturally). Older Chromium revisions ship a more limited
23
+ param set (no `options.includeCredentials`, no `options.disableCache`);
24
+ the production code already has a Node-fetch fallback, but verify the
25
+ fallback path is exercised under the older Chromium.
26
+
27
+ - **`Page.captureScreenshot`** flag set — `captureBeyondViewport` and
28
+ `quality` (when `format=jpeg`) gained options across versions. The
29
+ screenshot e2e spec asserts byte-shape, not flag-respect, so this is
30
+ most likely to surface as a "bytes don't match" assertion under older
31
+ Chromium.
32
+
33
+ ## Conventions
34
+
35
+ - Add a `// @chromium-skip — <gap-id>` comment on the spec's `it()` line.
36
+ - Set the spec to `it.skipIf(process.env.CDP_TEST_BROWSER === "chromium")` or
37
+ use vitest's `.skip` with a runtime check.
38
+ - Every skip MUST have a corresponding row in the table above. **Enforced** by
39
+ `scripts/check-chromium-skips.mjs` — runs as `pretest:e2e` on every PR and
40
+ also as `npm run lint:chromium-skips`. Greps `test/e2e/**/*.test.ts` for
41
+ `@chromium-skip` tags and `it.skipIf`/`describe.skipIf` Chromium guards,
42
+ parses the table above, exits 1 if any skip lacks a row OR any row points
43
+ at a spec that no longer exists. Zero-skip state (the current state) is
44
+ fine — the script is a no-op.
45
+
46
+ _(no entries below this line yet means no L3 specs needed a Chromium skip)_
47
+
48
+ ## Known host gaps (not Chromium-version issues)
49
+
50
+ These are host/library combinations where the e2e suite cannot run end-to-
51
+ end, separate from the per-Chromium-version skip mechanism above. Listed
52
+ here so future contributors don't waste a debug cycle.
53
+
54
+ - **Windows 11 + chrome-launcher 1.2.1.** `chrome-launcher.launch()`'s
55
+ internal startup-port poll always fails with `ECONNREFUSED` on this Win11
56
+ configuration, regardless of headless mode (`--headless=new`, classic
57
+ `--headless`, or non-headless), browser (Chrome stable from Program
58
+ Files, Playwright-bundled Chromium under `~/AppData/Local/ms-playwright/
59
+ chromium-XXXX/chrome-win64/chrome.exe`), or how the port is selected
60
+ (chrome-launcher-managed vs explicit). Spawning `chrome.exe` directly via
61
+ `Start-Process` and probing `/json/version` over HTTP works fine — only
62
+ chrome-launcher's launch path fails. The same code works on Linux (CI)
63
+ and is widely used elsewhere, so this is a Windows-host quirk rather
64
+ than a cdp-mcp issue. **Workaround**: run L3 changes under WSL2
65
+ (Ubuntu) or push and let CI validate (but see WSL2 caveat below). Unit
66
+ + L2 tests work fine on native Windows.
67
+
68
+ *Originally hit on agents/l3-impl during PR #11 implementation.
69
+ Cross-confirmed by Codex reviewer who diagnosed the on-CI failure
70
+ separately — turned out to be a different root cause (Codex blocker on
71
+ --remote-debugging-port=0 in chromeFlags overriding chrome-launcher's
72
+ own port). After that fix, CI on Linux is the live validation; Win11
73
+ local-host status remains as documented here.*
74
+
75
+ - **macOS arm64 + system unbranded Chromium (brew cask).** The `chromium`
76
+ Homebrew cask is **deprecated** ("does not pass the macOS Gatekeeper
77
+ check; will be disabled 2026-09-01"). Install completes and the wrapper
78
+ script lands at `/opt/homebrew/bin/chromium`, but on first launch
79
+ Gatekeeper rejects the `.app` as "damaged" and the binary is unusable
80
+ for unattended e2e/eval runs. Workaround for darwin-arm64: use Playwright
81
+ Chrome-for-Testing (`npx playwright install chromium`) — `resolveBrowser`
82
+ picks it up automatically from `~/Library/Caches/ms-playwright/chromium-
83
+ <rev>/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/
84
+ Google Chrome for Testing` (CfT layout added to `pickPlaywrightExe` in
85
+ the same PR that landed this entry). The resolver also explicitly skips
86
+ the brew-cask wrapper on darwin (`isBrewCaskChromium`) so users who
87
+ tried the deprecated cask first — and then turn to Playwright per this
88
+ entry — fall through to the Playwright cache instead of getting the
89
+ Gatekeeper-rejected wrapper back from Step 2. Functionally Chromium-
90
+ channel at a fixed protocol revision, but Google-branded — call it out
91
+ if your eval needs *unbranded* Chromium. Building Chromium from source
92
+ on macOS is multi-hour + multi-GB ongoing maintenance; not a viable
93
+ automation path.
94
+
95
+ *Originally hit while validating set_breakpoint idempotency on a
96
+ macOS arm64 host.*
97
+
98
+ - **WSL2 (Ubuntu) + snap-installed chromium.** Default-template Ubuntu on
99
+ WSL2 ships chromium as a snap (`/snap/bin/chromium`), which runs in a
100
+ confined namespace. chrome-launcher launches the binary successfully
101
+ (visible window appears under WSLg) but its startup-port poll
102
+ `ECONNREFUSEs` on iter 1 and often iter 2 — the debug port either
103
+ binds to a different port than chrome-launcher polled (race) or is
104
+ invisible across the snap sandbox boundary. After 2-4 retries the
105
+ agent's tool-use loop eventually picks an instance that responds, so
106
+ the eval does run, but every trial pays an inflated cost in retry
107
+ iterations and the trace is contaminated with WARN entries that look
108
+ like real failures. Same chrome-launcher code path is clean on macOS,
109
+ Linux native, and Linux CI.
110
+
111
+ **Workaround options**: (a) **prefer macOS or Linux native** for
112
+ interactive eval iteration — a macOS arm64 host confirmed clean;
113
+ (b) install non-snap chromium in WSL2 via apt or
114
+ symlink Playwright's bundled chromium to `/usr/local/bin/chromium-browser`
115
+ before the snap path; (c) accept the noise and rely on CI for the
116
+ authoritative signal. Don't rely on WSL2 for eval validation runs.
117
+
118
+ *A re-run on the same commit (`f0ce92a`) on a native host showed 0
119
+ chrome-launcher errors, isolating the cause to the WSL2 + snap-chromium
120
+ combination rather than the harness.*
121
+
122
+ - **Ubuntu 23.10+ (incl. 24.04) + Playwright-bundled Chromium.** Recent
123
+ Ubuntu kernels restrict unprivileged user namespaces via AppArmor, and
124
+ Playwright-bundled Chromium ships without a SUID `chrome_sandbox`
125
+ helper. Without `--no-sandbox`, Chromium FATALs at startup
126
+ (`zygote_host_impl_linux.cc: No usable sandbox!`) before opening its
127
+ debug port — chrome-launcher's port-poll loop then times out with
128
+ ECONNREFUSED, looking exactly like the WSL2/snap gap above but with a
129
+ different root cause. **Mitigation:** `launchChrome` defaults
130
+ `sandbox: false` so `--no-sandbox` is added automatically; eval
131
+ pipelines on this host work out-of-the-box. For the full security model,
132
+ including `sandbox: true`, AppArmor, snap confinement, and Bubblewrap, see
133
+ [docs/chromium-sandboxing.md](./chromium-sandboxing.md).
134
+
135
+ *First observed on an Ubuntu 24.04 arm64 host (Parallels VM) while
136
+ validating the L4 eval suite. Quick eval went from FAIL/$0.34/445s
137
+ (chrome-launcher retry storm) to PASS/$0.31/107s once `--no-sandbox`
138
+ was the default.*
@@ -0,0 +1,217 @@
1
+ # macOS: Run as a Persistent Service (launchd)
2
+
3
+ Register `cdp-mcp` as a launchd user agent so it starts automatically on login
4
+ and exposes the MCP SSE endpoint on `127.0.0.1:9719`.
5
+
6
+ Persistent service mode is useful for MCP clients that support SSE because the
7
+ `cdp-mcp` process and its browser/CDP session can survive MCP client restarts or
8
+ reconnects. It does **not** persist state across service-process restarts.
9
+
10
+ > Security note: the local SSE endpoint has no authentication. MCP tools include
11
+ > in-page JavaScript evaluation and filesystem writes via screenshot paths. Only
12
+ > run a persistent service on trusted single-user machines, and do not bind it to
13
+ > non-loopback interfaces unless you understand the `--allow-remote` exposure.
14
+
15
+ ## Contents
16
+
17
+ - [1. Install the server](#1-install-the-server)
18
+ - [2. Create the plist](#2-create-the-plist)
19
+ - [3. Load and start](#3-load-and-start)
20
+ - [4. Verify](#4-verify)
21
+ - [5. Configure an MCP client](#5-configure-an-mcp-client)
22
+ - [6. Logs](#6-logs)
23
+ - [7. Stop / uninstall](#7-stop--uninstall)
24
+ - [8. Upgrade](#8-upgrade)
25
+ - [Troubleshooting](#troubleshooting)
26
+
27
+ ## 1. Install the server
28
+
29
+ Requires Node.js 20+ and a local Chrome/Chromium browser.
30
+
31
+ ```bash
32
+ npm install -g cdp-mcp
33
+ ```
34
+
35
+ Verify with `cdp-mcp --help`. The package ships prebuilt `dist/`, so there is no
36
+ build step and no repo checkout needed.
37
+
38
+ If `launch_chrome` cannot find Chrome/Chromium automatically, set `CHROME_PATH`
39
+ in the plist generated below.
40
+
41
+ ## 2. Create the plist
42
+
43
+ Run this from any directory:
44
+
45
+ ```bash
46
+ # If you use fnm, nvm, or another Node version manager, set these variables to
47
+ # stable paths before running this snippet. Example:
48
+ # NODE_BIN="$HOME/.local/share/fnm/aliases/default/bin/node"
49
+ # CDP_SCRIPT="$HOME/.local/share/fnm/aliases/default/bin/cdp-mcp"
50
+ NODE_BIN="${NODE_BIN:-$(command -v node)}"
51
+ CDP_SCRIPT="${CDP_SCRIPT:-$(command -v cdp-mcp)}"
52
+ CHROME_PATH="${CHROME_PATH:-}"
53
+
54
+ if [ -z "$NODE_BIN" ]; then
55
+ echo "Error: node not found in PATH. Install Node 20+ first." >&2
56
+ exit 1
57
+ fi
58
+ if [ -z "$CDP_SCRIPT" ]; then
59
+ echo "Error: cdp-mcp not found. Run 'npm install -g cdp-mcp' first." >&2
60
+ exit 1
61
+ fi
62
+
63
+ xml_escape() {
64
+ printf '%s' "$1" \
65
+ | sed \
66
+ -e 's/&/\&amp;/g' \
67
+ -e 's/</\&lt;/g' \
68
+ -e 's/>/\&gt;/g' \
69
+ -e 's/"/\&quot;/g' \
70
+ -e "s/'/\&apos;/g"
71
+ }
72
+
73
+ NODE_DIR="$(dirname "$NODE_BIN")"
74
+ mkdir -p ~/Library/LaunchAgents ~/Library/Logs/cdp-mcp
75
+ ESC_NODE=$(xml_escape "$NODE_BIN")
76
+ ESC_NODE_DIR=$(xml_escape "$NODE_DIR")
77
+ ESC_CDP=$(xml_escape "$CDP_SCRIPT")
78
+ ESC_HOME=$(xml_escape "$HOME")
79
+ ESC_CHROME=$(xml_escape "$CHROME_PATH")
80
+ cat > ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist <<PLIST
81
+ <?xml version="1.0" encoding="UTF-8"?>
82
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
83
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
84
+ <plist version="1.0">
85
+ <dict>
86
+ <key>Label</key>
87
+ <string>io.github.lcjanke2020.cdp-mcp</string>
88
+ <key>ProgramArguments</key>
89
+ <array>
90
+ <string>$ESC_NODE</string>
91
+ <string>$ESC_CDP</string>
92
+ <string>--port</string>
93
+ <string>9719</string>
94
+ </array>
95
+ <key>RunAtLoad</key>
96
+ <true/>
97
+ <key>KeepAlive</key>
98
+ <true/>
99
+ <key>StandardOutPath</key>
100
+ <string>$ESC_HOME/Library/Logs/cdp-mcp/server.stdout.log</string>
101
+ <key>StandardErrorPath</key>
102
+ <string>$ESC_HOME/Library/Logs/cdp-mcp/server.stderr.log</string>
103
+ <key>EnvironmentVariables</key>
104
+ <dict>
105
+ <key>PATH</key>
106
+ <string>$ESC_NODE_DIR:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
107
+ $(if [ -n "$CHROME_PATH" ]; then printf ' <key>CHROME_PATH</key>\n <string>%s</string>\n' "$ESC_CHROME"; fi)
108
+ </dict>
109
+ </dict>
110
+ </plist>
111
+ PLIST
112
+ ```
113
+
114
+ The plist invokes `node` directly with the `cdp-mcp` script path. That makes the
115
+ `NODE_BIN` override authoritative even when your shell uses a Node version
116
+ manager.
117
+
118
+ ## 3. Load and start
119
+
120
+ On macOS 10.15+, use:
121
+
122
+ ```bash
123
+ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
124
+ launchctl kickstart -k gui/$UID/io.github.lcjanke2020.cdp-mcp
125
+ ```
126
+
127
+ Older macOS releases also support:
128
+
129
+ ```bash
130
+ launchctl load ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
131
+ ```
132
+
133
+ ## 4. Verify
134
+
135
+ ```bash
136
+ launchctl print gui/$UID/io.github.lcjanke2020.cdp-mcp
137
+ lsof -i :9719
138
+ curl -v --max-time 2 http://127.0.0.1:9719/sse 2>&1 | head -20
139
+ ```
140
+
141
+ The `curl` command should show a `200 OK` response and SSE event output. A
142
+ timeout after the first event is expected because `/sse` keeps the connection
143
+ open. The server also sends periodic SSE keepalive comments by default; tune
144
+ with `CDP_MCP_SSE_KEEPALIVE_MS` only if your MCP client needs a different idle
145
+ interval.
146
+
147
+ ## 5. Configure an MCP client
148
+
149
+ Point an SSE-capable MCP client at:
150
+
151
+ ```text
152
+ http://127.0.0.1:9719/sse
153
+ ```
154
+
155
+ For example, clients that use JSON MCP server config commonly use:
156
+
157
+ ```json
158
+ {
159
+ "mcpServers": {
160
+ "cdp-mcp": {
161
+ "type": "sse",
162
+ "url": "http://127.0.0.1:9719/sse"
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ SSE mode is single-client today. Multiple MCP clients connected to the same
169
+ service share one process-global browser/CDP session and can interfere with each
170
+ other. Use one active debugging client per service, or run separate services on
171
+ separate ports.
172
+
173
+ A reconnecting client resumes the prior session. If you want a clean browser
174
+ session after reconnecting, call `close_session` before launching or attaching
175
+ again.
176
+
177
+ ## 6. Logs
178
+
179
+ ```bash
180
+ tail -f ~/Library/Logs/cdp-mcp/server.stderr.log
181
+ ```
182
+
183
+ ## 7. Stop / uninstall
184
+
185
+ ```bash
186
+ launchctl bootout gui/$UID/io.github.lcjanke2020.cdp-mcp
187
+ rm ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
188
+ ```
189
+
190
+ Older macOS releases also support:
191
+
192
+ ```bash
193
+ launchctl unload ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
194
+ ```
195
+
196
+ ## 8. Upgrade
197
+
198
+ ```bash
199
+ npm install -g cdp-mcp@latest
200
+ launchctl kickstart -k gui/$UID/io.github.lcjanke2020.cdp-mcp
201
+ ```
202
+
203
+ Restart or reconnect your MCP client after a server upgrade so it reloads tool
204
+ schemas.
205
+
206
+ ## Troubleshooting
207
+
208
+ | Symptom | Fix |
209
+ |---|---|
210
+ | `bootstrap` says service already loaded | Run `launchctl bootout gui/$UID/io.github.lcjanke2020.cdp-mcp`, then bootstrap again |
211
+ | Service exits immediately | Check `~/Library/Logs/cdp-mcp/server.stderr.log`; usually `cdp-mcp` is not installed, Node is too old, or a version-manager path moved |
212
+ | Port 9719 is already in use | Check `lsof -i :9719`, then stop the other process or change the port in the plist |
213
+ | MCP client rejects the config | Confirm the client supports SSE MCP servers and include both `"type": "sse"` and the `/sse` URL if your client uses JSON config |
214
+ | `launch_chrome` cannot find Chrome | Set `CHROME_PATH` before generating the plist, or edit the plist environment and reload the service |
215
+ | Service not starting after reboot | Verify the plist is in `~/Library/LaunchAgents/`, not `LaunchDaemons` |
216
+ | Service not starting after reboot with fnm/nvm | Version-manager shell paths can be ephemeral. Recreate the plist with stable `NODE_BIN` and `CDP_SCRIPT` paths, or install with a system Node |
217
+ | `already_session` after reconnecting | The prior browser/CDP session is still alive. Resume it, or call `close_session` before starting fresh |
@@ -0,0 +1,199 @@
1
+ # Local L3 e2e setup (Playwright Chromium + AppArmor)
2
+
3
+ **Last updated: 2026-06-09**
4
+
5
+ A step-by-step runbook for getting `npm run test:e2e` (the L3 real-browser
6
+ suite) passing on a local Linux machine **with Chromium's sandbox on**. This is
7
+ the practical companion to [`chromium-sandboxing.md`](./chromium-sandboxing.md);
8
+ read that for the full `--no-sandbox` / `sandbox: true` threat model and the
9
+ validated-hosts table.
10
+
11
+ Assumes Ubuntu (23.10+/24.04). Other distributions may need no host-side work at
12
+ all — see "Other distributions" below.
13
+
14
+ ## Why this is needed on Ubuntu
15
+
16
+ The L3 e2e harness launches Chromium with the sandbox **on** when running
17
+ locally; it only adds `--no-sandbox` when the `CI` env var is set
18
+ (`test/e2e/setup/global.ts`). That is deliberate — locally we want the sandbox
19
+ when the host can provide it.
20
+
21
+ But recent Ubuntu releases ship:
22
+
23
+ ```sh
24
+ kernel.apparmor_restrict_unprivileged_userns = 1
25
+ ```
26
+
27
+ which blocks the unprivileged **user namespace** that Chromium's sandbox needs.
28
+ Playwright-bundled Chromium does not ship a SUID `chrome_sandbox` helper, so on a
29
+ stock Ubuntu host a sandbox-on launch fails before the DevTools port opens:
30
+
31
+ ```text
32
+ zygote_host_impl_linux.cc: No usable sandbox!
33
+ ```
34
+
35
+ From `chrome-launcher` this usually surfaces as a startup port-poll timeout or
36
+ `ECONNREFUSED`.
37
+
38
+ The fix is an AppArmor profile that grants `userns,` to the Playwright Chromium
39
+ binary, giving it a stable named label that is allowed to create the user
40
+ namespace. The steps below install Chromium, confirm the resolver finds it,
41
+ attach the profile, and run the suite.
42
+
43
+ ## 1. Install Playwright Chromium
44
+
45
+ From the repo:
46
+
47
+ ```sh
48
+ npx --yes playwright install chromium
49
+ ```
50
+
51
+ This drops a managed Chromium into the per-user cache:
52
+
53
+ ```text
54
+ ~/.cache/ms-playwright/chromium-<rev>/chrome-linux*/chrome
55
+ ```
56
+
57
+ The leaf directory varies by Playwright version and arch — `chrome-linux` on
58
+ ARM64 and older builds, `chrome-linux64` on x86_64 with the newer
59
+ Chrome-for-Testing layout (the resolver and the AppArmor glob below cover both).
60
+ Install it for **each OS user** that will run the suite — the cache is
61
+ per-`$HOME`.
62
+
63
+ ## 2. Verify the resolver finds it
64
+
65
+ The launcher resolver (`src/util/browser-resolve.ts`) finds Chromium in this
66
+ order: an explicit `CDP_TEST_BROWSER_PATH`, then a system `chromium` on `PATH`,
67
+ then the Playwright cache. After a build, confirm what it picks:
68
+
69
+ ```sh
70
+ npm run build
71
+ node --input-type=module \
72
+ -e "import('./dist/util/browser-resolve.js').then(m => console.log(JSON.stringify(m.resolveBrowser(), null, 2)))"
73
+ ```
74
+
75
+ This runbook targets the **Playwright-cache** binary, so expect
76
+ `source: "playwright-cache"` and a `binaryPath` under `~/.cache/ms-playwright/`:
77
+
78
+ ```json
79
+ {
80
+ "binaryPath": "/home/<user>/.cache/ms-playwright/chromium-<rev>/chrome-linux/chrome",
81
+ "choice": "chromium",
82
+ "snapConfined": false,
83
+ "source": "playwright-cache"
84
+ }
85
+ ```
86
+
87
+ If you have a system Chromium on `PATH` (apt `/usr/bin/chromium`, snap
88
+ `/snap/bin/chromium`), the resolver returns that first with
89
+ `source: "which-chromium"` — that binary is *not* covered by the AppArmor
90
+ profile below (snap brings its own confinement; apt Chromium is a separate
91
+ sandbox story). To exercise the Playwright binary under this profile, point the
92
+ suite at it explicitly:
93
+
94
+ ```sh
95
+ export CDP_TEST_BROWSER_PATH="$(ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome | head -1)"
96
+ ```
97
+
98
+ ## 3. Attach the AppArmor profile
99
+
100
+ First confirm the kernel knob is the restrictive default:
101
+
102
+ ```sh
103
+ sysctl kernel.apparmor_restrict_unprivileged_userns # = 1 on stock Ubuntu 24.04
104
+ ```
105
+
106
+ If it is `0` (some hosts turn it off system-wide), Chromium's sandbox already
107
+ works and you can skip to step 5.
108
+
109
+ Create a profile that grants `userns,` to the Playwright Chromium binary path.
110
+ This mirrors the shape of Ubuntu's stock `chrome` / `msedge` / `brave` profiles
111
+ (a named-unconfined profile that opts into user namespaces):
112
+
113
+ ```apparmor
114
+ # /etc/apparmor.d/cdp-mcp-chromium
115
+ abi <abi/4.0>,
116
+ include <tunables/global>
117
+
118
+ profile cdp-mcp-chromium /home/*/.cache/ms-playwright/chromium-*/chrome-linux*/chrome flags=(unconfined) {
119
+ userns,
120
+
121
+ include if exists <local/cdp-mcp-chromium>
122
+ }
123
+ ```
124
+
125
+ The `chrome-linux*` component matches both the `chrome-linux` (ARM64/older) and
126
+ `chrome-linux64` (x86_64 Chrome-for-Testing) layouts — in AppArmor `*` matches
127
+ within a single path segment, so a too-specific `chrome-linux` would silently
128
+ fail to attach on x86_64. The `/home/*/` glob matches any user's Playwright
129
+ cache; if you prefer to scope it to specific accounts, replace `*` with a brace
130
+ list of usernames, e.g. `/home/{alice,bob}/.cache/...`.
131
+
132
+ Load it (profiles in `/etc/apparmor.d/` also auto-load at boot):
133
+
134
+ ```sh
135
+ sudo apparmor_parser -r /etc/apparmor.d/cdp-mcp-chromium
136
+ ```
137
+
138
+ ## 4. Verify the label attaches
139
+
140
+ Launch the bundled Chromium sandbox-on and read its AppArmor label — it must be
141
+ the named profile, not bare `unconfined`:
142
+
143
+ ```sh
144
+ BIN=$(ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome | head -1)
145
+ "$BIN" --headless=new --no-startup-window --remote-debugging-port=0 \
146
+ --user-data-dir=$(mktemp -d) about:blank & pid=$!
147
+ sleep 3; cat /proc/$pid/attr/current # -> cdp-mcp-chromium (unconfined)
148
+ kill $pid
149
+ ```
150
+
151
+ If this prints `cdp-mcp-chromium (unconfined)`, the profile is attached. A bare
152
+ `unconfined` means the binary path didn't match the profile's glob — re-check
153
+ the cache path against the profile.
154
+
155
+ ## 5. Run L3
156
+
157
+ ```sh
158
+ npm run test:e2e
159
+ ```
160
+
161
+ With the sandbox on and the profile attached, the suite should pass, e.g.:
162
+
163
+ ```text
164
+ Test Files 10 passed (10)
165
+ Tests 29 passed (29)
166
+ ```
167
+
168
+ ## Fallback (before AppArmor is configured)
169
+
170
+ The L3 harness adds `--no-sandbox` when `CI` is set, so you can run the suite
171
+ without the profile as a lower-security stopgap:
172
+
173
+ ```sh
174
+ env CI=1 npm run test:e2e
175
+ ```
176
+
177
+ This keeps work moving, but the AppArmor profile is the desired long-term
178
+ posture so that plain `npm run test:e2e` exercises sandbox-on Chromium. See
179
+ [`chromium-sandboxing.md`](./chromium-sandboxing.md) for why `--no-sandbox`
180
+ widens the blast radius of a compromised renderer.
181
+
182
+ ## Other distributions
183
+
184
+ This profile work is Ubuntu-specific. Distributions that don't restrict
185
+ unprivileged user namespaces by default (or use SELinux instead of AppArmor,
186
+ e.g. Fedora) generally run sandbox-on Chromium without a host-side profile.
187
+ Validate the actual host before assuming the Ubuntu steps are required — check
188
+ `sysctl kernel.apparmor_restrict_unprivileged_userns` (absent or `0` means no
189
+ AppArmor userns restriction to work around).
190
+
191
+ ## Related
192
+
193
+ - [`docs/chromium-sandboxing.md`](./chromium-sandboxing.md) — the canonical
194
+ `--no-sandbox` / `sandbox: true` threat model, the AppArmor / userns / snap /
195
+ Bubblewrap mechanism map, and the validated-hosts table.
196
+ - [`docs/known-chromium-gaps.md`](./known-chromium-gaps.md) — per-spec
197
+ Chromium-vs-Chrome gaps and host-OS workarounds.
198
+ - [README §L3](../README.md) — browser selection, `CDP_TEST_BROWSER_PATH`, and
199
+ the per-platform support matrix.