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.
- package/README.md +100 -37
- 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 +197 -0
- package/docs/known-chromium-gaps.md +138 -0
- package/docs/launchd-service.md +217 -0
- package/docs/local-l3-e2e-setup.md +199 -0
- package/docs/systemd-service.md +233 -0
- package/package.json +18 -2
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
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Chromium sandboxing
|
|
2
|
+
|
|
3
|
+
**Last updated: 2026-05-16**
|
|
4
|
+
|
|
5
|
+
This project launches Chromium in two different contexts:
|
|
6
|
+
|
|
7
|
+
- L3 e2e tests and L4 evals launch a real browser through `launch_chrome`.
|
|
8
|
+
- Agents then control that browser through CDP, including `Runtime.evaluate`,
|
|
9
|
+
`Debugger.*`, DOM interaction, console inspection, and network inspection.
|
|
10
|
+
|
|
11
|
+
That makes sandboxing a host setup and threat-model decision, not just a
|
|
12
|
+
Chrome flag. This document is the canonical reference for `--no-sandbox`,
|
|
13
|
+
AppArmor, unprivileged user namespaces, snap confinement, and Bubblewrap.
|
|
14
|
+
|
|
15
|
+
## Current default
|
|
16
|
+
|
|
17
|
+
`launch_chrome` defaults to `sandbox: false`, which adds `--no-sandbox`.
|
|
18
|
+
|
|
19
|
+
The default exists because Ubuntu 23.10+ and 24.04 commonly restrict
|
|
20
|
+
unprivileged user namespaces through AppArmor. Playwright-bundled Chromium
|
|
21
|
+
does not ship with a SUID `chrome_sandbox` helper. On those hosts, launching
|
|
22
|
+
Chromium without `--no-sandbox` can fail before the DevTools port opens:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
zygote_host_impl_linux.cc: No usable sandbox!
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
From `chrome-launcher`, that often surfaces as a startup port poll timeout or
|
|
29
|
+
`ECONNREFUSED`.
|
|
30
|
+
|
|
31
|
+
Other Linux distributions may allow Chromium's unprivileged user namespace
|
|
32
|
+
sandbox path by default. Validate the actual host before assuming the Ubuntu
|
|
33
|
+
automation default is necessary there.
|
|
34
|
+
|
|
35
|
+
Use `sandbox: true` only when the host has a working Chromium sandbox path:
|
|
36
|
+
|
|
37
|
+
- An AppArmor policy that permits the specific Chromium binary to create the
|
|
38
|
+
unprivileged user namespace it needs.
|
|
39
|
+
- Or a working SUID `chrome_sandbox` helper installed alongside that binary.
|
|
40
|
+
|
|
41
|
+
## Why the Chromium sandbox still matters
|
|
42
|
+
|
|
43
|
+
The MCP caller is already highly privileged relative to the page. It can ask
|
|
44
|
+
the server to evaluate JavaScript, drive the DOM, set breakpoints, inspect
|
|
45
|
+
scopes, and read browser-observed network activity. For that caller, the
|
|
46
|
+
Chromium renderer sandbox is not the primary trust boundary.
|
|
47
|
+
|
|
48
|
+
The Chromium sandbox still matters for hostile page content and browser
|
|
49
|
+
exploitation risk. With the sandbox enabled, Chromium isolates renderer, GPU,
|
|
50
|
+
and utility child processes from the browser process using mechanisms such as
|
|
51
|
+
namespaces, seccomp filters, brokered filesystem access, and per-process
|
|
52
|
+
capability reduction. With `--no-sandbox`, a compromised renderer has a much
|
|
53
|
+
larger blast radius inside the browser process tree.
|
|
54
|
+
|
|
55
|
+
So the project default is a pragmatic automation default, not a claim that
|
|
56
|
+
`--no-sandbox` is equally safe.
|
|
57
|
+
|
|
58
|
+
## How the mechanisms relate
|
|
59
|
+
|
|
60
|
+
These mechanisms solve different problems:
|
|
61
|
+
|
|
62
|
+
- Chromium sandbox: Chromium's internal process sandbox. It is the boundary
|
|
63
|
+
between web content renderer processes and the rest of the browser.
|
|
64
|
+
- AppArmor: host-enforced mandatory access control. It can confine Chromium,
|
|
65
|
+
and on recent Ubuntu systems it can also restrict whether an unprivileged
|
|
66
|
+
process may create user namespaces.
|
|
67
|
+
- Snap confinement: the packaging sandbox used by snap-installed Chromium.
|
|
68
|
+
It can hide or remap filesystem locations and has caused DevTools port and
|
|
69
|
+
`userDataDir` friction in local runs.
|
|
70
|
+
- Bubblewrap (`bwrap`): a small Linux sandboxing tool that starts a process in
|
|
71
|
+
new namespaces with a controlled filesystem, process, and optional network
|
|
72
|
+
view.
|
|
73
|
+
|
|
74
|
+
Bubblewrap is useful defense-in-depth around a browser or eval job. For
|
|
75
|
+
example, it can run the whole MCP server plus browser process tree with only
|
|
76
|
+
the repository and selected temp/profile directories writable. That helps
|
|
77
|
+
prevent accidental or malicious access to unrelated files such as SSH keys,
|
|
78
|
+
cloud credentials, or the rest of the home directory.
|
|
79
|
+
|
|
80
|
+
Bubblewrap is not a clean substitute for Chromium's own sandbox. If Chromium
|
|
81
|
+
runs with `--no-sandbox` inside Bubblewrap, the entire browser process tree is
|
|
82
|
+
inside an outer container-like boundary, but Chromium's internal renderer
|
|
83
|
+
isolation is still disabled. A renderer compromise may be contained by the
|
|
84
|
+
outer Bubblewrap filesystem or network policy, but it is not contained in the
|
|
85
|
+
same way as Chromium's per-renderer sandbox.
|
|
86
|
+
|
|
87
|
+
## Recommended posture
|
|
88
|
+
|
|
89
|
+
Local development:
|
|
90
|
+
|
|
91
|
+
- Treat the current default, `sandbox: false`, as a host-capability fallback:
|
|
92
|
+
it keeps work moving when Chromium sandbox setup is the blocker.
|
|
93
|
+
- Prefer `sandbox: true` once the host has a known-good Chromium sandbox path.
|
|
94
|
+
|
|
95
|
+
CI and L4 eval hosts:
|
|
96
|
+
|
|
97
|
+
- Keep the default working and deterministic. If `--no-sandbox` is needed for
|
|
98
|
+
Playwright-bundled Chromium on Ubuntu, document that in the host setup.
|
|
99
|
+
- Consider running the whole job inside an outer sandbox, VM, container, or
|
|
100
|
+
Bubblewrap profile with a throwaway browser profile and limited writable
|
|
101
|
+
paths.
|
|
102
|
+
|
|
103
|
+
Untrusted browsing:
|
|
104
|
+
|
|
105
|
+
- Prefer Chromium's sandbox on.
|
|
106
|
+
- Use a throwaway `userDataDir`.
|
|
107
|
+
- Add host-level confinement such as AppArmor, a container, VM, or Bubblewrap.
|
|
108
|
+
- Do not treat `bwrap + --no-sandbox` as equivalent to Chromium sandboxing.
|
|
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
|
+
|
|
116
|
+
## Validated hosts
|
|
117
|
+
|
|
118
|
+
Hosts where `sandbox: true` has been verified working against this project, with the supporting posture:
|
|
119
|
+
|
|
120
|
+
| Host | OS | Arch | `sandbox: true` | AppArmor profile | Notes |
|
|
121
|
+
|---|---|---|---|---|---|
|
|
122
|
+
| Ubuntu 24.04 arm64 (Parallels VM) | Ubuntu 24.04 | arm64 | ✓ | `/etc/apparmor.d/cdp-mcp-chromium` (named-unconfined, mirrors Ubuntu's stock `chrome` / `msedge` / `brave`) | `kernel.apparmor_restrict_unprivileged_userns = 0` was set as a side effect of enabling Bubblewrap, so the kernel-level userns restriction is already off system-wide. The AppArmor profile gives Playwright Chromium a stable named label (instead of `unconfined`) and grants `userns,` explicitly, so `sandbox: true` keeps working even if a future kernel/package update flips the global knob back to `1`. |
|
|
123
|
+
|
|
124
|
+
When adding a new host to this table:
|
|
125
|
+
|
|
126
|
+
1. Run the smoke tests below and capture the values.
|
|
127
|
+
2. If `kernel.apparmor_restrict_unprivileged_userns` is `1` (Ubuntu's stock default) and `sandbox: true` is desired, install a profile under `/etc/apparmor.d/` mirroring Ubuntu's stock `chrome` / `msedge` / `brave` shape:
|
|
128
|
+
```apparmor
|
|
129
|
+
abi <abi/4.0>,
|
|
130
|
+
include <tunables/global>
|
|
131
|
+
|
|
132
|
+
profile cdp-mcp-chromium /path/to/chromium flags=(unconfined) {
|
|
133
|
+
userns,
|
|
134
|
+
include if exists <local/cdp-mcp-chromium>
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
Load with `sudo apparmor_parser -r /etc/apparmor.d/cdp-mcp-chromium`. The profile auto-loads at boot from `/etc/apparmor.d/`.
|
|
138
|
+
3. Verify the running browser process is labelled correctly:
|
|
139
|
+
```sh
|
|
140
|
+
cat /proc/<chromium-pid>/attr/current
|
|
141
|
+
```
|
|
142
|
+
Expect the profile name (e.g. `cdp-mcp-chromium (unconfined)`), not `unconfined` alone.
|
|
143
|
+
4. Add a row to the table above.
|
|
144
|
+
|
|
145
|
+
Other hosts to characterize as they come online: Fedora — Fedora uses SELinux rather than AppArmor and ships userns enabled by default, so `sandbox: true` is expected to work without host-side profile work. The `dnf install bubblewrap` path is also first-class on Fedora.
|
|
146
|
+
|
|
147
|
+
## Smoke tests
|
|
148
|
+
|
|
149
|
+
Check whether unprivileged user namespaces are enabled:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
cat /proc/sys/kernel/unprivileged_userns_clone
|
|
153
|
+
cat /proc/sys/user/max_user_namespaces
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Expected working values are usually `1` for
|
|
157
|
+
`kernel.unprivileged_userns_clone` and a nonzero number for
|
|
158
|
+
`user.max_user_namespaces`.
|
|
159
|
+
|
|
160
|
+
On Ubuntu, AppArmor may still restrict unprivileged user namespaces:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
sysctl kernel.apparmor_restrict_unprivileged_userns
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Minimal Bubblewrap smoke tests:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / /usr/bin/true
|
|
170
|
+
bwrap --unshare-user --uid 0 --gid 0 --unshare-net --ro-bind / / /usr/bin/true
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If the first command fails with `setting up uid map: Permission denied`, the
|
|
174
|
+
host still blocks the user namespace setup Bubblewrap needs.
|
|
175
|
+
|
|
176
|
+
If the second command fails with `loopback: Failed RTM_NEWADDR: Operation not
|
|
177
|
+
permitted`, the network namespace setup is still blocked.
|
|
178
|
+
|
|
179
|
+
Project-level browser check:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
npm run test:e2e
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
For a direct MCP check, call `launch_chrome` with `sandbox: true` on the host
|
|
186
|
+
you want to validate. If Chromium cannot create a usable sandbox, it will fail
|
|
187
|
+
before exposing its DevTools target.
|
|
188
|
+
|
|
189
|
+
## Decision summary
|
|
190
|
+
|
|
191
|
+
- `--no-sandbox` remains the automation default because it keeps Ubuntu
|
|
192
|
+
Playwright-Chromium runs working.
|
|
193
|
+
- `sandbox: true` is the preferred security posture when the host supports it.
|
|
194
|
+
- AppArmor is the long-term host policy path for allowing Chromium's needed
|
|
195
|
+
user namespace behavior narrowly.
|
|
196
|
+
- Bubblewrap is an outer containment layer and is valuable defense-in-depth.
|
|
197
|
+
- Bubblewrap does not replace Chromium's internal renderer sandbox.
|