cdp-mcp 0.1.3 → 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.
- package/README.md +67 -31
- package/dist/contract.d.ts +11 -0
- package/dist/contract.js +11 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/locator.d.ts +108 -0
- package/dist/locator.js +176 -0
- package/dist/locator.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/dist/session/browser.d.ts +29 -0
- package/dist/session/browser.js +17 -2
- package/dist/session/browser.js.map +1 -1
- package/dist/session/buffers.d.ts +48 -0
- package/dist/session/pause.d.ts +21 -0
- package/dist/session/state.d.ts +53 -0
- package/dist/sourcemap/loader.d.ts +4 -0
- package/dist/sourcemap/normalize.d.ts +2 -0
- package/dist/sourcemap/store.d.ts +57 -0
- package/dist/tools/_locator_runtime.d.ts +31 -0
- package/dist/tools/_locator_runtime.js +243 -0
- package/dist/tools/_locator_runtime.js.map +1 -0
- package/dist/tools/_register.d.ts +2 -0
- package/dist/tools/breakpoints.d.ts +4 -0
- package/dist/tools/console.d.ts +2 -0
- package/dist/tools/dom.d.ts +2 -0
- package/dist/tools/dom.js +3 -221
- package/dist/tools/dom.js.map +1 -1
- package/dist/tools/execution.d.ts +29 -0
- package/dist/tools/forms.d.ts +8 -0
- package/dist/tools/forms.js +256 -0
- package/dist/tools/forms.js.map +1 -0
- package/dist/tools/inspect.d.ts +2 -0
- package/dist/tools/nav.d.ts +2 -0
- package/dist/tools/network.d.ts +2 -0
- package/dist/tools/session.d.ts +2 -0
- package/dist/tools/session.js +1 -1
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/source.d.ts +2 -0
- package/dist/tools/storage.d.ts +2 -0
- package/dist/tools/storage.js +296 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/util/browser-resolve.d.ts +19 -0
- package/dist/util/errors.d.ts +7 -0
- package/dist/util/format.d.ts +20 -0
- package/dist/util/log.d.ts +6 -0
- package/docs/chromium-sandboxing.md +6 -0
- package/docs/local-l3-e2e-setup.md +199 -0
- package/package.json +13 -1
package/dist/tools/session.js
CHANGED
|
@@ -16,7 +16,7 @@ export function registerSessionTools(server) {
|
|
|
16
16
|
sandbox: z
|
|
17
17
|
.boolean()
|
|
18
18
|
.optional()
|
|
19
|
-
.describe("Enable Chromium's sandbox.
|
|
19
|
+
.describe("Enable Chromium's sandbox. When omitted, defaults from the CDP_SANDBOX env ('true' or '1' enable it; default false → we add --no-sandbox). On Ubuntu 23.10+ AppArmor restricts the unprivileged user namespace Chromium's sandbox depends on, so unsandboxed launch is the working default for automation. Pass true only on a host with a working sandbox path (AppArmor userns allowance or SUID chrome_sandbox helper)."),
|
|
20
20
|
}, async (input) => {
|
|
21
21
|
return await launchChrome({
|
|
22
22
|
url: input.url,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,GAAG,MAAM,yBAAyB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,UAAU,oBAAoB,CAAC,MAAiB;IACpD,gBAAgB,CACd,MAAM,EACN,eAAe,EACf,2FAA2F,EAC3F;QACE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;QACjF,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;QACzD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC5D,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QACnE,WAAW,EAAE,CAAC;aACX,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,+RAA+R,CAChS;QACH,OAAO,EAAE,CAAC;aACP,OAAO,EAAE;aACT,QAAQ,EAAE;aACV,QAAQ,CACP,
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile, writeFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { requireSession } from "../session/state.js";
|
|
5
|
+
import { ToolError } from "../util/errors.js";
|
|
6
|
+
import { registerJsonTool } from "./_register.js";
|
|
7
|
+
/**
|
|
8
|
+
* Session-portability tools (issue #12 items 4, 5).
|
|
9
|
+
*
|
|
10
|
+
* Cookies go through CDP `Network.*` (NOT `document.cookie`) so HttpOnly
|
|
11
|
+
* auth/session cookies — the whole reason to resume a session — round-trip.
|
|
12
|
+
* localStorage is read/written via `Runtime.evaluate` (the `Storage.*` domain
|
|
13
|
+
* is out of scope per AGENTS.md). The on-disk shape is the de-facto Playwright
|
|
14
|
+
* `storageState` JSON so a flow can be handed to/from mainstream tooling.
|
|
15
|
+
*
|
|
16
|
+
* v1 localStorage scope is the **current page origin** only: localStorage is
|
|
17
|
+
* origin-partitioned and CDP can only read/write it for a document that is
|
|
18
|
+
* actually on that origin. Cookies (the load-bearing part for auth resume) are
|
|
19
|
+
* captured/restored for all origins. Multi-origin localStorage restore would
|
|
20
|
+
* require navigating to each origin first; origins in the file that don't match
|
|
21
|
+
* the current page are reported in `origins_skipped` rather than silently lost.
|
|
22
|
+
*
|
|
23
|
+
* File read/write mirrors `screenshot path=`: no extra path filter — the
|
|
24
|
+
* operator gate is the same `--allow-remote`/non-loopback rule (README §SSE).
|
|
25
|
+
*/
|
|
26
|
+
const SENSITIVE_NAME = /(sess|sid|token|auth|csrf|xsrf|jwt|secret|api[-_]?key)/i;
|
|
27
|
+
const sameSiteSchema = z.enum(["Strict", "Lax", "None"]);
|
|
28
|
+
const storageStateSchema = z.object({
|
|
29
|
+
cookies: z
|
|
30
|
+
.array(z.object({
|
|
31
|
+
name: z.string(),
|
|
32
|
+
value: z.string(),
|
|
33
|
+
domain: z.string(),
|
|
34
|
+
path: z.string(),
|
|
35
|
+
expires: z.number().optional(),
|
|
36
|
+
httpOnly: z.boolean().optional(),
|
|
37
|
+
secure: z.boolean().optional(),
|
|
38
|
+
sameSite: sameSiteSchema.optional(),
|
|
39
|
+
}))
|
|
40
|
+
.default([]),
|
|
41
|
+
origins: z
|
|
42
|
+
.array(z.object({
|
|
43
|
+
origin: z.string(),
|
|
44
|
+
localStorage: z.array(z.object({ name: z.string(), value: z.string() })).default([]),
|
|
45
|
+
}))
|
|
46
|
+
.default([]),
|
|
47
|
+
});
|
|
48
|
+
const cookieParamSchema = z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
value: z.string(),
|
|
51
|
+
url: z.string().optional().describe("Cookie URL (provide this OR domain)."),
|
|
52
|
+
domain: z.string().optional(),
|
|
53
|
+
path: z.string().optional(),
|
|
54
|
+
secure: z.boolean().optional(),
|
|
55
|
+
httpOnly: z.boolean().optional(),
|
|
56
|
+
sameSite: sameSiteSchema.optional(),
|
|
57
|
+
expires: z.number().optional().describe("Expiry as seconds since epoch; omit for a session cookie."),
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Write `data` to `dest` at mode 0o600, overwrite-safe and TOCTOU-safe.
|
|
61
|
+
*
|
|
62
|
+
* The storage-state file holds plaintext cookie secrets, so the 0o600
|
|
63
|
+
* postcondition must hold even on a shared, world-writable directory. Two traps:
|
|
64
|
+
* - Node's writeFile `mode` only applies when it *creates* the file, so writing
|
|
65
|
+
* straight to `dest` would leave an existing 0644 file world-readable.
|
|
66
|
+
* - A *predictable* temp path lets an attacker pre-create (or symlink) it at
|
|
67
|
+
* 0644, so the secret gets written through their file and renamed into place.
|
|
68
|
+
*
|
|
69
|
+
* So: create a temp with an **unpredictable** same-directory suffix using
|
|
70
|
+
* exclusive create (`flag: "wx"` ⇒ O_CREAT|O_EXCL — fails on any pre-existing
|
|
71
|
+
* file and refuses to follow a symlink), which makes 0o600 a real creation-time
|
|
72
|
+
* guarantee, then atomically rename it over the destination (same filesystem, so
|
|
73
|
+
* the secret never exists at the destination path in a half-written/0644 state).
|
|
74
|
+
* Retry on the astronomically-unlikely name collision.
|
|
75
|
+
*/
|
|
76
|
+
async function writeSecretFileAtomic(dest, data) {
|
|
77
|
+
let lastErr;
|
|
78
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
79
|
+
const tmp = `${dest}.${randomBytes(8).toString("hex")}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
await writeFile(tmp, data, { flag: "wx", mode: 0o600 });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (e.code === "EEXIST") {
|
|
85
|
+
lastErr = e;
|
|
86
|
+
continue; // collision (or planted file/symlink) — try a fresh random name
|
|
87
|
+
}
|
|
88
|
+
throw e; // ENOENT (missing parent dir), EACCES, etc.
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await rename(tmp, dest);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
await rm(tmp, { force: true }).catch(() => { });
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw lastErr;
|
|
100
|
+
}
|
|
101
|
+
export function registerStorageTools(server) {
|
|
102
|
+
registerJsonTool(server, "export_storage_state", "Save the browser session (all cookies including HttpOnly, plus the current page origin's localStorage) to a JSON file in the de-facto Playwright storageState shape, so a flow can be resumed in a fresh session or handed to other tooling. The file preserves full cookie values (it exists to resume auth) — treat it as a secret. File write is gated by the same operator rule as screenshot path= / --allow-remote.", { path: z.string().describe("Absolute path to write the storageState JSON to.") }, async (input) => {
|
|
103
|
+
const s = requireSession();
|
|
104
|
+
const { cookies } = (await s.client.send("Network.getAllCookies"));
|
|
105
|
+
const probe = await s.client.send("Runtime.evaluate", {
|
|
106
|
+
expression: READ_ORIGIN_AND_LOCALSTORAGE,
|
|
107
|
+
returnByValue: true,
|
|
108
|
+
});
|
|
109
|
+
const probed = (probe.result?.value ?? { origin: "null", localStorage: [] });
|
|
110
|
+
const origins = probed.origin && probed.origin !== "null"
|
|
111
|
+
? [{ origin: probed.origin, localStorage: probed.localStorage }]
|
|
112
|
+
: [];
|
|
113
|
+
const state = { cookies: cookies.map(cdpCookieToState), origins };
|
|
114
|
+
// The file holds full cookie values (incl. HttpOnly auth/session tokens) —
|
|
115
|
+
// the description says "treat it as a secret". writeSecretFileAtomic writes
|
|
116
|
+
// it 0o600 and overwrite-safe (see the helper for the threat model).
|
|
117
|
+
try {
|
|
118
|
+
await writeSecretFileAtomic(input.path, JSON.stringify(state, null, 2));
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (e.code === "ENOENT") {
|
|
122
|
+
throw new ToolError("not_found", `could not write storage-state file (does the parent directory exist?): ${e.message}`);
|
|
123
|
+
}
|
|
124
|
+
throw e; // EACCES/EISDIR/etc. surface as internal_error via registerJsonTool
|
|
125
|
+
}
|
|
126
|
+
return { saved: input.path, cookies: state.cookies.length, origins: origins.length };
|
|
127
|
+
});
|
|
128
|
+
registerJsonTool(server, "load_storage_state", "Restore a session from a Playwright-shaped storageState JSON file: sets all cookies (no navigation needed), then restores localStorage for the entries whose origin matches the current page. Cookies are added on top of the existing jar (additive, not a clean-context replace), so prefer a fresh session for Playwright-equivalent semantics. Origins that don't match the current page are returned in origins_skipped (restoring them would require navigating there first). File read is gated by the same operator rule as screenshot path= / --allow-remote.", { path: z.string().describe("Absolute path to a storageState JSON file (as written by export_storage_state).") }, async (input) => {
|
|
129
|
+
const s = requireSession();
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = await readFile(input.path, "utf8");
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
if (e.code === "ENOENT") {
|
|
136
|
+
throw new ToolError("not_found", `could not read storage-state file: ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
throw e; // EACCES/EISDIR/etc. surface as internal_error rather than masquerading as not_found
|
|
139
|
+
}
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new ToolError("invalid_arg", "storage-state file is not valid JSON");
|
|
146
|
+
}
|
|
147
|
+
const result = storageStateSchema.safeParse(parsed);
|
|
148
|
+
if (!result.success) {
|
|
149
|
+
throw new ToolError("invalid_arg", `storage-state file is not a valid storageState: ${result.error.message}`);
|
|
150
|
+
}
|
|
151
|
+
const state = result.data;
|
|
152
|
+
if (state.cookies.length > 0) {
|
|
153
|
+
await s.client.send("Network.setCookies", { cookies: state.cookies.map(stateCookieToCdp) });
|
|
154
|
+
}
|
|
155
|
+
const restore = await s.client.send("Runtime.evaluate", {
|
|
156
|
+
expression: restoreLocalStorageExpr(state.origins),
|
|
157
|
+
returnByValue: true,
|
|
158
|
+
});
|
|
159
|
+
const restored = (restore.result?.value ?? { restored: [], skipped: state.origins.map((o) => o.origin) });
|
|
160
|
+
return {
|
|
161
|
+
loaded: input.path,
|
|
162
|
+
cookies: state.cookies.length,
|
|
163
|
+
origins_restored: restored.restored,
|
|
164
|
+
origins_skipped: restored.skipped,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
registerJsonTool(server, "get_cookies", "List cookies (via CDP, so HttpOnly cookies are included) with their flags. For safe printing, the VALUE of likely session/auth cookies is redacted (httpOnly cookies, or names matching sess/sid/token/auth/csrf/jwt/secret/api-key); only value_length is shown for those. Full values are obtainable only through export_storage_state.", {
|
|
168
|
+
urls: z
|
|
169
|
+
.array(z.string())
|
|
170
|
+
.optional()
|
|
171
|
+
.describe("Restrict to cookies visible to these URLs; omit for all cookies in the browser."),
|
|
172
|
+
}, async (input) => {
|
|
173
|
+
const s = requireSession();
|
|
174
|
+
const res = (input.urls && input.urls.length > 0
|
|
175
|
+
? await s.client.send("Network.getCookies", { urls: input.urls })
|
|
176
|
+
: await s.client.send("Network.getAllCookies"));
|
|
177
|
+
return { cookies: res.cookies.map(redactForDisplay) };
|
|
178
|
+
});
|
|
179
|
+
registerJsonTool(server, "set_cookies", "Set one or more cookies in the browser cookie jar via CDP. Each cookie needs a url OR a domain. For restoring a saved session, prefer load_storage_state; this is the lower-level primitive.", { cookies: z.array(cookieParamSchema).describe("Cookies to set.") }, async (input) => {
|
|
180
|
+
const s = requireSession();
|
|
181
|
+
if (input.cookies.length === 0)
|
|
182
|
+
throw new ToolError("missing_arg", "cookies array is empty");
|
|
183
|
+
const missing = input.cookies.find((c) => !c.url && !c.domain);
|
|
184
|
+
if (missing)
|
|
185
|
+
throw new ToolError("missing_arg", `cookie '${missing.name}' needs a url or domain`);
|
|
186
|
+
// Normalize the session-cookie sentinel the same way load_storage_state
|
|
187
|
+
// does, so an exported `expires: -1` piped in here isn't sent as a 1969 expiry.
|
|
188
|
+
await s.client.send("Network.setCookies", {
|
|
189
|
+
cookies: input.cookies.map(withSessionExpiryOmitted),
|
|
190
|
+
});
|
|
191
|
+
return { set: input.cookies.length };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function cdpCookieToState(c) {
|
|
195
|
+
return {
|
|
196
|
+
name: c.name,
|
|
197
|
+
value: c.value,
|
|
198
|
+
domain: c.domain,
|
|
199
|
+
path: c.path,
|
|
200
|
+
// CDP uses -1 (or a missing field) for session cookies; Playwright too.
|
|
201
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
202
|
+
httpOnly: !!c.httpOnly,
|
|
203
|
+
secure: !!c.secure,
|
|
204
|
+
// Playwright requires sameSite; CDP may omit it. Lax is the browser default.
|
|
205
|
+
sameSite: c.sameSite ?? "Lax",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* CDP's `Network.setCookies` reads `expires` as an absolute epoch-seconds
|
|
210
|
+
* timestamp, so the session-cookie sentinel (`-1`, which `export_storage_state`
|
|
211
|
+
* writes) must be dropped rather than forwarded — otherwise the cookie is set
|
|
212
|
+
* already-expired (1969) and silently rejected. Shared by `load_storage_state`
|
|
213
|
+
* and `set_cookies` so both restore paths normalize identically.
|
|
214
|
+
*/
|
|
215
|
+
function withSessionExpiryOmitted(cookie) {
|
|
216
|
+
if (typeof cookie.expires === "number" && cookie.expires >= 0)
|
|
217
|
+
return cookie;
|
|
218
|
+
const copy = { ...cookie };
|
|
219
|
+
delete copy.expires;
|
|
220
|
+
return copy;
|
|
221
|
+
}
|
|
222
|
+
function stateCookieToCdp(c) {
|
|
223
|
+
const param = {
|
|
224
|
+
name: c.name,
|
|
225
|
+
value: c.value,
|
|
226
|
+
domain: c.domain,
|
|
227
|
+
path: c.path,
|
|
228
|
+
secure: !!c.secure,
|
|
229
|
+
httpOnly: !!c.httpOnly,
|
|
230
|
+
expires: c.expires,
|
|
231
|
+
};
|
|
232
|
+
if (c.sameSite)
|
|
233
|
+
param.sameSite = c.sameSite;
|
|
234
|
+
// -1 / undefined means a session cookie — omit expires entirely.
|
|
235
|
+
return withSessionExpiryOmitted(param);
|
|
236
|
+
}
|
|
237
|
+
function redactForDisplay(c) {
|
|
238
|
+
const redacted = !!c.httpOnly || SENSITIVE_NAME.test(c.name);
|
|
239
|
+
const out = {
|
|
240
|
+
name: c.name,
|
|
241
|
+
domain: c.domain,
|
|
242
|
+
path: c.path,
|
|
243
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
244
|
+
httpOnly: !!c.httpOnly,
|
|
245
|
+
secure: !!c.secure,
|
|
246
|
+
sameSite: c.sameSite ?? null,
|
|
247
|
+
redacted,
|
|
248
|
+
value_length: (c.value ?? "").length,
|
|
249
|
+
};
|
|
250
|
+
if (!redacted)
|
|
251
|
+
out.value = c.value;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// In-page expressions
|
|
256
|
+
/** Read the current origin and its localStorage as { origin, localStorage:[{name,value}] }. */
|
|
257
|
+
const READ_ORIGIN_AND_LOCALSTORAGE = String.raw `(() => {
|
|
258
|
+
const items = [];
|
|
259
|
+
try {
|
|
260
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
261
|
+
const k = localStorage.key(i);
|
|
262
|
+
if (k === null) continue;
|
|
263
|
+
const v = localStorage.getItem(k);
|
|
264
|
+
if (v === null) continue;
|
|
265
|
+
items.push({ name: k, value: v });
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {}
|
|
268
|
+
return { origin: location.origin, localStorage: items };
|
|
269
|
+
})()`;
|
|
270
|
+
/**
|
|
271
|
+
* For each origin in the file whose origin matches the current page, write its
|
|
272
|
+
* localStorage entries; report which origins were restored vs skipped.
|
|
273
|
+
*/
|
|
274
|
+
function restoreLocalStorageExpr(origins) {
|
|
275
|
+
return String.raw `(() => {
|
|
276
|
+
const origins = ${JSON.stringify(origins)};
|
|
277
|
+
const current = location.origin;
|
|
278
|
+
const restored = [];
|
|
279
|
+
const skipped = [];
|
|
280
|
+
for (const o of origins) {
|
|
281
|
+
if (o.origin !== current) {
|
|
282
|
+
skipped.push(o.origin);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// If any setItem throws (quota exceeded, storage disabled), report the
|
|
286
|
+
// origin as skipped rather than misleadingly claiming it was restored.
|
|
287
|
+
let ok = true;
|
|
288
|
+
for (const it of o.localStorage) {
|
|
289
|
+
try { localStorage.setItem(it.name, it.value); } catch (e) { ok = false; }
|
|
290
|
+
}
|
|
291
|
+
(ok ? restored : skipped).push(o.origin);
|
|
292
|
+
}
|
|
293
|
+
return { restored, skipped };
|
|
294
|
+
})()`;
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/tools/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type BrowserChoice = "chromium" | "chrome";
|
|
2
|
+
export interface ResolvedBrowser {
|
|
3
|
+
/** Absolute path to the Chrome/Chromium executable. */
|
|
4
|
+
binaryPath: string;
|
|
5
|
+
/** Which logical browser this binary represents. */
|
|
6
|
+
choice: BrowserChoice;
|
|
7
|
+
/** True when the binary is snap-confined (/snap/bin/* on Linux). Triggers
|
|
8
|
+
* the user-data-dir workaround in launch_chrome. */
|
|
9
|
+
snapConfined: boolean;
|
|
10
|
+
/** Diagnostic — which resolution step produced this. */
|
|
11
|
+
source: "CDP_TEST_BROWSER_PATH" | "which-chromium" | "playwright-cache" | "chrome-launcher-default";
|
|
12
|
+
}
|
|
13
|
+
export declare function getBrowserChoice(): BrowserChoice;
|
|
14
|
+
export declare function resolveBrowser(choice?: BrowserChoice): ResolvedBrowser;
|
|
15
|
+
/** True when the given binary is the snap-marker returned by step 4. */
|
|
16
|
+
export declare function isChromeLauncherDefault(b: ResolvedBrowser): boolean;
|
|
17
|
+
/** Determine the user-data-dir for snap-confined Chromium. Snap confinement
|
|
18
|
+
* rejects /tmp/... paths; only ~/snap/<app>/current/ is writable. */
|
|
19
|
+
export declare function snapUserDataDir(binaryPath: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class ToolError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const noSession: () => ToolError;
|
|
6
|
+
export declare const notPaused: () => ToolError;
|
|
7
|
+
export declare const alreadySession: () => ToolError;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Protocol } from "devtools-protocol";
|
|
2
|
+
export declare function truncate(s: string, max?: number): string;
|
|
3
|
+
export declare function previewRemoteObject(obj: Protocol.Runtime.RemoteObject): string;
|
|
4
|
+
export declare function describeRemote(obj: Protocol.Runtime.RemoteObject): {
|
|
5
|
+
type: string;
|
|
6
|
+
preview: string;
|
|
7
|
+
objectId?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function toolText(text: string): {
|
|
10
|
+
content: {
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
export declare function toolJson(value: unknown): {
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const log: {
|
|
2
|
+
debug: (msg: string, meta?: Record<string, unknown>) => void;
|
|
3
|
+
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
4
|
+
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
5
|
+
error: (msg: string, meta?: Record<string, unknown>) => void;
|
|
6
|
+
};
|
|
@@ -107,6 +107,12 @@ Untrusted browsing:
|
|
|
107
107
|
- Add host-level confinement such as AppArmor, a container, VM, or Bubblewrap.
|
|
108
108
|
- Do not treat `bwrap + --no-sandbox` as equivalent to Chromium sandboxing.
|
|
109
109
|
|
|
110
|
+
This outer containment also pairs with the agent-operator threat (prompt-injected
|
|
111
|
+
page content steering the agent into actions or unscoped filesystem writes); see
|
|
112
|
+
the agent-operator threat model and deployment hardening in
|
|
113
|
+
[SECURITY.md](../SECURITY.md). A containerized outer-sandbox run mode that would
|
|
114
|
+
make this contained posture the default is a direction under consideration.
|
|
115
|
+
|
|
110
116
|
## Validated hosts
|
|
111
117
|
|
|
112
118
|
Hosts where `sandbox: true` has been verified working against this project, with the supporting posture:
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Chrome DevTools Protocol MCP server — a TypeScript-aware frontend debugger for AI agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leonard Janke",
|
|
@@ -28,11 +28,23 @@
|
|
|
28
28
|
"cdp-mcp": "dist/index.js"
|
|
29
29
|
},
|
|
30
30
|
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./contract": {
|
|
38
|
+
"types": "./dist/contract.d.ts",
|
|
39
|
+
"import": "./dist/contract.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
31
42
|
"files": [
|
|
32
43
|
"dist",
|
|
33
44
|
"docs/launchd-service.md",
|
|
34
45
|
"docs/systemd-service.md",
|
|
35
46
|
"docs/chromium-sandboxing.md",
|
|
47
|
+
"docs/local-l3-e2e-setup.md",
|
|
36
48
|
"docs/known-chromium-gaps.md"
|
|
37
49
|
],
|
|
38
50
|
"scripts": {
|