@mehmoodqureshi/chrome-mcp 0.6.7 → 0.7.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 +116 -0
- package/dist/shared/observers.d.ts +110 -0
- package/dist/shared/observers.js +127 -0
- package/dist/shared/page-fns.d.ts +46 -0
- package/dist/shared/page-fns.js +292 -0
- package/dist/shared/policy.d.ts +9 -0
- package/dist/shared/policy.js +16 -0
- package/dist/shared/protocol.d.ts +11 -2
- package/dist/shared/protocol.js +3 -0
- package/dist/shared/snapshot.d.ts +2 -0
- package/dist/shared/snapshot.js +8 -1
- package/dist/src/bridge/workspace.d.ts +6 -0
- package/dist/src/bridge/workspace.js +20 -0
- package/dist/src/config.js +31 -0
- package/dist/src/executor/extension-executor.d.ts +27 -13
- package/dist/src/executor/extension-executor.js +53 -12
- package/dist/src/executor/stub-executor.d.ts +19 -1
- package/dist/src/executor/stub-executor.js +22 -6
- package/dist/src/executor/types.d.ts +72 -13
- package/dist/src/mcp/audit.d.ts +39 -0
- package/dist/src/mcp/audit.js +60 -0
- package/dist/src/mcp/helpers.d.ts +4 -4
- package/dist/src/mcp/helpers.js +9 -4
- package/dist/src/mcp/locate.d.ts +45 -0
- package/dist/src/mcp/locate.js +105 -0
- package/dist/src/mcp/redact.d.ts +48 -0
- package/dist/src/mcp/redact.js +92 -0
- package/dist/src/mcp/snapdiff.d.ts +43 -0
- package/dist/src/mcp/snapdiff.js +92 -0
- package/dist/src/mcp/tools.js +429 -43
- package/dist/src/security/policy.d.ts +5 -0
- package/dist/src/security/policy.js +6 -0
- package/docs/BLUEPRINT.md +15 -1
- package/extension-dist/background.js +617 -214
- package/extension-dist/page-hook.js +215 -0
- package/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* shared/page-fns.ts — the ONE function that runs inside a page for every
|
|
4
|
+
* DOM-touching command.
|
|
5
|
+
*
|
|
6
|
+
* MUST be self-contained: `chrome.scripting.executeScript` serializes it to
|
|
7
|
+
* source, so it may not close over anything from this module. That constraint is
|
|
8
|
+
* exactly why every op lives in one function instead of ten — the shared
|
|
9
|
+
* helpers below (`deepQuery` above all) can then be written once and are
|
|
10
|
+
* automatically used by every op.
|
|
11
|
+
*
|
|
12
|
+
* `deepQuery` is the reason this file exists. `snapshot` deliberately walks open
|
|
13
|
+
* shadow roots and stamps `data-mcp-ref` on what it finds there, but every
|
|
14
|
+
* action used to resolve that ref with a plain `document.querySelector`, which
|
|
15
|
+
* cannot cross a shadow boundary. So the snapshot advertised elements that no
|
|
16
|
+
* click could ever reach — a structural dead end on every web-component site.
|
|
17
|
+
* One resolver, used by every op, is what closes it.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.pageOp = pageOp;
|
|
21
|
+
/**
|
|
22
|
+
* Runs IN THE PAGE (or in one frame of it). Returns a plain JSON-able object;
|
|
23
|
+
* `found: false` means the selector matched nothing, which the caller renders as
|
|
24
|
+
* SELECTOR_NOT_FOUND. Never throws across the boundary — a page that blows up
|
|
25
|
+
* inside an op is reported as `{ ok: false, error }`.
|
|
26
|
+
*/
|
|
27
|
+
function pageOp(a) {
|
|
28
|
+
// -- shared helpers (must stay INSIDE: this function is serialized alone) --
|
|
29
|
+
/** querySelector that descends into open shadow roots, breadth-ish first. */
|
|
30
|
+
const deepQuery = (sel) => {
|
|
31
|
+
const roots = [document];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
while (roots.length > 0) {
|
|
34
|
+
const root = roots.shift();
|
|
35
|
+
if (seen.has(root))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(root);
|
|
38
|
+
let hit = null;
|
|
39
|
+
try {
|
|
40
|
+
hit = root.querySelector(sel);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null; // malformed selector — same answer for every root
|
|
44
|
+
}
|
|
45
|
+
if (hit)
|
|
46
|
+
return hit;
|
|
47
|
+
let hosts = [];
|
|
48
|
+
try {
|
|
49
|
+
hosts = Array.from(root.querySelectorAll('*'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
hosts = [];
|
|
53
|
+
}
|
|
54
|
+
for (const host of hosts) {
|
|
55
|
+
const sr = host.shadowRoot;
|
|
56
|
+
if (sr && !seen.has(sr))
|
|
57
|
+
roots.push(sr);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Where this frame sits inside the TOP document, so a CDP mouse event (which
|
|
64
|
+
* is dispatched in top-viewport coordinates) lands on an element that lives in
|
|
65
|
+
* an iframe. `window.frameElement` is readable only when the parent is
|
|
66
|
+
* same-origin, so a cross-origin ancestor yields `exact: false` and the caller
|
|
67
|
+
* falls back to a synthetic click rather than clicking the wrong pixel.
|
|
68
|
+
*/
|
|
69
|
+
const frameOffset = () => {
|
|
70
|
+
let dx = 0;
|
|
71
|
+
let dy = 0;
|
|
72
|
+
let win = window;
|
|
73
|
+
try {
|
|
74
|
+
while (win.parent && win.parent !== win) {
|
|
75
|
+
const fe = win.frameElement;
|
|
76
|
+
if (!fe)
|
|
77
|
+
return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
78
|
+
const r = fe.getBoundingClientRect();
|
|
79
|
+
dx += r.left;
|
|
80
|
+
dy += r.top;
|
|
81
|
+
win = win.parent;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
86
|
+
}
|
|
87
|
+
return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
|
|
88
|
+
};
|
|
89
|
+
const sel = typeof a.selector === 'string' && a.selector.length > 0 ? a.selector : null;
|
|
90
|
+
const el = sel ? deepQuery(sel) : null;
|
|
91
|
+
const missing = sel !== null && el === null;
|
|
92
|
+
/** Set a value the way React/Vue see it (they patch the instance setter). */
|
|
93
|
+
const setValue = (node, next) => {
|
|
94
|
+
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), 'value')?.set;
|
|
95
|
+
if (setter)
|
|
96
|
+
setter.call(node, next);
|
|
97
|
+
else
|
|
98
|
+
node.value = next;
|
|
99
|
+
node.dispatchEvent(new Event('input', { bubbles: true }));
|
|
100
|
+
};
|
|
101
|
+
switch (a.op) {
|
|
102
|
+
case 'probe':
|
|
103
|
+
return { found: true, url: location.href, title: document.title };
|
|
104
|
+
case 'text': {
|
|
105
|
+
if (missing)
|
|
106
|
+
return { found: false };
|
|
107
|
+
const root = el ?? document.body;
|
|
108
|
+
return { found: true, text: root ? root.innerText ?? '' : '' };
|
|
109
|
+
}
|
|
110
|
+
case 'html': {
|
|
111
|
+
if (missing)
|
|
112
|
+
return { found: false };
|
|
113
|
+
const root = el ?? document.documentElement;
|
|
114
|
+
if (!root)
|
|
115
|
+
return { found: true, html: '' };
|
|
116
|
+
const outer = a.outer === true || !sel;
|
|
117
|
+
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
118
|
+
}
|
|
119
|
+
case 'click': {
|
|
120
|
+
if (!el)
|
|
121
|
+
return { found: false };
|
|
122
|
+
el.scrollIntoView({ block: 'center' });
|
|
123
|
+
el.click();
|
|
124
|
+
return { found: true };
|
|
125
|
+
}
|
|
126
|
+
case 'type': {
|
|
127
|
+
const node = el;
|
|
128
|
+
if (!node)
|
|
129
|
+
return { found: false };
|
|
130
|
+
node.focus();
|
|
131
|
+
const next = (a.clear ? '' : node.value ?? '') + (a.text ?? '');
|
|
132
|
+
setValue(node, next);
|
|
133
|
+
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
134
|
+
return { found: true };
|
|
135
|
+
}
|
|
136
|
+
case 'focus': {
|
|
137
|
+
const node = el;
|
|
138
|
+
if (!node)
|
|
139
|
+
return { found: false };
|
|
140
|
+
node.focus();
|
|
141
|
+
if (a.clear)
|
|
142
|
+
setValue(node, '');
|
|
143
|
+
return { found: true };
|
|
144
|
+
}
|
|
145
|
+
case 'point': {
|
|
146
|
+
if (!el)
|
|
147
|
+
return { found: false };
|
|
148
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
149
|
+
const r = el.getBoundingClientRect();
|
|
150
|
+
const off = frameOffset();
|
|
151
|
+
return {
|
|
152
|
+
found: true,
|
|
153
|
+
x: r.left + r.width / 2 + off.dx,
|
|
154
|
+
y: r.top + r.height / 2 + off.dy,
|
|
155
|
+
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
156
|
+
exact: off.exact,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
case 'hover': {
|
|
160
|
+
if (!el)
|
|
161
|
+
return { found: false };
|
|
162
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
163
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
164
|
+
return { found: true };
|
|
165
|
+
}
|
|
166
|
+
case 'select': {
|
|
167
|
+
const node = el;
|
|
168
|
+
if (!node || !node.options)
|
|
169
|
+
return { found: false };
|
|
170
|
+
const want = new Set(a.values ?? []);
|
|
171
|
+
let matched = false;
|
|
172
|
+
for (const opt of Array.from(node.options)) {
|
|
173
|
+
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
174
|
+
opt.selected = on;
|
|
175
|
+
if (on)
|
|
176
|
+
matched = true;
|
|
177
|
+
}
|
|
178
|
+
node.dispatchEvent(new Event('input', { bubbles: true }));
|
|
179
|
+
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
180
|
+
return { found: true, matched };
|
|
181
|
+
}
|
|
182
|
+
case 'measure': {
|
|
183
|
+
const d = document.documentElement;
|
|
184
|
+
const dims = {
|
|
185
|
+
w: window.innerWidth,
|
|
186
|
+
h: window.innerHeight,
|
|
187
|
+
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
188
|
+
fullH: Math.max(d.scrollHeight, d.clientHeight),
|
|
189
|
+
};
|
|
190
|
+
if (!sel)
|
|
191
|
+
return { found: true, dims, element: null, missing: false };
|
|
192
|
+
if (!el)
|
|
193
|
+
return { found: true, dims, element: null, missing: true };
|
|
194
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
195
|
+
const r = el.getBoundingClientRect();
|
|
196
|
+
const off = frameOffset();
|
|
197
|
+
// viewport rect + this frame's offset + the TOP document's scroll ->
|
|
198
|
+
// document coordinates of the page the screenshot actually captures.
|
|
199
|
+
return {
|
|
200
|
+
found: true,
|
|
201
|
+
dims,
|
|
202
|
+
element: {
|
|
203
|
+
x: r.left + off.dx + off.scrollX,
|
|
204
|
+
y: r.top + off.dy + off.scrollY,
|
|
205
|
+
w: r.width,
|
|
206
|
+
h: r.height,
|
|
207
|
+
},
|
|
208
|
+
missing: false,
|
|
209
|
+
exact: off.exact,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case 'scroll': {
|
|
213
|
+
if (el)
|
|
214
|
+
el.scrollIntoView({ block: 'center' });
|
|
215
|
+
else if (a.x != null || a.y != null)
|
|
216
|
+
window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
217
|
+
else
|
|
218
|
+
window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
219
|
+
return { found: !missing };
|
|
220
|
+
}
|
|
221
|
+
case 'storage': {
|
|
222
|
+
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
223
|
+
const op = a.storageOp;
|
|
224
|
+
if (op === 'set') {
|
|
225
|
+
store.setItem(String(a.key), String(a.value ?? ''));
|
|
226
|
+
return { found: true, ok: true };
|
|
227
|
+
}
|
|
228
|
+
if (op === 'remove') {
|
|
229
|
+
store.removeItem(String(a.key));
|
|
230
|
+
return { found: true, ok: true };
|
|
231
|
+
}
|
|
232
|
+
if (op === 'clear') {
|
|
233
|
+
store.clear();
|
|
234
|
+
return { found: true, ok: true };
|
|
235
|
+
}
|
|
236
|
+
if (a.key)
|
|
237
|
+
return { found: true, ok: true, value: store.getItem(a.key) };
|
|
238
|
+
const entries = {};
|
|
239
|
+
for (let i = 0; i < store.length; i++) {
|
|
240
|
+
const k = store.key(i);
|
|
241
|
+
if (k)
|
|
242
|
+
entries[k] = store.getItem(k) ?? '';
|
|
243
|
+
}
|
|
244
|
+
return { found: true, ok: true, entries };
|
|
245
|
+
}
|
|
246
|
+
// -- the two polling ops: one injection that resolves in-page, rather than
|
|
247
|
+
// one executeScript round-trip per tick --
|
|
248
|
+
case 'waitSelector': {
|
|
249
|
+
const deadline = Date.now() + (a.timeoutMs ?? 5_000);
|
|
250
|
+
const every = a.interval ?? 120;
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
const tick = () => {
|
|
253
|
+
if (sel && deepQuery(sel))
|
|
254
|
+
return resolve({ found: true });
|
|
255
|
+
if (Date.now() > deadline)
|
|
256
|
+
return resolve({ found: false });
|
|
257
|
+
setTimeout(tick, every);
|
|
258
|
+
};
|
|
259
|
+
tick();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
case 'waitFor': {
|
|
263
|
+
const deadline = Date.now() + (a.timeoutMs ?? 30_000);
|
|
264
|
+
const every = a.interval ?? 150;
|
|
265
|
+
const want = typeof a.textContains === 'string' && a.textContains.length > 0 ? a.textContains : null;
|
|
266
|
+
const gone = a.gone === true;
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
const hit = () => {
|
|
269
|
+
let present;
|
|
270
|
+
if (sel)
|
|
271
|
+
present = !!deepQuery(sel);
|
|
272
|
+
else if (want)
|
|
273
|
+
present = (document.body?.innerText ?? '').includes(want);
|
|
274
|
+
else
|
|
275
|
+
present = true;
|
|
276
|
+
return gone ? !present : present;
|
|
277
|
+
};
|
|
278
|
+
const tick = () => {
|
|
279
|
+
if (hit())
|
|
280
|
+
return resolve({ found: true, matched: true });
|
|
281
|
+
if (Date.now() > deadline)
|
|
282
|
+
return resolve({ found: true, matched: false });
|
|
283
|
+
setTimeout(tick, every);
|
|
284
|
+
};
|
|
285
|
+
tick();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
return { found: false, error: `unknown page op: ${String(a.op)}` };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=page-fns.js.map
|
package/dist/shared/policy.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ export declare function isMutatingMethod(method: WireMethod): boolean;
|
|
|
17
17
|
export declare function isUrlGated(method: WireMethod): boolean;
|
|
18
18
|
/** Parse a host out of a URL string; '' if it has none (about:blank, data:, …). */
|
|
19
19
|
export declare function hostOf(url: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Reduce an allowlist entry to the bare host it constrains. Users routinely paste
|
|
22
|
+
* a full URL ("https://example.com/app") or a "host:port/path" instead of a bare
|
|
23
|
+
* host; those forms would never equal a hostname and so silently match nothing.
|
|
24
|
+
* We strip the scheme, userinfo, port, and path/query/fragment, preserving a
|
|
25
|
+
* leading "*." wildcard and the two catch-all forms. Returns '' for a pattern
|
|
26
|
+
* that carries no host (which then matches nothing).
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeDomainPattern(pattern: string): string;
|
|
20
29
|
export declare function isDomainAllowed(url: string, policy: WirePolicy): boolean;
|
|
21
30
|
export type PolicyVerdict = {
|
|
22
31
|
ok: true;
|
package/dist/shared/policy.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.isReadMethod = isReadMethod;
|
|
|
15
15
|
exports.isMutatingMethod = isMutatingMethod;
|
|
16
16
|
exports.isUrlGated = isUrlGated;
|
|
17
17
|
exports.hostOf = hostOf;
|
|
18
|
+
exports.normalizeDomainPattern = normalizeDomainPattern;
|
|
18
19
|
exports.isDomainAllowed = isDomainAllowed;
|
|
19
20
|
exports.evaluatePolicy = evaluatePolicy;
|
|
20
21
|
exports.blockedDomainMessage = blockedDomainMessage;
|
|
@@ -27,6 +28,12 @@ const READ_CONTENT = new Set([
|
|
|
27
28
|
'get_html',
|
|
28
29
|
'screenshot',
|
|
29
30
|
'wait_for',
|
|
31
|
+
// A frame list leaks the URLs a page embeds; console/network buffers are the
|
|
32
|
+
// page's own traffic and error text; a PDF is a screenshot by another name.
|
|
33
|
+
// All three are page CONTENT and gate exactly like the reads above.
|
|
34
|
+
'frames_list',
|
|
35
|
+
'observers',
|
|
36
|
+
'print_pdf',
|
|
30
37
|
]);
|
|
31
38
|
/** Content-mutating actions — URL-gated AND mutation-gated. */
|
|
32
39
|
const MUTATE_CONTENT = new Set([
|
|
@@ -132,6 +139,14 @@ function evaluatePolicy(url, method, policy) {
|
|
|
132
139
|
if (method === 'download_file' && !policy.allowDownloads) {
|
|
133
140
|
return { ok: false, reason: 'downloads are disabled. Pass --enable-downloads or set allowDownloads.' };
|
|
134
141
|
}
|
|
142
|
+
if (method === 'observers' && !policy.allowObservers) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: 'the in-page observers (console_logs / network_log / dialogs) are disabled. They patch console, ' +
|
|
146
|
+
'fetch, XMLHttpRequest and the dialog functions on every allowlisted page in your real browser, ' +
|
|
147
|
+
'so they are opt-in: pass --enable-observers or set allowObservers.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
135
150
|
if (method === 'upload_file' && !policy.allowUploads) {
|
|
136
151
|
return {
|
|
137
152
|
ok: false,
|
|
@@ -182,5 +197,6 @@ exports.DENY_ALL_WIRE_POLICY = {
|
|
|
182
197
|
allowUploads: false,
|
|
183
198
|
allowAllTabs: false,
|
|
184
199
|
enableMutations: false,
|
|
200
|
+
allowObservers: false,
|
|
185
201
|
};
|
|
186
202
|
//# sourceMappingURL=policy.js.map
|
|
@@ -44,10 +44,10 @@ export declare const WIRE_CAP_TAB_URL: "tab-url";
|
|
|
44
44
|
* `download_file` (privileged, executor-owned) and `ping_probe` (a short-deadline
|
|
45
45
|
* responsiveness check used to detect a dead-but-not-yet-reconnected worker).
|
|
46
46
|
*/
|
|
47
|
-
export type WireMethod = 'tabs_list' | 'tab_select' | 'tab_new' | 'tab_close' | 'navigate' | 'back' | 'forward' | 'reload' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'screenshot' | 'get_text' | 'get_html' | 'snapshot' | 'select_option' | 'get_cookies' | 'storage' | 'eval' | 'wait_for' | 'download_file' | 'upload_file' | 'ping_probe';
|
|
47
|
+
export type WireMethod = 'tabs_list' | 'tab_select' | 'tab_new' | 'tab_close' | 'navigate' | 'back' | 'forward' | 'reload' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'screenshot' | 'get_text' | 'get_html' | 'snapshot' | 'select_option' | 'get_cookies' | 'storage' | 'eval' | 'wait_for' | 'download_file' | 'upload_file' | 'frames_list' | 'observers' | 'print_pdf' | 'ping_probe';
|
|
48
48
|
/** Runtime list of every WireMethod, for boot-time drift assertions on both ends. */
|
|
49
49
|
export declare const WIRE_METHODS: readonly WireMethod[];
|
|
50
|
-
export type ExecutorErrorCode = 'NO_TARGET' | 'TARGET_GONE' | 'DETACHED' | 'DEVTOOLS_OPEN' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_THREW' | 'TIMEOUT' | 'BAD_ARGS' | 'CDP_ERROR' | 'POLICY_DENIED' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'UNKNOWN_METHOD';
|
|
50
|
+
export type ExecutorErrorCode = 'NO_TARGET' | 'TARGET_GONE' | 'DETACHED' | 'DEVTOOLS_OPEN' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_THREW' | 'TIMEOUT' | 'BAD_ARGS' | 'CDP_ERROR' | 'POLICY_DENIED' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'FRAME_NOT_FOUND' | 'OBSERVERS_DISABLED' | 'UNKNOWN_METHOD';
|
|
51
51
|
export interface BaseFrame {
|
|
52
52
|
type: string;
|
|
53
53
|
v: ProtocolVersion;
|
|
@@ -80,6 +80,15 @@ export interface WirePolicy {
|
|
|
80
80
|
allowUploads: boolean;
|
|
81
81
|
allowAllTabs: boolean;
|
|
82
82
|
enableMutations: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the in-page observer hook (console / network / dialog capture) may
|
|
85
|
+
* be installed. Off by default: it patches `console`, `fetch`,
|
|
86
|
+
* `XMLHttpRequest` and the dialog functions on every allowlisted page in the
|
|
87
|
+
* user's real browser, which is too invasive to turn on for someone silently.
|
|
88
|
+
* Optional so an older extension deserializing a newer welcome frame reads it
|
|
89
|
+
* as undefined -> falsy -> the safe answer.
|
|
90
|
+
*/
|
|
91
|
+
allowObservers?: boolean;
|
|
83
92
|
}
|
|
84
93
|
export interface WelcomeFrame extends BaseFrame {
|
|
85
94
|
type: 'welcome';
|
package/dist/shared/protocol.js
CHANGED
package/dist/shared/snapshot.js
CHANGED
|
@@ -140,8 +140,15 @@ function collectSnapshot(interactiveOnly = true, max = 200) {
|
|
|
140
140
|
const ref = `e${++n}`;
|
|
141
141
|
el.setAttribute('data-mcp-ref', ref);
|
|
142
142
|
const node = { ref, role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
|
|
143
|
+
// A password field's characters never leave the page. The node still
|
|
144
|
+
// appears (so the model can target it) and is flagged `secret`, but the
|
|
145
|
+
// value is not something a caller has a use for and every caller would
|
|
146
|
+
// otherwise get it by default.
|
|
147
|
+
const isSecret = el.tagName === 'INPUT' && el.type === 'password';
|
|
148
|
+
if (isSecret)
|
|
149
|
+
node.secret = true;
|
|
143
150
|
const v = el.value;
|
|
144
|
-
if (typeof v === 'string' && v)
|
|
151
|
+
if (!isSecret && typeof v === 'string' && v)
|
|
145
152
|
node.value = v.slice(0, 200);
|
|
146
153
|
if (el.disabled)
|
|
147
154
|
node.disabled = true;
|
|
@@ -37,6 +37,12 @@ export declare function switchWorkspace(opts: {
|
|
|
37
37
|
* Returns the path written, or null if persisted nowhere (no workspace / error).
|
|
38
38
|
*/
|
|
39
39
|
export declare function saveResult(tool: string, ext: string, body: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Save a binary artifact (a PDF, today) into the active task's results dir.
|
|
42
|
+
* Same contract as `saveResult`: best-effort, returns the path or null, and a
|
|
43
|
+
* failure here never fails the tool call that produced the bytes.
|
|
44
|
+
*/
|
|
45
|
+
export declare function saveBinary(tool: string, ext: string, bytes: Buffer): string | null;
|
|
40
46
|
/** Save a screenshot PNG (base64) into the active task's `screenshots/`. */
|
|
41
47
|
export declare function saveScreenshot(dataBase64: string): string | null;
|
|
42
48
|
/**
|
|
@@ -19,6 +19,7 @@ exports.peekActiveWorkspace = peekActiveWorkspace;
|
|
|
19
19
|
exports.resetActiveWorkspaceForTesting = resetActiveWorkspaceForTesting;
|
|
20
20
|
exports.switchWorkspace = switchWorkspace;
|
|
21
21
|
exports.saveResult = saveResult;
|
|
22
|
+
exports.saveBinary = saveBinary;
|
|
22
23
|
exports.saveScreenshot = saveScreenshot;
|
|
23
24
|
exports.captureDownload = captureDownload;
|
|
24
25
|
exports.appendHistory = appendHistory;
|
|
@@ -103,6 +104,25 @@ function saveResult(tool, ext, body) {
|
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Save a binary artifact (a PDF, today) into the active task's results dir.
|
|
109
|
+
* Same contract as `saveResult`: best-effort, returns the path or null, and a
|
|
110
|
+
* failure here never fails the tool call that produced the bytes.
|
|
111
|
+
*/
|
|
112
|
+
function saveBinary(tool, ext, bytes) {
|
|
113
|
+
const w = peekActiveWorkspace();
|
|
114
|
+
if (!w)
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
const path = (0, node_path_1.join)(w.resultsDir, `${stem(tool)}.${ext}`);
|
|
118
|
+
(0, node_fs_1.writeFileSync)(path, bytes, { mode: 0o600 });
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
logErr(`results save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
106
126
|
/** Save a screenshot PNG (base64) into the active task's `screenshots/`. */
|
|
107
127
|
function saveScreenshot(dataBase64) {
|
|
108
128
|
const w = peekActiveWorkspace();
|
package/dist/src/config.js
CHANGED
|
@@ -134,6 +134,18 @@ function parseArgs(argv) {
|
|
|
134
134
|
case '--allow-all-tabs':
|
|
135
135
|
policyFlags.allowAllTabs = true;
|
|
136
136
|
break;
|
|
137
|
+
case '--enable-observers':
|
|
138
|
+
policyFlags.allowObservers = true;
|
|
139
|
+
break;
|
|
140
|
+
case '--redact':
|
|
141
|
+
policyFlags.redact = true;
|
|
142
|
+
break;
|
|
143
|
+
case '--redact-pattern':
|
|
144
|
+
// A custom pattern only means anything with redaction on, so asking for
|
|
145
|
+
// one turns it on rather than being silently ignored.
|
|
146
|
+
policyFlags.redact = true;
|
|
147
|
+
(policyFlags.redactPatterns ??= []).push(requireValue(argv[++i], '--redact-pattern'));
|
|
148
|
+
break;
|
|
137
149
|
case '--cdp-fallback':
|
|
138
150
|
cdpFallback = true;
|
|
139
151
|
break;
|
|
@@ -177,6 +189,17 @@ function parseArgs(argv) {
|
|
|
177
189
|
if (policy.allowUploads && !policy.uploadsDir) {
|
|
178
190
|
throw new Error('--enable-uploads requires --uploads-dir <path> (uploads must be confined to a directory)');
|
|
179
191
|
}
|
|
192
|
+
// Fail at startup, not on the first read, if a redaction pattern is malformed:
|
|
193
|
+
// a pattern the user believes is scrubbing secrets but that never compiled is
|
|
194
|
+
// the worst of both worlds.
|
|
195
|
+
for (const source of policy.redactPatterns ?? []) {
|
|
196
|
+
try {
|
|
197
|
+
new RegExp(source);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
throw new Error(`--redact-pattern ${JSON.stringify(source)} is not a valid regular expression: ${String(err)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
180
203
|
return {
|
|
181
204
|
wsPort,
|
|
182
205
|
dataDir: resolveDataDir(),
|
|
@@ -269,6 +292,14 @@ Security (default: deny-all safe mode):
|
|
|
269
292
|
--enable-uploads Enable upload_file — sends local files to a page (off by default)
|
|
270
293
|
--uploads-dir <path> Restrict upload_file to files inside <path> (recommended with --enable-uploads)
|
|
271
294
|
--allow-all-tabs Relax tab list/select to all tabs
|
|
295
|
+
--enable-observers Enable console_logs / network_log / dialogs. Installs an
|
|
296
|
+
in-page hook on allowlisted sites that records console
|
|
297
|
+
output, fetch/XHR traffic, and intercepts alert/confirm/
|
|
298
|
+
prompt (off by default: it patches page globals).
|
|
299
|
+
--redact Scrub secret-shaped strings (JWTs, cloud keys, bearer
|
|
300
|
+
tokens, private keys) out of page reads. Password field
|
|
301
|
+
values are always suppressed, with or without this.
|
|
302
|
+
--redact-pattern <re> Add a redaction regex (repeatable; implies --redact)
|
|
272
303
|
|
|
273
304
|
Misc:
|
|
274
305
|
--log-level <lvl> silent | info | debug (default info)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* method-specific arguments travel in `params`. Results are trusted shapes
|
|
8
8
|
* produced by the extension router (validated there).
|
|
9
9
|
*/
|
|
10
|
-
import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type KeyModifier, type MouseButton, type NavResult, type ScreenshotResult, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
|
|
10
|
+
import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type FrameInfo, type FrameOpts, type ObserverArgs, type ObserverReadResult, type PdfResult, type KeyModifier, type MouseButton, type NavResult, type ScreenshotResult, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
|
|
11
11
|
import type { BridgeServer } from '../bridge/server';
|
|
12
12
|
export declare class ExtensionExecutor implements Executor {
|
|
13
13
|
private readonly bridge;
|
|
@@ -49,27 +49,27 @@ export declare class ExtensionExecutor implements Executor {
|
|
|
49
49
|
button?: MouseButton;
|
|
50
50
|
clickCount?: number;
|
|
51
51
|
trusted?: boolean;
|
|
52
|
-
}): Promise<ActionOk>;
|
|
52
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
53
53
|
type(t: Target, text: string, opts?: {
|
|
54
54
|
tabId?: TabId;
|
|
55
55
|
clear?: boolean;
|
|
56
56
|
pressEnter?: boolean;
|
|
57
57
|
keyEvents?: boolean;
|
|
58
58
|
trusted?: boolean;
|
|
59
|
-
}): Promise<ActionOk>;
|
|
59
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
60
60
|
selectOption(t: Target, values: string[], opts?: {
|
|
61
61
|
tabId?: TabId;
|
|
62
|
-
}): Promise<ActionOk>;
|
|
62
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
63
63
|
fill(t: Target, value: string, opts?: {
|
|
64
64
|
tabId?: TabId;
|
|
65
|
-
}): Promise<ActionOk>;
|
|
65
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
66
66
|
press(key: string, opts?: {
|
|
67
67
|
tabId?: TabId;
|
|
68
68
|
modifiers?: KeyModifier[];
|
|
69
69
|
}): Promise<ActionOk>;
|
|
70
70
|
hover(t: Target, opts?: {
|
|
71
71
|
tabId?: TabId;
|
|
72
|
-
}): Promise<ActionOk>;
|
|
72
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
73
73
|
scroll(opts: {
|
|
74
74
|
tabId?: TabId;
|
|
75
75
|
x?: number;
|
|
@@ -77,24 +77,24 @@ export declare class ExtensionExecutor implements Executor {
|
|
|
77
77
|
deltaX?: number;
|
|
78
78
|
deltaY?: number;
|
|
79
79
|
target?: Target;
|
|
80
|
-
}): Promise<ActionOk>;
|
|
80
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
81
81
|
getText(t?: Target, opts?: {
|
|
82
82
|
tabId?: TabId;
|
|
83
|
-
}): Promise<{
|
|
83
|
+
} & FrameOpts): Promise<{
|
|
84
84
|
text: string;
|
|
85
85
|
ref?: string;
|
|
86
86
|
}>;
|
|
87
87
|
getHtml(t?: Target, opts?: {
|
|
88
88
|
tabId?: TabId;
|
|
89
89
|
outer?: boolean;
|
|
90
|
-
}): Promise<{
|
|
90
|
+
} & FrameOpts): Promise<{
|
|
91
91
|
html: string;
|
|
92
92
|
}>;
|
|
93
93
|
snapshot(opts?: {
|
|
94
94
|
tabId?: TabId;
|
|
95
95
|
interactiveOnly?: boolean;
|
|
96
96
|
max?: number;
|
|
97
|
-
}): Promise<SnapshotResult>;
|
|
97
|
+
} & FrameOpts): Promise<SnapshotResult>;
|
|
98
98
|
getCookies(opts?: {
|
|
99
99
|
tabId?: TabId;
|
|
100
100
|
url?: string;
|
|
@@ -112,18 +112,18 @@ export declare class ExtensionExecutor implements Executor {
|
|
|
112
112
|
tabId?: TabId;
|
|
113
113
|
fullPage?: boolean;
|
|
114
114
|
target?: Target;
|
|
115
|
-
}): Promise<ScreenshotResult>;
|
|
115
|
+
} & FrameOpts): Promise<ScreenshotResult>;
|
|
116
116
|
eval(expression: string, opts?: {
|
|
117
117
|
tabId?: TabId;
|
|
118
118
|
awaitPromise?: boolean;
|
|
119
|
-
}): Promise<EvalResult>;
|
|
119
|
+
} & FrameOpts): Promise<EvalResult>;
|
|
120
120
|
waitFor(opts: {
|
|
121
121
|
tabId?: TabId;
|
|
122
122
|
selector?: string;
|
|
123
123
|
textContains?: string;
|
|
124
124
|
gone?: boolean;
|
|
125
125
|
timeoutMs?: number;
|
|
126
|
-
}): Promise<WaitResult>;
|
|
126
|
+
} & FrameOpts): Promise<WaitResult>;
|
|
127
127
|
download(args: {
|
|
128
128
|
url?: string;
|
|
129
129
|
target?: Target;
|
|
@@ -133,4 +133,18 @@ export declare class ExtensionExecutor implements Executor {
|
|
|
133
133
|
uploadFile(t: Target, files: string[], opts?: {
|
|
134
134
|
tabId?: TabId;
|
|
135
135
|
}): Promise<ActionOk>;
|
|
136
|
+
framesList(opts?: {
|
|
137
|
+
tabId?: TabId;
|
|
138
|
+
}): Promise<FrameInfo[]>;
|
|
139
|
+
observers(args: ObserverArgs): Promise<ObserverReadResult>;
|
|
140
|
+
printPdf(opts?: {
|
|
141
|
+
tabId?: TabId;
|
|
142
|
+
landscape?: boolean;
|
|
143
|
+
printBackground?: boolean;
|
|
144
|
+
scale?: number;
|
|
145
|
+
paperWidth?: number;
|
|
146
|
+
paperHeight?: number;
|
|
147
|
+
pageRanges?: string;
|
|
148
|
+
preferCSSPageSize?: boolean;
|
|
149
|
+
}): Promise<PdfResult>;
|
|
136
150
|
}
|