@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.
Files changed (44) hide show
  1. package/README.md +116 -0
  2. package/dist/shared/observers.d.ts +110 -0
  3. package/dist/shared/observers.js +127 -0
  4. package/dist/shared/page-fns.d.ts +46 -0
  5. package/dist/shared/page-fns.js +292 -0
  6. package/dist/shared/policy.d.ts +9 -0
  7. package/dist/shared/policy.js +16 -0
  8. package/dist/shared/protocol.d.ts +11 -2
  9. package/dist/shared/protocol.js +3 -0
  10. package/dist/shared/snapshot.d.ts +2 -0
  11. package/dist/shared/snapshot.js +8 -1
  12. package/dist/src/bridge/workspace.d.ts +6 -0
  13. package/dist/src/bridge/workspace.js +20 -0
  14. package/dist/src/cli.js +13 -0
  15. package/dist/src/config.js +31 -0
  16. package/dist/src/executor/extension-executor.d.ts +27 -13
  17. package/dist/src/executor/extension-executor.js +53 -12
  18. package/dist/src/executor/stub-executor.d.ts +45 -1
  19. package/dist/src/executor/stub-executor.js +53 -8
  20. package/dist/src/executor/types.d.ts +72 -13
  21. package/dist/src/mcp/audit.d.ts +39 -0
  22. package/dist/src/mcp/audit.js +60 -0
  23. package/dist/src/mcp/batch.js +61 -9
  24. package/dist/src/mcp/helpers.d.ts +4 -4
  25. package/dist/src/mcp/helpers.js +9 -4
  26. package/dist/src/mcp/limits.d.ts +43 -0
  27. package/dist/src/mcp/limits.js +87 -0
  28. package/dist/src/mcp/locate.d.ts +45 -0
  29. package/dist/src/mcp/locate.js +105 -0
  30. package/dist/src/mcp/log.d.ts +17 -0
  31. package/dist/src/mcp/log.js +43 -0
  32. package/dist/src/mcp/redact.d.ts +48 -0
  33. package/dist/src/mcp/redact.js +92 -0
  34. package/dist/src/mcp/server.d.ts +1 -2
  35. package/dist/src/mcp/server.js +16 -12
  36. package/dist/src/mcp/snapdiff.d.ts +43 -0
  37. package/dist/src/mcp/snapdiff.js +92 -0
  38. package/dist/src/mcp/tools.js +481 -41
  39. package/dist/src/security/policy.d.ts +5 -0
  40. package/dist/src/security/policy.js +6 -0
  41. package/docs/BLUEPRINT.md +15 -1
  42. package/extension-dist/background.js +617 -214
  43. package/extension-dist/page-hook.js +215 -0
  44. 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
- return { text: 'stub text', ref: 'el_stub_1' };
170
+ this.maybeDisconnect();
171
+ return { text: this.textPayload, ref: 'el_stub_1' };
135
172
  }
136
173
  async getHtml() {
137
- return { html: '<html><body><a href="https://example.com">Example</a></body></html>' };
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