@mehmoodqureshi/chrome-mcp 0.6.6 → 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/cli.js +13 -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 +45 -1
- package/dist/src/executor/stub-executor.js +53 -8
- 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/batch.js +61 -9
- package/dist/src/mcp/helpers.d.ts +4 -4
- package/dist/src/mcp/helpers.js +9 -4
- package/dist/src/mcp/limits.d.ts +43 -0
- package/dist/src/mcp/limits.js +87 -0
- package/dist/src/mcp/locate.d.ts +45 -0
- package/dist/src/mcp/locate.js +105 -0
- package/dist/src/mcp/log.d.ts +17 -0
- package/dist/src/mcp/log.js +43 -0
- package/dist/src/mcp/redact.d.ts +48 -0
- package/dist/src/mcp/redact.js +92 -0
- package/dist/src/mcp/server.d.ts +1 -2
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/snapdiff.d.ts +43 -0
- package/dist/src/mcp/snapdiff.js +92 -0
- package/dist/src/mcp/tools.js +481 -41
- 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
|
@@ -22,6 +22,15 @@ const workspace_1 = require("../bridge/workspace");
|
|
|
22
22
|
* pre-check precision for half the traffic — never enforcement itself.
|
|
23
23
|
*/
|
|
24
24
|
const ACTIVE_URL_TTL_MS = 1_000;
|
|
25
|
+
/** Flatten frame options into the params a wire command carries. */
|
|
26
|
+
function frameParams(o) {
|
|
27
|
+
if (!o)
|
|
28
|
+
return {};
|
|
29
|
+
return {
|
|
30
|
+
...(o.frameId !== undefined ? { frameId: o.frameId } : {}),
|
|
31
|
+
...(o.allFrames ? { allFrames: true } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
25
34
|
/** Flatten a Target into the params a wire command carries. */
|
|
26
35
|
function targetParams(t) {
|
|
27
36
|
if (!t)
|
|
@@ -107,36 +116,36 @@ class ExtensionExecutor {
|
|
|
107
116
|
}
|
|
108
117
|
// -- interaction --------------------------------------------------------
|
|
109
118
|
async click(t, opts) {
|
|
110
|
-
return (await this.send('click', { ...targetParams(t), button: opts?.button, clickCount: opts?.clickCount, trusted: opts?.trusted }, { tabId: opts?.tabId }));
|
|
119
|
+
return (await this.send('click', { ...targetParams(t), ...frameParams(opts), button: opts?.button, clickCount: opts?.clickCount, trusted: opts?.trusted }, { tabId: opts?.tabId }));
|
|
111
120
|
}
|
|
112
121
|
async type(t, text, opts) {
|
|
113
|
-
return (await this.send('type', { ...targetParams(t), text, clear: opts?.clear, pressEnter: opts?.pressEnter, keyEvents: opts?.keyEvents, trusted: opts?.trusted }, { tabId: opts?.tabId }));
|
|
122
|
+
return (await this.send('type', { ...targetParams(t), ...frameParams(opts), text, clear: opts?.clear, pressEnter: opts?.pressEnter, keyEvents: opts?.keyEvents, trusted: opts?.trusted }, { tabId: opts?.tabId }));
|
|
114
123
|
}
|
|
115
124
|
async selectOption(t, values, opts) {
|
|
116
|
-
return (await this.send('select_option', { ...targetParams(t), values }, { tabId: opts?.tabId }));
|
|
125
|
+
return (await this.send('select_option', { ...targetParams(t), ...frameParams(opts), values }, { tabId: opts?.tabId }));
|
|
117
126
|
}
|
|
118
127
|
async fill(t, value, opts) {
|
|
119
128
|
// No dedicated wire method: a cleared insertText is the fill primitive.
|
|
120
|
-
return (await this.send('type', { ...targetParams(t), text: value, clear: true, keyEvents: false }, { tabId: opts?.tabId }));
|
|
129
|
+
return (await this.send('type', { ...targetParams(t), ...frameParams(opts), text: value, clear: true, keyEvents: false }, { tabId: opts?.tabId }));
|
|
121
130
|
}
|
|
122
131
|
async press(key, opts) {
|
|
123
132
|
return (await this.send('press', { key, modifiers: opts?.modifiers }, { tabId: opts?.tabId }));
|
|
124
133
|
}
|
|
125
134
|
async hover(t, opts) {
|
|
126
|
-
return (await this.send('hover', { ...targetParams(t) }, { tabId: opts?.tabId }));
|
|
135
|
+
return (await this.send('hover', { ...targetParams(t), ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
127
136
|
}
|
|
128
137
|
async scroll(opts) {
|
|
129
|
-
return (await this.send('scroll', { x: opts.x, y: opts.y, deltaX: opts.deltaX, deltaY: opts.deltaY, ...targetParams(opts.target) }, { tabId: opts.tabId }));
|
|
138
|
+
return (await this.send('scroll', { x: opts.x, y: opts.y, deltaX: opts.deltaX, deltaY: opts.deltaY, ...targetParams(opts.target), ...frameParams(opts) }, { tabId: opts.tabId }));
|
|
130
139
|
}
|
|
131
140
|
// -- read ---------------------------------------------------------------
|
|
132
141
|
async getText(t, opts) {
|
|
133
|
-
return (await this.send('get_text', { ...targetParams(t) }, { tabId: opts?.tabId }));
|
|
142
|
+
return (await this.send('get_text', { ...targetParams(t), ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
134
143
|
}
|
|
135
144
|
async getHtml(t, opts) {
|
|
136
|
-
return (await this.send('get_html', { ...targetParams(t), outer: opts?.outer }, { tabId: opts?.tabId }));
|
|
145
|
+
return (await this.send('get_html', { ...targetParams(t), ...frameParams(opts), outer: opts?.outer }, { tabId: opts?.tabId }));
|
|
137
146
|
}
|
|
138
147
|
async snapshot(opts) {
|
|
139
|
-
return (await this.send('snapshot', { interactiveOnly: opts?.interactiveOnly, max: opts?.max }, { tabId: opts?.tabId }));
|
|
148
|
+
return (await this.send('snapshot', { interactiveOnly: opts?.interactiveOnly, max: opts?.max, ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
140
149
|
}
|
|
141
150
|
async getCookies(opts) {
|
|
142
151
|
return (await this.send('get_cookies', { url: opts?.url }, { tabId: opts?.tabId }));
|
|
@@ -145,14 +154,14 @@ class ExtensionExecutor {
|
|
|
145
154
|
return (await this.send('storage', { op: args.op, key: args.key, value: args.value, session: args.session }, { tabId: args.tabId }));
|
|
146
155
|
}
|
|
147
156
|
async screenshot(opts) {
|
|
148
|
-
return (await this.send('screenshot', { fullPage: opts?.fullPage, ...targetParams(opts?.target) }, { tabId: opts?.tabId }));
|
|
157
|
+
return (await this.send('screenshot', { fullPage: opts?.fullPage, ...targetParams(opts?.target), ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
149
158
|
}
|
|
150
159
|
async eval(expression, opts) {
|
|
151
|
-
const result = (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise }, { tabId: opts?.tabId }));
|
|
160
|
+
const result = (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise, ...frameParams(opts) }, { tabId: opts?.tabId }));
|
|
152
161
|
return (0, types_1.truncateEvalResult)(result);
|
|
153
162
|
}
|
|
154
163
|
async waitFor(opts) {
|
|
155
|
-
return (await this.send('wait_for', { selector: opts.selector, textContains: opts.textContains, gone: opts.gone, timeoutMs: opts.timeoutMs }, { tabId: opts.tabId, timeoutMs: opts.timeoutMs ? opts.timeoutMs + 5_000 : undefined }));
|
|
164
|
+
return (await this.send('wait_for', { selector: opts.selector, textContains: opts.textContains, gone: opts.gone, timeoutMs: opts.timeoutMs, ...frameParams(opts) }, { tabId: opts.tabId, timeoutMs: opts.timeoutMs ? opts.timeoutMs + 5_000 : undefined }));
|
|
156
165
|
}
|
|
157
166
|
// -- privileged ---------------------------------------------------------
|
|
158
167
|
async download(args) {
|
|
@@ -176,6 +185,38 @@ class ExtensionExecutor {
|
|
|
176
185
|
async uploadFile(t, files, opts) {
|
|
177
186
|
return (await this.send('upload_file', { ...targetParams(t), files }, { tabId: opts?.tabId }));
|
|
178
187
|
}
|
|
188
|
+
// -- optional capabilities ----------------------------------------------
|
|
189
|
+
async framesList(opts) {
|
|
190
|
+
const res = (await this.send('frames_list', {}, { tabId: opts?.tabId }));
|
|
191
|
+
return res.frames ?? [];
|
|
192
|
+
}
|
|
193
|
+
async observers(args) {
|
|
194
|
+
return (await this.send('observers', {
|
|
195
|
+
...frameParams(args),
|
|
196
|
+
console: args.console,
|
|
197
|
+
network: args.network,
|
|
198
|
+
dialogs: args.dialogs,
|
|
199
|
+
sinceSeq: args.sinceSeq,
|
|
200
|
+
limit: args.limit,
|
|
201
|
+
clear: args.clear,
|
|
202
|
+
setPolicy: args.setPolicy,
|
|
203
|
+
promptText: args.promptText,
|
|
204
|
+
includeResources: args.includeResources,
|
|
205
|
+
}, { tabId: args.tabId }));
|
|
206
|
+
}
|
|
207
|
+
async printPdf(opts) {
|
|
208
|
+
return (await this.send('print_pdf', {
|
|
209
|
+
landscape: opts?.landscape,
|
|
210
|
+
printBackground: opts?.printBackground,
|
|
211
|
+
scale: opts?.scale,
|
|
212
|
+
paperWidth: opts?.paperWidth,
|
|
213
|
+
paperHeight: opts?.paperHeight,
|
|
214
|
+
pageRanges: opts?.pageRanges,
|
|
215
|
+
preferCSSPageSize: opts?.preferCSSPageSize,
|
|
216
|
+
},
|
|
217
|
+
// A large page can take a while through Chrome's print pipeline.
|
|
218
|
+
{ tabId: opts?.tabId, timeoutMs: 60_000 }));
|
|
219
|
+
}
|
|
179
220
|
}
|
|
180
221
|
exports.ExtensionExecutor = ExtensionExecutor;
|
|
181
222
|
//# sourceMappingURL=extension-executor.js.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 2. Drives the dispatch/policy/envelope tests with deterministic, canned
|
|
8
8
|
* values (and a couple of forced-failure switches).
|
|
9
9
|
*/
|
|
10
|
-
import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type NavResult, type ScreenshotResult, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult } from './types';
|
|
10
|
+
import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type FrameInfo, type NavResult, type ObserverArgs, type ObserverReadResult, type PdfResult, type ScreenshotResult, type SnapshotNode, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult } from './types';
|
|
11
11
|
export interface StubOptions {
|
|
12
12
|
/** URL of the (single) active tab — used to exercise the domain policy gate. */
|
|
13
13
|
activeUrl?: string;
|
|
@@ -32,6 +32,31 @@ export interface StubOptions {
|
|
|
32
32
|
/** When true, tabs exist but none is flagged active — the case the gate used to
|
|
33
33
|
* paper over by silently gating against `tabs[0]`. */
|
|
34
34
|
noActiveTab?: boolean;
|
|
35
|
+
/** Text returned by `getText`. Set it large to exercise the output cap. */
|
|
36
|
+
textPayload?: string;
|
|
37
|
+
/** HTML returned by `getHtml`. Set it large to exercise the output cap. */
|
|
38
|
+
htmlPayload?: string;
|
|
39
|
+
/**
|
|
40
|
+
* How many of the first content reads reject with `EXTENSION_DISCONNECTED`
|
|
41
|
+
* before one succeeds — models MV3 recycling the service worker mid-command,
|
|
42
|
+
* the fault the dispatch layer retries once.
|
|
43
|
+
*/
|
|
44
|
+
disconnectReads?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Same, but for the mutating `type` path. Mutations are deliberately NOT
|
|
47
|
+
* retried — repeating a write could submit a form twice — so a test can assert
|
|
48
|
+
* exactly one attempt was made.
|
|
49
|
+
*/
|
|
50
|
+
disconnectWrites?: number;
|
|
51
|
+
/** Nodes the stub snapshot reports — lets a test drive locator resolution and
|
|
52
|
+
* the snapshot diff without a browser. Mutate between calls to model a page
|
|
53
|
+
* changing under an action. */
|
|
54
|
+
snapshotNodes?: SnapshotNode[];
|
|
55
|
+
/** Frames the stub reports for `frames_list`. */
|
|
56
|
+
frames?: FrameInfo[];
|
|
57
|
+
/** What the in-page observers return. Absent = the hook is not installed,
|
|
58
|
+
* which is the case the tools must report clearly rather than as an empty list. */
|
|
59
|
+
observers?: ObserverReadResult;
|
|
35
60
|
}
|
|
36
61
|
export declare class StubExecutor implements Executor {
|
|
37
62
|
readonly backend: BackendKind;
|
|
@@ -39,15 +64,31 @@ export declare class StubExecutor implements Executor {
|
|
|
39
64
|
private readonly evalThrows;
|
|
40
65
|
private readonly tabsListThrows;
|
|
41
66
|
private readonly noTabs;
|
|
67
|
+
private readonly textPayload;
|
|
68
|
+
private readonly htmlPayload;
|
|
69
|
+
private remainingDisconnects;
|
|
70
|
+
private remainingWriteDisconnects;
|
|
42
71
|
private readonly blankTabUrl;
|
|
43
72
|
private readonly cached;
|
|
44
73
|
private readonly backgroundTabs;
|
|
45
74
|
private readonly noActiveTab;
|
|
75
|
+
/** Mutable so a test can change the page between two snapshots. */
|
|
76
|
+
snapshotNodes: SnapshotNode[];
|
|
77
|
+
private readonly frames;
|
|
78
|
+
private readonly observerState?;
|
|
79
|
+
/** The last observer args received, so a test can assert what was requested. */
|
|
80
|
+
lastObserverArgs?: ObserverArgs;
|
|
46
81
|
/** How many times the gate actually asked for the tab list — the round-trip
|
|
47
82
|
* counter the caching path exists to keep at zero. */
|
|
48
83
|
tabsListCalls: number;
|
|
49
84
|
private ready;
|
|
50
85
|
constructor(opts?: StubOptions);
|
|
86
|
+
/** Fail this read if a scripted disconnect is still pending, then consume it. */
|
|
87
|
+
private maybeDisconnect;
|
|
88
|
+
/** How many scripted disconnects are left (lets a test assert one was consumed). */
|
|
89
|
+
get pendingDisconnects(): number;
|
|
90
|
+
/** Same for the write path — a mutating call must consume exactly one. */
|
|
91
|
+
get pendingWriteDisconnects(): number;
|
|
51
92
|
private tab;
|
|
52
93
|
cachedActiveUrl(): string | null;
|
|
53
94
|
status(): ExecutorStatus;
|
|
@@ -98,4 +139,7 @@ export declare class StubExecutor implements Executor {
|
|
|
98
139
|
suggestedName?: string;
|
|
99
140
|
}): Promise<DownloadResult>;
|
|
100
141
|
uploadFile(): Promise<ActionOk>;
|
|
142
|
+
framesList(): Promise<FrameInfo[]>;
|
|
143
|
+
observers(args: ObserverArgs): Promise<ObserverReadResult>;
|
|
144
|
+
printPdf(): Promise<PdfResult>;
|
|
101
145
|
}
|
|
@@ -20,10 +20,20 @@ class StubExecutor {
|
|
|
20
20
|
evalThrows;
|
|
21
21
|
tabsListThrows;
|
|
22
22
|
noTabs;
|
|
23
|
+
textPayload;
|
|
24
|
+
htmlPayload;
|
|
25
|
+
remainingDisconnects;
|
|
26
|
+
remainingWriteDisconnects;
|
|
23
27
|
blankTabUrl;
|
|
24
28
|
cached;
|
|
25
29
|
backgroundTabs;
|
|
26
30
|
noActiveTab;
|
|
31
|
+
/** Mutable so a test can change the page between two snapshots. */
|
|
32
|
+
snapshotNodes;
|
|
33
|
+
frames;
|
|
34
|
+
observerState;
|
|
35
|
+
/** The last observer args received, so a test can assert what was requested. */
|
|
36
|
+
lastObserverArgs;
|
|
27
37
|
/** How many times the gate actually asked for the tab list — the round-trip
|
|
28
38
|
* counter the caching path exists to keep at zero. */
|
|
29
39
|
tabsListCalls = 0;
|
|
@@ -37,6 +47,28 @@ class StubExecutor {
|
|
|
37
47
|
this.cached = opts.cachedUrl ?? null;
|
|
38
48
|
this.backgroundTabs = opts.backgroundTabs ?? [];
|
|
39
49
|
this.noActiveTab = opts.noActiveTab ?? false;
|
|
50
|
+
this.textPayload = opts.textPayload ?? 'stub text';
|
|
51
|
+
this.htmlPayload = opts.htmlPayload ?? '<html><body><a href="https://example.com">Example</a></body></html>';
|
|
52
|
+
this.remainingDisconnects = opts.disconnectReads ?? 0;
|
|
53
|
+
this.remainingWriteDisconnects = opts.disconnectWrites ?? 0;
|
|
54
|
+
this.snapshotNodes = opts.snapshotNodes ?? [{ ref: 'e1', role: 'link', name: 'Example', tag: 'a' }];
|
|
55
|
+
this.frames = opts.frames ?? [{ frameId: 0, top: true, url: opts.activeUrl ?? 'about:blank', title: 'Stub Page' }];
|
|
56
|
+
this.observerState = opts.observers;
|
|
57
|
+
}
|
|
58
|
+
/** Fail this read if a scripted disconnect is still pending, then consume it. */
|
|
59
|
+
maybeDisconnect() {
|
|
60
|
+
if (this.remainingDisconnects <= 0)
|
|
61
|
+
return;
|
|
62
|
+
this.remainingDisconnects--;
|
|
63
|
+
throw new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'stub: service worker recycled mid-command');
|
|
64
|
+
}
|
|
65
|
+
/** How many scripted disconnects are left (lets a test assert one was consumed). */
|
|
66
|
+
get pendingDisconnects() {
|
|
67
|
+
return this.remainingDisconnects;
|
|
68
|
+
}
|
|
69
|
+
/** Same for the write path — a mutating call must consume exactly one. */
|
|
70
|
+
get pendingWriteDisconnects() {
|
|
71
|
+
return this.remainingWriteDisconnects;
|
|
40
72
|
}
|
|
41
73
|
tab() {
|
|
42
74
|
return {
|
|
@@ -113,6 +145,10 @@ class StubExecutor {
|
|
|
113
145
|
return ok;
|
|
114
146
|
}
|
|
115
147
|
async type() {
|
|
148
|
+
if (this.remainingWriteDisconnects > 0) {
|
|
149
|
+
this.remainingWriteDisconnects--;
|
|
150
|
+
throw new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'stub: service worker recycled mid-command');
|
|
151
|
+
}
|
|
116
152
|
return ok;
|
|
117
153
|
}
|
|
118
154
|
async fill() {
|
|
@@ -131,18 +167,15 @@ class StubExecutor {
|
|
|
131
167
|
return ok;
|
|
132
168
|
}
|
|
133
169
|
async getText(_t) {
|
|
134
|
-
|
|
170
|
+
this.maybeDisconnect();
|
|
171
|
+
return { text: this.textPayload, ref: 'el_stub_1' };
|
|
135
172
|
}
|
|
136
173
|
async getHtml() {
|
|
137
|
-
|
|
174
|
+
this.maybeDisconnect();
|
|
175
|
+
return { html: this.htmlPayload };
|
|
138
176
|
}
|
|
139
177
|
async snapshot() {
|
|
140
|
-
return {
|
|
141
|
-
url: this.url,
|
|
142
|
-
title: 'Stub Page',
|
|
143
|
-
nodes: [{ ref: 'e1', role: 'link', name: 'Example', tag: 'a' }],
|
|
144
|
-
truncated: false,
|
|
145
|
-
};
|
|
178
|
+
return { url: this.url, title: 'Stub Page', nodes: this.snapshotNodes, truncated: false };
|
|
146
179
|
}
|
|
147
180
|
async getCookies() {
|
|
148
181
|
return { cookies: [{ name: 'stub', value: '1', domain: 'example.com', path: '/', secure: true, httpOnly: false }] };
|
|
@@ -174,6 +207,18 @@ class StubExecutor {
|
|
|
174
207
|
async uploadFile() {
|
|
175
208
|
return ok;
|
|
176
209
|
}
|
|
210
|
+
// -- optional capabilities ----------------------------------------------
|
|
211
|
+
async framesList() {
|
|
212
|
+
return this.frames;
|
|
213
|
+
}
|
|
214
|
+
async observers(args) {
|
|
215
|
+
this.lastObserverArgs = args;
|
|
216
|
+
return this.observerState ?? { installed: false };
|
|
217
|
+
}
|
|
218
|
+
async printPdf() {
|
|
219
|
+
// "%PDF-1.4" in base64 — enough for a caller to assert real bytes landed.
|
|
220
|
+
return { dataBase64: 'JVBERi0xLjQK', mimeType: 'application/pdf', url: this.url, title: 'Stub Page' };
|
|
221
|
+
}
|
|
177
222
|
}
|
|
178
223
|
exports.StubExecutor = StubExecutor;
|
|
179
224
|
//# sourceMappingURL=stub-executor.js.map
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Helpers (extract_links / read_as_markdown / fill_form) are composed in
|
|
10
10
|
* `mcp/helpers.ts` from these primitives; only `download` is privileged.
|
|
11
11
|
*/
|
|
12
|
+
import type { ObserverReadResult, DialogPolicy } from '../../shared/observers';
|
|
13
|
+
export type { ObserverReadResult, DialogPolicy };
|
|
12
14
|
export type BackendKind = 'extension' | 'cdp';
|
|
13
15
|
export type WaitUntil = 'load' | 'domcontentloaded' | 'networkidle';
|
|
14
16
|
export type KeyModifier = 'Alt' | 'Control' | 'Meta' | 'Shift';
|
|
@@ -32,6 +34,44 @@ export type Target = {
|
|
|
32
34
|
* reconnect (a mismatch becomes a clean `STALE_TAB`, not a wrong-tab action).
|
|
33
35
|
*/
|
|
34
36
|
export type TabId = string;
|
|
37
|
+
/**
|
|
38
|
+
* Which frame(s) of a tab a call acts on. Omitted = the top frame, which is
|
|
39
|
+
* every call that predates frame support.
|
|
40
|
+
*
|
|
41
|
+
* `frameId` pins one frame (ids come from `framesList`). `allFrames` scans every
|
|
42
|
+
* frame and acts on the first that has the element — the answer to "the button
|
|
43
|
+
* is in the checkout iframe and my selector never matches". Each frame is
|
|
44
|
+
* authorized against ITS OWN url before anything runs there, so a scan can never
|
|
45
|
+
* reach into a frame the allowlist does not cover.
|
|
46
|
+
*/
|
|
47
|
+
export interface FrameOpts {
|
|
48
|
+
frameId?: number;
|
|
49
|
+
allFrames?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface FrameInfo {
|
|
52
|
+
frameId: number;
|
|
53
|
+
top: boolean;
|
|
54
|
+
url: string;
|
|
55
|
+
title: string;
|
|
56
|
+
}
|
|
57
|
+
export interface PdfResult {
|
|
58
|
+
dataBase64: string;
|
|
59
|
+
mimeType: 'application/pdf';
|
|
60
|
+
url: string;
|
|
61
|
+
title: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ObserverArgs extends FrameOpts {
|
|
64
|
+
tabId?: TabId;
|
|
65
|
+
console?: boolean;
|
|
66
|
+
network?: boolean;
|
|
67
|
+
dialogs?: boolean;
|
|
68
|
+
sinceSeq?: number;
|
|
69
|
+
limit?: number;
|
|
70
|
+
clear?: boolean;
|
|
71
|
+
setPolicy?: DialogPolicy;
|
|
72
|
+
promptText?: string;
|
|
73
|
+
includeResources?: boolean;
|
|
74
|
+
}
|
|
35
75
|
export interface TabInfo {
|
|
36
76
|
tabId: TabId;
|
|
37
77
|
url: string;
|
|
@@ -105,6 +145,8 @@ export interface SnapshotNode {
|
|
|
105
145
|
value?: string;
|
|
106
146
|
disabled?: boolean;
|
|
107
147
|
checked?: boolean;
|
|
148
|
+
/** A password field: present so it can be targeted, `value` deliberately absent. */
|
|
149
|
+
secret?: boolean;
|
|
108
150
|
}
|
|
109
151
|
export interface SnapshotResult {
|
|
110
152
|
url: string;
|
|
@@ -189,29 +231,29 @@ export interface Executor {
|
|
|
189
231
|
button?: MouseButton;
|
|
190
232
|
clickCount?: number;
|
|
191
233
|
trusted?: boolean;
|
|
192
|
-
}): Promise<ActionOk>;
|
|
234
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
193
235
|
type(t: Target, text: string, opts?: {
|
|
194
236
|
tabId?: TabId;
|
|
195
237
|
clear?: boolean;
|
|
196
238
|
pressEnter?: boolean;
|
|
197
239
|
keyEvents?: boolean;
|
|
198
240
|
trusted?: boolean;
|
|
199
|
-
}): Promise<ActionOk>;
|
|
241
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
200
242
|
/** Choose option(s) of a <select> by value or visible label. */
|
|
201
243
|
selectOption(t: Target, values: string[], opts?: {
|
|
202
244
|
tabId?: TabId;
|
|
203
|
-
}): Promise<ActionOk>;
|
|
245
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
204
246
|
/** Value-set + input/change events (used by fill_form). */
|
|
205
247
|
fill(t: Target, value: string, opts?: {
|
|
206
248
|
tabId?: TabId;
|
|
207
|
-
}): Promise<ActionOk>;
|
|
249
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
208
250
|
press(key: string, opts?: {
|
|
209
251
|
tabId?: TabId;
|
|
210
252
|
modifiers?: KeyModifier[];
|
|
211
253
|
}): Promise<ActionOk>;
|
|
212
254
|
hover(t: Target, opts?: {
|
|
213
255
|
tabId?: TabId;
|
|
214
|
-
}): Promise<ActionOk>;
|
|
256
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
215
257
|
scroll(opts: {
|
|
216
258
|
tabId?: TabId;
|
|
217
259
|
x?: number;
|
|
@@ -219,17 +261,17 @@ export interface Executor {
|
|
|
219
261
|
deltaX?: number;
|
|
220
262
|
deltaY?: number;
|
|
221
263
|
target?: Target;
|
|
222
|
-
}): Promise<ActionOk>;
|
|
264
|
+
} & FrameOpts): Promise<ActionOk>;
|
|
223
265
|
getText(t?: Target, opts?: {
|
|
224
266
|
tabId?: TabId;
|
|
225
|
-
}): Promise<{
|
|
267
|
+
} & FrameOpts): Promise<{
|
|
226
268
|
text: string;
|
|
227
269
|
ref?: string;
|
|
228
270
|
}>;
|
|
229
271
|
getHtml(t?: Target, opts?: {
|
|
230
272
|
tabId?: TabId;
|
|
231
273
|
outer?: boolean;
|
|
232
|
-
}): Promise<{
|
|
274
|
+
} & FrameOpts): Promise<{
|
|
233
275
|
html: string;
|
|
234
276
|
}>;
|
|
235
277
|
/** Accessibility snapshot: interactive/landmark elements with stable refs the model can target. */
|
|
@@ -237,7 +279,7 @@ export interface Executor {
|
|
|
237
279
|
tabId?: TabId;
|
|
238
280
|
interactiveOnly?: boolean;
|
|
239
281
|
max?: number;
|
|
240
|
-
}): Promise<SnapshotResult>;
|
|
282
|
+
} & FrameOpts): Promise<SnapshotResult>;
|
|
241
283
|
/** Read cookies visible to the active tab's URL (or a given url). */
|
|
242
284
|
getCookies(opts?: {
|
|
243
285
|
tabId?: TabId;
|
|
@@ -257,18 +299,35 @@ export interface Executor {
|
|
|
257
299
|
tabId?: TabId;
|
|
258
300
|
fullPage?: boolean;
|
|
259
301
|
target?: Target;
|
|
260
|
-
}): Promise<ScreenshotResult>;
|
|
302
|
+
} & FrameOpts): Promise<ScreenshotResult>;
|
|
261
303
|
eval(expression: string, opts?: {
|
|
262
304
|
tabId?: TabId;
|
|
263
305
|
awaitPromise?: boolean;
|
|
264
|
-
}): Promise<EvalResult>;
|
|
306
|
+
} & FrameOpts): Promise<EvalResult>;
|
|
265
307
|
waitFor(opts: {
|
|
266
308
|
tabId?: TabId;
|
|
267
309
|
selector?: string;
|
|
268
310
|
textContains?: string;
|
|
269
311
|
gone?: boolean;
|
|
270
312
|
timeoutMs?: number;
|
|
271
|
-
}): Promise<WaitResult>;
|
|
313
|
+
} & FrameOpts): Promise<WaitResult>;
|
|
314
|
+
/** Every frame of a tab that the extension can inject into, with its URL. */
|
|
315
|
+
framesList?(opts?: {
|
|
316
|
+
tabId?: TabId;
|
|
317
|
+
}): Promise<FrameInfo[]>;
|
|
318
|
+
/** Read (and configure) the in-page console / network / dialog observers. */
|
|
319
|
+
observers?(args: ObserverArgs): Promise<ObserverReadResult>;
|
|
320
|
+
/** Render the page to PDF (Chrome's own print pipeline). */
|
|
321
|
+
printPdf?(opts?: {
|
|
322
|
+
tabId?: TabId;
|
|
323
|
+
landscape?: boolean;
|
|
324
|
+
printBackground?: boolean;
|
|
325
|
+
scale?: number;
|
|
326
|
+
paperWidth?: number;
|
|
327
|
+
paperHeight?: number;
|
|
328
|
+
pageRanges?: string;
|
|
329
|
+
preferCSSPageSize?: boolean;
|
|
330
|
+
}): Promise<PdfResult>;
|
|
272
331
|
download(args: {
|
|
273
332
|
url?: string;
|
|
274
333
|
target?: Target;
|
|
@@ -290,7 +349,7 @@ export interface Executor {
|
|
|
290
349
|
* (which only carries codes that originate inside the extension); these extra
|
|
291
350
|
* codes describe failures on the server half (no backend, launch failed, etc.).
|
|
292
351
|
*/
|
|
293
|
-
export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'BACKPRESSURE';
|
|
352
|
+
export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'FRAME_NOT_FOUND' | 'OBSERVERS_DISABLED' | 'UNSUPPORTED' | 'BACKPRESSURE';
|
|
294
353
|
export declare class ExecutorError extends Error {
|
|
295
354
|
readonly code: ExecutorErrorCodeLocal;
|
|
296
355
|
constructor(code: ExecutorErrorCodeLocal, message: string);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/mcp/audit.ts — per-call context so the action log can record WHICH page a
|
|
3
|
+
* call touched and what the policy decided about it.
|
|
4
|
+
*
|
|
5
|
+
* `history.jsonl` already records tool + args + ok. For a tool that drives a
|
|
6
|
+
* real, logged-in browser, the question people actually ask afterwards is "what
|
|
7
|
+
* did it touch, and what was it allowed to touch" — and the target URL, the
|
|
8
|
+
* policy verdict, and the size of what came back were all missing from the
|
|
9
|
+
* record. They are known inside the call and nowhere after it.
|
|
10
|
+
*
|
|
11
|
+
* `AsyncLocalStorage` rather than a module-level variable because `batch` runs
|
|
12
|
+
* ops CONCURRENTLY: one shared slot would attribute one op's URL to another's
|
|
13
|
+
* log line, which is worse than not logging it at all.
|
|
14
|
+
*/
|
|
15
|
+
export interface CallAudit {
|
|
16
|
+
/** The URL the policy gate was evaluated against, once one is resolved. */
|
|
17
|
+
url?: string;
|
|
18
|
+
/** Set when the gate denied the call. */
|
|
19
|
+
denied?: boolean;
|
|
20
|
+
/** Bytes of content this call returned to the caller (post-cap). */
|
|
21
|
+
bytes?: number;
|
|
22
|
+
/** How many secrets the redaction pass replaced. */
|
|
23
|
+
redactions?: number;
|
|
24
|
+
/** Frame the call actually acted on, when it was not the top frame. */
|
|
25
|
+
frameId?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Run `fn` with a fresh audit record, and hand that record back. */
|
|
28
|
+
export declare function withAudit<T>(fn: (audit: CallAudit) => Promise<T>): Promise<{
|
|
29
|
+
result: T;
|
|
30
|
+
audit: CallAudit;
|
|
31
|
+
}>;
|
|
32
|
+
/** The current call's audit record, or undefined outside a dispatch. */
|
|
33
|
+
export declare function currentAudit(): CallAudit | undefined;
|
|
34
|
+
/** Record the policy decision for this call. Safe to call outside a dispatch. */
|
|
35
|
+
export declare function noteGate(url: string, allowed: boolean): void;
|
|
36
|
+
/** Record how much content crossed back to the caller. */
|
|
37
|
+
export declare function noteBytes(bytes: number): void;
|
|
38
|
+
/** Record how many secrets were scrubbed on the way out. */
|
|
39
|
+
export declare function noteRedactions(count: number): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/audit.ts — per-call context so the action log can record WHICH page a
|
|
4
|
+
* call touched and what the policy decided about it.
|
|
5
|
+
*
|
|
6
|
+
* `history.jsonl` already records tool + args + ok. For a tool that drives a
|
|
7
|
+
* real, logged-in browser, the question people actually ask afterwards is "what
|
|
8
|
+
* did it touch, and what was it allowed to touch" — and the target URL, the
|
|
9
|
+
* policy verdict, and the size of what came back were all missing from the
|
|
10
|
+
* record. They are known inside the call and nowhere after it.
|
|
11
|
+
*
|
|
12
|
+
* `AsyncLocalStorage` rather than a module-level variable because `batch` runs
|
|
13
|
+
* ops CONCURRENTLY: one shared slot would attribute one op's URL to another's
|
|
14
|
+
* log line, which is worse than not logging it at all.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.withAudit = withAudit;
|
|
18
|
+
exports.currentAudit = currentAudit;
|
|
19
|
+
exports.noteGate = noteGate;
|
|
20
|
+
exports.noteBytes = noteBytes;
|
|
21
|
+
exports.noteRedactions = noteRedactions;
|
|
22
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
23
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
24
|
+
/** Run `fn` with a fresh audit record, and hand that record back. */
|
|
25
|
+
async function withAudit(fn) {
|
|
26
|
+
const audit = {};
|
|
27
|
+
const result = await storage.run(audit, () => fn(audit));
|
|
28
|
+
return { result, audit };
|
|
29
|
+
}
|
|
30
|
+
/** The current call's audit record, or undefined outside a dispatch. */
|
|
31
|
+
function currentAudit() {
|
|
32
|
+
return storage.getStore();
|
|
33
|
+
}
|
|
34
|
+
/** Record the policy decision for this call. Safe to call outside a dispatch. */
|
|
35
|
+
function noteGate(url, allowed) {
|
|
36
|
+
const a = storage.getStore();
|
|
37
|
+
if (!a)
|
|
38
|
+
return;
|
|
39
|
+
if (url)
|
|
40
|
+
a.url = url;
|
|
41
|
+
if (!allowed)
|
|
42
|
+
a.denied = true;
|
|
43
|
+
}
|
|
44
|
+
/** Record how much content crossed back to the caller. */
|
|
45
|
+
function noteBytes(bytes) {
|
|
46
|
+
const a = storage.getStore();
|
|
47
|
+
if (!a)
|
|
48
|
+
return;
|
|
49
|
+
a.bytes = (a.bytes ?? 0) + bytes;
|
|
50
|
+
}
|
|
51
|
+
/** Record how many secrets were scrubbed on the way out. */
|
|
52
|
+
function noteRedactions(count) {
|
|
53
|
+
if (count <= 0)
|
|
54
|
+
return;
|
|
55
|
+
const a = storage.getStore();
|
|
56
|
+
if (!a)
|
|
57
|
+
return;
|
|
58
|
+
a.redactions = (a.redactions ?? 0) + count;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=audit.js.map
|