@mehmoodqureshi/chrome-mcp 0.6.7 → 0.7.1
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 +26 -7
- 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
package/README.md
CHANGED
|
@@ -201,6 +201,99 @@ silently mis-routed. (`tab_new`, `tabs_list`, `chrome_status` are exempt.)
|
|
|
201
201
|
> `navigate`, to open without losing the current tab). Pass `active: false` to
|
|
202
202
|
> open in the background; parallel batches do this automatically.
|
|
203
203
|
|
|
204
|
+
### Reaching into iframes and shadow roots
|
|
205
|
+
|
|
206
|
+
A selector that "should" match but doesn't almost always means the element is
|
|
207
|
+
somewhere your selector cannot reach: inside an `<iframe>` (checkout widgets,
|
|
208
|
+
OAuth consent screens, embedded editors) or inside a web component's shadow root.
|
|
209
|
+
|
|
210
|
+
Shadow roots are handled for you — every selector and every `ref` now resolves
|
|
211
|
+
through open shadow roots, so anything `snapshot` shows you is something you can
|
|
212
|
+
click. (It used to show you elements no click could reach: the snapshot walked
|
|
213
|
+
shadow roots, the actions did not.)
|
|
214
|
+
|
|
215
|
+
Frames are opt-in, because reaching into one is a decision:
|
|
216
|
+
|
|
217
|
+
```jsonc
|
|
218
|
+
frames_list {} // what frames exist, and their URLs
|
|
219
|
+
click { "selector": "#pay", "allFrames": true } // find it in whichever frame has it
|
|
220
|
+
get_text { "frameId": 7 } // pin one frame
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Every frame is authorized against **its own** URL before anything runs in it, so
|
|
224
|
+
an allowlisted page embedding a third-party iframe does not become a way to read
|
|
225
|
+
that third party. Frames whose origin isn't on your allowlist are skipped.
|
|
226
|
+
|
|
227
|
+
### Seeing why a page broke — `console_logs`, `network_log`, `dialogs`
|
|
228
|
+
|
|
229
|
+
Reading the DOM tells you what a page looks like after it failed, not why. With
|
|
230
|
+
`--enable-observers`, an in-page hook records console output, uncaught errors,
|
|
231
|
+
and `fetch`/`XMLHttpRequest` traffic, and intercepts native dialogs:
|
|
232
|
+
|
|
233
|
+
```jsonc
|
|
234
|
+
console_logs { "level": "error" } // the exception the page swallowed
|
|
235
|
+
network_log { "failedOnly": true } // the 500 behind the blank screen
|
|
236
|
+
dialogs { "policy": "accept" } // answer confirm() with true from here on
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
It is **off by default and deliberately so**: the hook patches `console`,
|
|
240
|
+
`fetch`, `XMLHttpRequest` and the dialog functions on every allowlisted page in
|
|
241
|
+
your real browser. When it's on, it is registered only for the domains on your
|
|
242
|
+
allowlist, at document_start (so it catches load-time failures), and nothing it
|
|
243
|
+
records leaves the page until a tool call reads it — through the same gate as any
|
|
244
|
+
other page read.
|
|
245
|
+
|
|
246
|
+
Dialog interception is also a fix, not just an observation: `alert`/`confirm`/
|
|
247
|
+
`beforeunload` block the renderer, so a click that opened one used to hang every
|
|
248
|
+
injected script until the command timed out and reported `TIMEOUT` with nothing
|
|
249
|
+
to point at. With observers on, the dialog is answered (`dismiss` by default:
|
|
250
|
+
confirm → false, prompt → null) and recorded.
|
|
251
|
+
|
|
252
|
+
> **What `network_log` sees:** the requests page code makes — `fetch` and
|
|
253
|
+
> `XMLHttpRequest`, with method, URL, status and duration — plus Resource Timing
|
|
254
|
+
> entries (scripts, images, styles) when you ask for them. Not the document
|
|
255
|
+
> request, redirects, or headers. That is the cost of not holding a debugger
|
|
256
|
+
> session open on your browser.
|
|
257
|
+
|
|
258
|
+
### Only what changed — `snapshot { diff: true }`
|
|
259
|
+
|
|
260
|
+
A snapshot is the most expensive read in the tool surface, and the loop that uses
|
|
261
|
+
it most (snapshot → click → snapshot) re-sends a page that is mostly identical
|
|
262
|
+
every time. Ask for the delta instead:
|
|
263
|
+
|
|
264
|
+
```jsonc
|
|
265
|
+
snapshot { "diff": true } // added / removed / changed only
|
|
266
|
+
click { "selector": "#save", "snapshotAfter": true } // what the click changed
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Nodes are matched across snapshots by role + accessible name, not by `ref` —
|
|
270
|
+
refs renumber in document order on every snapshot, so diffing on them would
|
|
271
|
+
report an unchanged button as removed-and-re-added the moment anything above it
|
|
272
|
+
appears.
|
|
273
|
+
|
|
274
|
+
### Targeting by role and name
|
|
275
|
+
|
|
276
|
+
Actions accept a locator instead of a CSS selector, so you don't need a snapshot
|
|
277
|
+
first just to learn a ref:
|
|
278
|
+
|
|
279
|
+
```jsonc
|
|
280
|
+
click { "role": "button", "name": "Sign in" }
|
|
281
|
+
type { "role": "textbox", "name": "Email", "text": "a@b.com" }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Resolution is server-side and refuses to guess: an ambiguous locator fails with
|
|
285
|
+
the candidates listed rather than acting on the first one (pass `nth` to pick).
|
|
286
|
+
|
|
287
|
+
### Printing — `print_pdf`
|
|
288
|
+
|
|
289
|
+
```jsonc
|
|
290
|
+
print_pdf { "landscape": true }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Renders through Chrome's own print pipeline and saves to the task's `results/`
|
|
294
|
+
dir, returning the path and size. The bytes themselves are never returned — a
|
|
295
|
+
PDF is megabytes of base64 no model can read.
|
|
296
|
+
|
|
204
297
|
## Status
|
|
205
298
|
|
|
206
299
|
v0.5.0 — **safe multi-tab concurrency.** Adds the `batch` fan-out tool, makes
|
|
@@ -261,8 +354,31 @@ status badge, and a stable pairing token (`--persist-token`).
|
|
|
261
354
|
chrome-mcp --allow-domain example.com --enable-mutations
|
|
262
355
|
chrome-mcp --policy ./policy.json # see policy.example.json
|
|
263
356
|
chrome-mcp --unsafe-all-domains # loud footgun
|
|
357
|
+
chrome-mcp --enable-observers # console/network/dialog capture (patches page globals)
|
|
358
|
+
chrome-mcp --redact # scrub secret-shaped strings out of page reads
|
|
264
359
|
```
|
|
265
360
|
|
|
361
|
+
**What comes back is gated too.** The allowlist decides which pages may be read;
|
|
362
|
+
it says nothing about what is on them. A logged-in page routinely renders a
|
|
363
|
+
session token into a script tag or an API key onto a settings screen.
|
|
364
|
+
|
|
365
|
+
- **Password field values are always suppressed** — in `get_html`, and in
|
|
366
|
+
`snapshot`, where the field still appears (so you can type into it) flagged
|
|
367
|
+
`secret: true` with no value. No flag, no opt-in: nobody wants those characters.
|
|
368
|
+
- `--redact` additionally scrubs secret-shaped strings — JWTs, AWS/GitHub/Slack/
|
|
369
|
+
Google keys, `Bearer` headers, private-key blocks — out of `get_text`,
|
|
370
|
+
`get_html`, `read_as_markdown` and `eval`. It is opt-in because a pattern will
|
|
371
|
+
eventually fire on something you actually wanted. `--redact-pattern <regex>`
|
|
372
|
+
adds your own (and implies `--redact`); an invalid one fails at startup rather
|
|
373
|
+
than silently never matching.
|
|
374
|
+
- Redaction runs **before** the output cap, so a truncated read cannot leak what
|
|
375
|
+
a full one would have hidden.
|
|
376
|
+
|
|
377
|
+
Every call is recorded to the task's `history.jsonl` with the URL it touched, the
|
|
378
|
+
policy verdict (`allowed`/`denied`), how long it took, how many bytes came back,
|
|
379
|
+
and how many secrets were scrubbed — so "what did the agent do in my browser" has
|
|
380
|
+
an answer after the fact.
|
|
381
|
+
|
|
266
382
|
The per-boot 256-bit token in `~/.chrome-mcp/handshake.json` (mode 0600) is the
|
|
267
383
|
only trust boundary; it is never written to stdout/stderr. On POSIX the mode is
|
|
268
384
|
re-verified after every write and the server **fails closed** if the file ends up
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shared/observers.ts — the record shapes produced by the in-page observer hook
|
|
3
|
+
* (`extension/src/page/hook.ts`) and consumed by the `console_logs`,
|
|
4
|
+
* `network_log` and `dialogs` tools.
|
|
5
|
+
*
|
|
6
|
+
* Why an in-page hook rather than the CDP `Log`/`Console`/`Network` domains: the
|
|
7
|
+
* executor attaches `chrome.debugger` for the duration of ONE op and detaches in
|
|
8
|
+
* `finally` (that is what keeps the "being debugged" banner off the user's
|
|
9
|
+
* browser and keeps other CDP clients working). Observing console and network
|
|
10
|
+
* needs a session that spans commands, which that model cannot provide. A
|
|
11
|
+
* MAIN-world hook installed at document_start costs no attach, survives the
|
|
12
|
+
* service worker being recycled (the buffers live in the page), and captures
|
|
13
|
+
* exactly what page code did — which is what an agent debugging a page is
|
|
14
|
+
* asking about.
|
|
15
|
+
*
|
|
16
|
+
* The trade-off, stated plainly: `network_log` sees `fetch` and
|
|
17
|
+
* `XMLHttpRequest` — the calls page code makes — plus whatever the Resource
|
|
18
|
+
* Timing API reports for everything else. It does not see the document request,
|
|
19
|
+
* redirects, or request/response headers.
|
|
20
|
+
*/
|
|
21
|
+
import type { WirePolicy } from './protocol';
|
|
22
|
+
/** Bumped when the hook's on-page state shape changes. */
|
|
23
|
+
export declare const OBSERVER_HOOK_VERSION: 1;
|
|
24
|
+
/** The MAIN-world global the hook installs its state on. */
|
|
25
|
+
export declare const OBSERVER_GLOBAL: "__chromeMcpObservers";
|
|
26
|
+
/** Ring-buffer ceilings, enforced in-page so a chatty site cannot grow unbounded. */
|
|
27
|
+
export declare const MAX_CONSOLE_ENTRIES = 500;
|
|
28
|
+
export declare const MAX_NETWORK_ENTRIES = 300;
|
|
29
|
+
export declare const MAX_DIALOG_ENTRIES = 50;
|
|
30
|
+
export type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug' | 'exception';
|
|
31
|
+
export interface ConsoleEntry {
|
|
32
|
+
/** Monotonic per-page sequence, so a caller can poll for "what is new". */
|
|
33
|
+
seq: number;
|
|
34
|
+
ts: number;
|
|
35
|
+
level: ConsoleLevel;
|
|
36
|
+
text: string;
|
|
37
|
+
/** Present for `exception`: the error's stack, trimmed. */
|
|
38
|
+
stack?: string;
|
|
39
|
+
url?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface NetworkEntry {
|
|
42
|
+
seq: number;
|
|
43
|
+
ts: number;
|
|
44
|
+
/** 'fetch' | 'xhr' | 'resource' (the Resource Timing fallback). */
|
|
45
|
+
via: 'fetch' | 'xhr' | 'resource';
|
|
46
|
+
method: string;
|
|
47
|
+
url: string;
|
|
48
|
+
status?: number;
|
|
49
|
+
ok?: boolean;
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
/** Set when the request rejected (network error, CORS, abort). */
|
|
52
|
+
error?: string;
|
|
53
|
+
/** Resource Timing only: the initiator type Chrome reports (script, img, css…). */
|
|
54
|
+
initiator?: string;
|
|
55
|
+
bytes?: number;
|
|
56
|
+
}
|
|
57
|
+
export type DialogKind = 'alert' | 'confirm' | 'prompt' | 'beforeunload';
|
|
58
|
+
export interface DialogEntry {
|
|
59
|
+
seq: number;
|
|
60
|
+
ts: number;
|
|
61
|
+
kind: DialogKind;
|
|
62
|
+
message: string;
|
|
63
|
+
/** What the hook answered on the page's behalf. */
|
|
64
|
+
answered: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* How the hook answers a native dialog. `dismiss` is the default: it is what a
|
|
68
|
+
* blocked renderer would eventually be told anyway, and it never confirms a
|
|
69
|
+
* destructive action the agent did not ask for. `accept` is opt-in per tab.
|
|
70
|
+
*/
|
|
71
|
+
export type DialogPolicy = 'dismiss' | 'accept';
|
|
72
|
+
export interface ObserverReadResult {
|
|
73
|
+
/** Absent hook (observers disabled, or the page loaded before it registered). */
|
|
74
|
+
installed: boolean;
|
|
75
|
+
hookVersion?: number;
|
|
76
|
+
console?: ConsoleEntry[];
|
|
77
|
+
network?: NetworkEntry[];
|
|
78
|
+
dialogs?: DialogEntry[];
|
|
79
|
+
dialogPolicy?: DialogPolicy;
|
|
80
|
+
/** True when the ring buffer dropped older entries before this read. */
|
|
81
|
+
dropped?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Runs IN THE PAGE (MAIN world), serialized to source — so it must be
|
|
85
|
+
* self-contained and takes the global's name rather than importing the constant.
|
|
86
|
+
*
|
|
87
|
+
* Draining is explicit: `clear` empties the buffers it returned, and `sinceSeq`
|
|
88
|
+
* lets a caller poll for only what is new. Nothing here reaches out; it reads
|
|
89
|
+
* what the hook already recorded on this page.
|
|
90
|
+
*/
|
|
91
|
+
export declare function readObservers(globalName: string, opts: {
|
|
92
|
+
console?: boolean;
|
|
93
|
+
network?: boolean;
|
|
94
|
+
dialogs?: boolean;
|
|
95
|
+
sinceSeq?: number;
|
|
96
|
+
limit?: number;
|
|
97
|
+
clear?: boolean;
|
|
98
|
+
setPolicy?: string | null;
|
|
99
|
+
promptText?: string | null;
|
|
100
|
+
includeResources?: boolean;
|
|
101
|
+
}): unknown;
|
|
102
|
+
/**
|
|
103
|
+
* Translate the domain allowlist into Chrome match patterns for registering the
|
|
104
|
+
* hook as a content script. A bare host becomes `*://host/*`; `*.host` becomes
|
|
105
|
+
* `*://*.host/*` (which Chrome reads as the host AND its subdomains, matching
|
|
106
|
+
* how the allowlist itself behaves); `*` becomes `<all_urls>`. Entries carrying
|
|
107
|
+
* no host are dropped rather than widened — the page the tool may not read is
|
|
108
|
+
* also the page it must not instrument.
|
|
109
|
+
*/
|
|
110
|
+
export declare function observerMatches(policy: WirePolicy): string[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* shared/observers.ts — the record shapes produced by the in-page observer hook
|
|
4
|
+
* (`extension/src/page/hook.ts`) and consumed by the `console_logs`,
|
|
5
|
+
* `network_log` and `dialogs` tools.
|
|
6
|
+
*
|
|
7
|
+
* Why an in-page hook rather than the CDP `Log`/`Console`/`Network` domains: the
|
|
8
|
+
* executor attaches `chrome.debugger` for the duration of ONE op and detaches in
|
|
9
|
+
* `finally` (that is what keeps the "being debugged" banner off the user's
|
|
10
|
+
* browser and keeps other CDP clients working). Observing console and network
|
|
11
|
+
* needs a session that spans commands, which that model cannot provide. A
|
|
12
|
+
* MAIN-world hook installed at document_start costs no attach, survives the
|
|
13
|
+
* service worker being recycled (the buffers live in the page), and captures
|
|
14
|
+
* exactly what page code did — which is what an agent debugging a page is
|
|
15
|
+
* asking about.
|
|
16
|
+
*
|
|
17
|
+
* The trade-off, stated plainly: `network_log` sees `fetch` and
|
|
18
|
+
* `XMLHttpRequest` — the calls page code makes — plus whatever the Resource
|
|
19
|
+
* Timing API reports for everything else. It does not see the document request,
|
|
20
|
+
* redirects, or request/response headers.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MAX_DIALOG_ENTRIES = exports.MAX_NETWORK_ENTRIES = exports.MAX_CONSOLE_ENTRIES = exports.OBSERVER_GLOBAL = exports.OBSERVER_HOOK_VERSION = void 0;
|
|
24
|
+
exports.readObservers = readObservers;
|
|
25
|
+
exports.observerMatches = observerMatches;
|
|
26
|
+
const policy_1 = require("./policy");
|
|
27
|
+
/** Bumped when the hook's on-page state shape changes. */
|
|
28
|
+
exports.OBSERVER_HOOK_VERSION = 1;
|
|
29
|
+
/** The MAIN-world global the hook installs its state on. */
|
|
30
|
+
exports.OBSERVER_GLOBAL = '__chromeMcpObservers';
|
|
31
|
+
/** Ring-buffer ceilings, enforced in-page so a chatty site cannot grow unbounded. */
|
|
32
|
+
exports.MAX_CONSOLE_ENTRIES = 500;
|
|
33
|
+
exports.MAX_NETWORK_ENTRIES = 300;
|
|
34
|
+
exports.MAX_DIALOG_ENTRIES = 50;
|
|
35
|
+
/**
|
|
36
|
+
* Runs IN THE PAGE (MAIN world), serialized to source — so it must be
|
|
37
|
+
* self-contained and takes the global's name rather than importing the constant.
|
|
38
|
+
*
|
|
39
|
+
* Draining is explicit: `clear` empties the buffers it returned, and `sinceSeq`
|
|
40
|
+
* lets a caller poll for only what is new. Nothing here reaches out; it reads
|
|
41
|
+
* what the hook already recorded on this page.
|
|
42
|
+
*/
|
|
43
|
+
function readObservers(globalName, opts) {
|
|
44
|
+
const state = window[globalName];
|
|
45
|
+
if (!state)
|
|
46
|
+
return { installed: false };
|
|
47
|
+
if (opts.setPolicy === 'dismiss' || opts.setPolicy === 'accept')
|
|
48
|
+
state.dialogPolicy = opts.setPolicy;
|
|
49
|
+
if (typeof opts.promptText === 'string')
|
|
50
|
+
state.promptText = opts.promptText;
|
|
51
|
+
const since = typeof opts.sinceSeq === 'number' ? opts.sinceSeq : 0;
|
|
52
|
+
const limit = typeof opts.limit === 'number' && opts.limit > 0 ? opts.limit : 200;
|
|
53
|
+
const take = (buf) => {
|
|
54
|
+
const picked = buf.filter((e) => e.seq > since);
|
|
55
|
+
return picked.length > limit ? picked.slice(picked.length - limit) : picked;
|
|
56
|
+
};
|
|
57
|
+
// Resource Timing covers what the fetch/XHR patches structurally cannot see
|
|
58
|
+
// (documents, scripts, images, styles). It carries no status code, so those
|
|
59
|
+
// entries report timing and size only — stated, not implied.
|
|
60
|
+
if (opts.network && opts.includeResources) {
|
|
61
|
+
try {
|
|
62
|
+
const entries = performance.getEntriesByType('resource');
|
|
63
|
+
for (let i = state.resourceCursor; i < entries.length; i++) {
|
|
64
|
+
const r = entries[i];
|
|
65
|
+
state.network.push({
|
|
66
|
+
seq: ++state.seq,
|
|
67
|
+
ts: Math.round(performance.timeOrigin + r.startTime),
|
|
68
|
+
via: 'resource',
|
|
69
|
+
method: 'GET',
|
|
70
|
+
url: r.name.length > 2000 ? `${r.name.slice(0, 2000)}…` : r.name,
|
|
71
|
+
durationMs: Math.round(r.duration),
|
|
72
|
+
initiator: r.initiatorType,
|
|
73
|
+
bytes: r.transferSize || undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
state.resourceCursor = entries.length;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* Resource Timing unavailable — the fetch/XHR records still stand */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const out = {
|
|
83
|
+
installed: true,
|
|
84
|
+
hookVersion: state.v,
|
|
85
|
+
dialogPolicy: state.dialogPolicy,
|
|
86
|
+
dropped: state.dropped,
|
|
87
|
+
};
|
|
88
|
+
if (opts.console)
|
|
89
|
+
out.console = take(state.console);
|
|
90
|
+
if (opts.network)
|
|
91
|
+
out.network = take(state.network);
|
|
92
|
+
if (opts.dialogs)
|
|
93
|
+
out.dialogs = take(state.dialogs);
|
|
94
|
+
if (opts.clear) {
|
|
95
|
+
if (opts.console)
|
|
96
|
+
state.console.length = 0;
|
|
97
|
+
if (opts.network)
|
|
98
|
+
state.network.length = 0;
|
|
99
|
+
if (opts.dialogs)
|
|
100
|
+
state.dialogs.length = 0;
|
|
101
|
+
state.dropped = false;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Translate the domain allowlist into Chrome match patterns for registering the
|
|
107
|
+
* hook as a content script. A bare host becomes `*://host/*`; `*.host` becomes
|
|
108
|
+
* `*://*.host/*` (which Chrome reads as the host AND its subdomains, matching
|
|
109
|
+
* how the allowlist itself behaves); `*` becomes `<all_urls>`. Entries carrying
|
|
110
|
+
* no host are dropped rather than widened — the page the tool may not read is
|
|
111
|
+
* also the page it must not instrument.
|
|
112
|
+
*/
|
|
113
|
+
function observerMatches(policy) {
|
|
114
|
+
if (policy.allowObservers !== true)
|
|
115
|
+
return [];
|
|
116
|
+
const out = new Set();
|
|
117
|
+
for (const raw of policy.allowDomains ?? []) {
|
|
118
|
+
const p = (0, policy_1.normalizeDomainPattern)(raw);
|
|
119
|
+
if (!p)
|
|
120
|
+
continue;
|
|
121
|
+
if (p === '*')
|
|
122
|
+
return ['<all_urls>'];
|
|
123
|
+
out.add(`*://${p}/*`);
|
|
124
|
+
}
|
|
125
|
+
return [...out];
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=observers.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shared/page-fns.ts — the ONE function that runs inside a page for every
|
|
3
|
+
* DOM-touching command.
|
|
4
|
+
*
|
|
5
|
+
* MUST be self-contained: `chrome.scripting.executeScript` serializes it to
|
|
6
|
+
* source, so it may not close over anything from this module. That constraint is
|
|
7
|
+
* exactly why every op lives in one function instead of ten — the shared
|
|
8
|
+
* helpers below (`deepQuery` above all) can then be written once and are
|
|
9
|
+
* automatically used by every op.
|
|
10
|
+
*
|
|
11
|
+
* `deepQuery` is the reason this file exists. `snapshot` deliberately walks open
|
|
12
|
+
* shadow roots and stamps `data-mcp-ref` on what it finds there, but every
|
|
13
|
+
* action used to resolve that ref with a plain `document.querySelector`, which
|
|
14
|
+
* cannot cross a shadow boundary. So the snapshot advertised elements that no
|
|
15
|
+
* click could ever reach — a structural dead end on every web-component site.
|
|
16
|
+
* One resolver, used by every op, is what closes it.
|
|
17
|
+
*/
|
|
18
|
+
/** Ops the page-side dispatcher understands. */
|
|
19
|
+
export type PageOpName = 'probe' | 'text' | 'html' | 'click' | 'type' | 'focus' | 'point' | 'hover' | 'select' | 'measure' | 'scroll' | 'storage' | 'waitSelector' | 'waitFor';
|
|
20
|
+
export interface PageOpArgs {
|
|
21
|
+
op: PageOpName;
|
|
22
|
+
selector?: string | null;
|
|
23
|
+
text?: string;
|
|
24
|
+
clear?: boolean;
|
|
25
|
+
outer?: boolean;
|
|
26
|
+
values?: string[];
|
|
27
|
+
x?: number | null;
|
|
28
|
+
y?: number | null;
|
|
29
|
+
deltaX?: number | null;
|
|
30
|
+
deltaY?: number | null;
|
|
31
|
+
storageOp?: string;
|
|
32
|
+
key?: string | null;
|
|
33
|
+
value?: string | null;
|
|
34
|
+
session?: boolean;
|
|
35
|
+
textContains?: string | null;
|
|
36
|
+
gone?: boolean;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
interval?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs IN THE PAGE (or in one frame of it). Returns a plain JSON-able object;
|
|
42
|
+
* `found: false` means the selector matched nothing, which the caller renders as
|
|
43
|
+
* SELECTOR_NOT_FOUND. Never throws across the boundary — a page that blows up
|
|
44
|
+
* inside an op is reported as `{ ok: false, error }`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pageOp(a: PageOpArgs): unknown;
|