@mehmoodqureshi/chrome-mcp 0.9.1 → 0.9.2

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 CHANGED
@@ -333,6 +333,46 @@ type { "role": "textbox", "name": "Email", "text": "a@b.com" }
333
333
  Resolution is server-side and refuses to guess: an ambiguous locator fails with
334
334
  the candidates listed rather than acting on the first one (pass `nth` to pick).
335
335
 
336
+ ### Did the session expire? — `auth_check` and `failOnAuthWall`
337
+
338
+ Reusing a signed-in Chrome removes the login step, but a session cookie can
339
+ still expire mid-run. Without a distinct signal the next step fails as
340
+ `SELECTOR_NOT_FOUND` or `TIMEOUT`, and an eval harness scores the run as an
341
+ agent failure when it was an auth failure. Every `snapshot` now carries an
342
+ `authWall` verdict when the page looks like a sign-in wall, and there is a
343
+ dedicated probe:
344
+
345
+ ```jsonc
346
+ auth_check {} // { authRequired, confidence, signals }
347
+ auth_check { "failOnAuthWall": true } // [AUTH_REQUIRED] error instead
348
+ navigate { "url": "https://app.example.com/dashboard", "failOnAuthWall": true }
349
+ snapshot { "failOnAuthWall": true }
350
+ ```
351
+
352
+ For a harness, set it once instead of per call:
353
+
354
+ ```
355
+ npx -y @mehmoodqureshi/chrome-mcp --allow-domain app.example.com --enable-mutations --fail-on-auth-wall
356
+ ```
357
+
358
+ With the flag on, every step that can move the tab (`navigate`, `click`, `type`,
359
+ `select_option`, `press`, `fill_form`, `back`, `forward`, `reload`) checks the
360
+ page it landed on and fails with `[AUTH_REQUIRED]` if that page is a sign-in
361
+ wall, and a `wait_for` that times out on such a page reports `[AUTH_REQUIRED]`
362
+ instead of `[TIMEOUT]`. Each guarded step costs one extra snapshot round-trip;
363
+ with the flag off the cost is zero. `[AUTH_REQUIRED]` is where a harness pauses
364
+ for a human to sign in again in the same Chrome, then retries the step. chrome-mcp
365
+ never re-authenticates on its own: it holds no credentials, by design.
366
+
367
+ Detection reads only what the snapshot already has: the URL (sign-in routes,
368
+ identity-provider hosts such as `accounts.google.com`, `login.microsoftonline.com`,
369
+ Okta, Auth0), the title, password fields, and sign-in controls. `high`
370
+ confidence needs two independent cues (a password field plus a sign-in button,
371
+ say); a lone password field or a bare `/auth/...` URL is `medium`. A header
372
+ "Sign in" link on an ordinary page never counts. `failOnAuthWall` fires only on
373
+ `high`, so a harness can bucket `[AUTH_REQUIRED]` separately from every other
374
+ failure while a settings page with a "current password" field carries on.
375
+
336
376
  ### Printing — `print_pdf`
337
377
 
338
378
  ```jsonc
@@ -0,0 +1,51 @@
1
+ /**
2
+ * shared/auth-wall.ts — decide whether a page is a login wall.
3
+ *
4
+ * A browser agent that reuses a signed-in Chrome profile still hits the moment
5
+ * the session cookie expires mid-run: the next page is a sign-in form, and
6
+ * every later step fails for a reason that has nothing to do with the task.
7
+ * Without a distinct signal, an eval harness buckets that as SELECTOR_NOT_FOUND
8
+ * or TIMEOUT and the run is scored as an agent failure instead of an auth
9
+ * failure.
10
+ *
11
+ * This module is the pure decision. It reads only what an accessibility
12
+ * snapshot already carries (URL, title, password fields, button/link names), so
13
+ * it works identically on both backends and needs nothing new from the
14
+ * extension. Server-side callers attach the verdict to snapshot results, expose
15
+ * it as `auth_check`, and can turn it into an `AUTH_REQUIRED` error.
16
+ */
17
+ export type AuthWallConfidence = 'high' | 'medium';
18
+ export type AuthWallSignal = 'password-field' | 'login-url' | 'identity-provider' | 'login-title' | 'login-button' | 'username-field';
19
+ export interface AuthWall {
20
+ detected: true;
21
+ confidence: AuthWallConfidence;
22
+ signals: AuthWallSignal[];
23
+ }
24
+ /** The subset of a snapshot the detector reads. */
25
+ export interface AuthWallPage {
26
+ url: string;
27
+ title: string;
28
+ nodes: Array<{
29
+ role: string;
30
+ name: string;
31
+ tag: string;
32
+ secret?: boolean;
33
+ }>;
34
+ }
35
+ /**
36
+ * Inspect a page and return a verdict, or `null` when it does not look like a
37
+ * login wall.
38
+ *
39
+ * Confidence:
40
+ * - `high`: a password field plus a sign-in control, title, or URL; or a
41
+ * sign-in URL plus a sign-in control or title; or an identity-provider host.
42
+ * - `medium`: exactly one weaker cue on its own (a password field, or a
43
+ * sign-in URL) with nothing to corroborate it. Callers that fail a run on a
44
+ * wall should usually require `high`.
45
+ *
46
+ * A lone "Sign in" link or a title mention never counts: most public pages have
47
+ * one in the header.
48
+ */
49
+ export declare function detectAuthWall(page: AuthWallPage): AuthWall | null;
50
+ /** A one-line, human-readable explanation for error messages and logs. */
51
+ export declare function describeAuthWall(wall: AuthWall, url: string): string;
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * shared/auth-wall.ts — decide whether a page is a login wall.
4
+ *
5
+ * A browser agent that reuses a signed-in Chrome profile still hits the moment
6
+ * the session cookie expires mid-run: the next page is a sign-in form, and
7
+ * every later step fails for a reason that has nothing to do with the task.
8
+ * Without a distinct signal, an eval harness buckets that as SELECTOR_NOT_FOUND
9
+ * or TIMEOUT and the run is scored as an agent failure instead of an auth
10
+ * failure.
11
+ *
12
+ * This module is the pure decision. It reads only what an accessibility
13
+ * snapshot already carries (URL, title, password fields, button/link names), so
14
+ * it works identically on both backends and needs nothing new from the
15
+ * extension. Server-side callers attach the verdict to snapshot results, expose
16
+ * it as `auth_check`, and can turn it into an `AUTH_REQUIRED` error.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.detectAuthWall = detectAuthWall;
20
+ exports.describeAuthWall = describeAuthWall;
21
+ /** Path segments that mark a sign-in route on most stacks. */
22
+ const LOGIN_PATH = /(^|[\/._-])(login|log-in|logon|log-on|signin|sign-in|sign_in|authenticate|auth|sso|oauth2?|saml|sessions?\/new|users\/sign_in|account\/login|idp)([\/?#._-]|$)/i;
23
+ /** Hosts (or host suffixes) that are identity providers, whatever the path. */
24
+ const IDP_HOSTS = [
25
+ 'accounts.google.com',
26
+ 'login.microsoftonline.com',
27
+ 'login.live.com',
28
+ 'login.microsoft.com',
29
+ 'appleid.apple.com',
30
+ 'login.yahoo.com',
31
+ 'auth0.com',
32
+ 'okta.com',
33
+ 'oktapreview.com',
34
+ 'onelogin.com',
35
+ 'pingidentity.com',
36
+ 'duosecurity.com',
37
+ 'github.com/login',
38
+ 'login.salesforce.com',
39
+ 'signin.aws.amazon.com',
40
+ 'auth.atlassian.com',
41
+ 'id.atlassian.com',
42
+ 'login.linkedin.com',
43
+ ];
44
+ const LOGIN_TITLE = /\b(sign in|sign-in|signin|log in|log-in|login|log on|logon|authenticate|authentication required|session (has )?expired|session timed out|please sign in|please log in|verify it'?s you)\b/i;
45
+ /** Button/link labels that submit or start a sign-in. Anchored so a header "Sign in" link with extra copy still matches only when the label is the action. */
46
+ const LOGIN_BUTTON = /^(sign in|sign-in|signin|log in|log-in|login|log on|logon|continue|next|submit|authenticate|(sign|log) ?in (with|via|using) .{1,40}|continue with .{1,40}|use (your )?password|sign in to continue|log in to continue)$/i;
47
+ /** Labels that mark a credential's username half; only a booster, never a wall on its own. */
48
+ const USERNAME_FIELD = /\b(email|e-mail|username|user name|user id|userid|phone|account|login id)\b/i;
49
+ /** The action-shaped labels that, next to a password field, make this a form and not a "change password" settings page. */
50
+ const STRONG_BUTTON = /^(sign in|sign-in|signin|log in|log-in|login|log on|logon|authenticate|sign in to continue|log in to continue|(sign|log) ?in (with|via|using) .{1,40}|continue with .{1,40})$/i;
51
+ function parseUrl(url) {
52
+ try {
53
+ return new URL(url);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function isIdpHost(u) {
60
+ const host = u.hostname.toLowerCase();
61
+ const hostAndPath = `${host}${u.pathname}`.toLowerCase();
62
+ return IDP_HOSTS.some((h) => {
63
+ if (h.includes('/'))
64
+ return hostAndPath === h || hostAndPath.startsWith(`${h}/`);
65
+ return host === h || host.endsWith(`.${h}`);
66
+ });
67
+ }
68
+ /**
69
+ * Inspect a page and return a verdict, or `null` when it does not look like a
70
+ * login wall.
71
+ *
72
+ * Confidence:
73
+ * - `high`: a password field plus a sign-in control, title, or URL; or a
74
+ * sign-in URL plus a sign-in control or title; or an identity-provider host.
75
+ * - `medium`: exactly one weaker cue on its own (a password field, or a
76
+ * sign-in URL) with nothing to corroborate it. Callers that fail a run on a
77
+ * wall should usually require `high`.
78
+ *
79
+ * A lone "Sign in" link or a title mention never counts: most public pages have
80
+ * one in the header.
81
+ */
82
+ function detectAuthWall(page) {
83
+ const signals = new Set();
84
+ const u = parseUrl(page.url);
85
+ if (u && LOGIN_PATH.test(u.pathname))
86
+ signals.add('login-url');
87
+ if (u && isIdpHost(u))
88
+ signals.add('identity-provider');
89
+ if (LOGIN_TITLE.test(page.title || ''))
90
+ signals.add('login-title');
91
+ let strongButton = false;
92
+ for (const n of page.nodes) {
93
+ if (n.secret === true || (n.tag === 'input' && n.role === 'textbox' && /password/i.test(n.name))) {
94
+ signals.add('password-field');
95
+ continue;
96
+ }
97
+ const label = (n.name || '').trim();
98
+ if (!label)
99
+ continue;
100
+ const clickable = n.role === 'button' || n.role === 'link' || n.tag === 'button';
101
+ if (clickable && LOGIN_BUTTON.test(label)) {
102
+ signals.add('login-button');
103
+ if (STRONG_BUTTON.test(label))
104
+ strongButton = true;
105
+ }
106
+ else if ((n.role === 'textbox' || n.role === 'combobox') && USERNAME_FIELD.test(label)) {
107
+ signals.add('username-field');
108
+ }
109
+ }
110
+ const has = (s) => signals.has(s);
111
+ const password = has('password-field');
112
+ const loginUrl = has('login-url');
113
+ const idp = has('identity-provider');
114
+ const title = has('login-title');
115
+ let confidence = null;
116
+ if (idp)
117
+ confidence = 'high';
118
+ else if (password && (strongButton || title || loginUrl || has('username-field')))
119
+ confidence = 'high';
120
+ else if (loginUrl && (strongButton || title || has('username-field')))
121
+ confidence = 'high';
122
+ else if (password || loginUrl)
123
+ confidence = 'medium';
124
+ if (!confidence)
125
+ return null;
126
+ const order = [
127
+ 'password-field',
128
+ 'identity-provider',
129
+ 'login-url',
130
+ 'login-title',
131
+ 'login-button',
132
+ 'username-field',
133
+ ];
134
+ return { detected: true, confidence, signals: order.filter(has) };
135
+ }
136
+ /** A one-line, human-readable explanation for error messages and logs. */
137
+ function describeAuthWall(wall, url) {
138
+ return `the page at ${url} looks like a sign-in wall (${wall.confidence} confidence: ${wall.signals.join(', ')}); the session has probably expired`;
139
+ }
140
+ //# sourceMappingURL=auth-wall.js.map
@@ -150,6 +150,9 @@ function parseArgs(argv) {
150
150
  policyFlags.redact = true;
151
151
  (policyFlags.redactPatterns ??= []).push(requireValue(argv[++i], '--redact-pattern'));
152
152
  break;
153
+ case '--fail-on-auth-wall':
154
+ policyFlags.failOnAuthWall = true;
155
+ break;
153
156
  case '--cdp-fallback':
154
157
  cdpFallback = true;
155
158
  break;
@@ -310,6 +313,10 @@ Security (default: deny-all safe mode):
310
313
  tokens, private keys) out of page reads. Password field
311
314
  values are always suppressed, with or without this.
312
315
  --redact-pattern <re> Add a redaction regex (repeatable; implies --redact)
316
+ --fail-on-auth-wall Fail any navigate/click/type/press/back/reload/wait_for
317
+ with [AUTH_REQUIRED] when the page it lands on is a
318
+ high-confidence sign-in wall (expired session), so an
319
+ eval harness never scores it as some other failure.
313
320
 
314
321
  Misc:
315
322
  --log-level <lvl> silent | info | debug (default info)
@@ -54,6 +54,15 @@ export interface StubOptions {
54
54
  snapshotNodes?: SnapshotNode[];
55
55
  /** Frames the stub reports for `frames_list`. */
56
56
  frames?: FrameInfo[];
57
+ /** Applied after any action or history move: models a click/submit/back that
58
+ * lands the tab on a different page (e.g. a redirect to a sign-in wall). */
59
+ afterAction?: {
60
+ url?: string;
61
+ nodes?: SnapshotNode[];
62
+ };
63
+ /** When true, `waitFor` rejects with TIMEOUT - the error a wait on a page
64
+ * that silently became a login form used to surface. */
65
+ waitForTimesOut?: boolean;
57
66
  /** What the in-page observers return. Absent = the hook is not installed,
58
67
  * which is the case the tools must report clearly rather than as an empty list. */
59
68
  observers?: ObserverReadResult;
@@ -76,6 +85,10 @@ export declare class StubExecutor implements Executor {
76
85
  snapshotNodes: SnapshotNode[];
77
86
  private readonly frames;
78
87
  private readonly observerState?;
88
+ private readonly afterAction?;
89
+ private readonly waitForTimesOut;
90
+ /** How many snapshots were taken - the auth guard must cost none when off. */
91
+ snapshotCalls: number;
79
92
  /** The last observer args received, so a test can assert what was requested. */
80
93
  lastObserverArgs?: ObserverArgs;
81
94
  /** How many times the gate actually asked for the tab list — the round-trip
@@ -110,6 +123,8 @@ export declare class StubExecutor implements Executor {
110
123
  back(): Promise<NavResult>;
111
124
  forward(): Promise<NavResult>;
112
125
  reload(): Promise<NavResult>;
126
+ /** Move the stub tab to the configured post-action page, if any. */
127
+ private landed;
113
128
  click(): Promise<ActionOk>;
114
129
  type(): Promise<ActionOk>;
115
130
  fill(): Promise<ActionOk>;
@@ -32,6 +32,10 @@ class StubExecutor {
32
32
  snapshotNodes;
33
33
  frames;
34
34
  observerState;
35
+ afterAction;
36
+ waitForTimesOut;
37
+ /** How many snapshots were taken - the auth guard must cost none when off. */
38
+ snapshotCalls = 0;
35
39
  /** The last observer args received, so a test can assert what was requested. */
36
40
  lastObserverArgs;
37
41
  /** How many times the gate actually asked for the tab list — the round-trip
@@ -40,6 +44,8 @@ class StubExecutor {
40
44
  ready = false;
41
45
  constructor(opts = {}) {
42
46
  this.url = opts.activeUrl ?? 'about:blank';
47
+ this.afterAction = opts.afterAction;
48
+ this.waitForTimesOut = opts.waitForTimesOut ?? false;
43
49
  this.evalThrows = opts.evalThrows ?? false;
44
50
  this.tabsListThrows = opts.tabsListThrows ?? false;
45
51
  this.noTabs = opts.noTabs ?? false;
@@ -133,15 +139,28 @@ class StubExecutor {
133
139
  return { url: args.url, title: 'Stub Page', httpStatus: 200 };
134
140
  }
135
141
  async back() {
142
+ this.landed();
136
143
  return { url: this.url, title: 'Stub Page' };
137
144
  }
138
145
  async forward() {
146
+ this.landed();
139
147
  return { url: this.url, title: 'Stub Page' };
140
148
  }
141
149
  async reload() {
150
+ this.landed();
142
151
  return { url: this.url, title: 'Stub Page' };
143
152
  }
153
+ /** Move the stub tab to the configured post-action page, if any. */
154
+ landed() {
155
+ if (!this.afterAction)
156
+ return;
157
+ if (this.afterAction.url !== undefined)
158
+ this.url = this.afterAction.url;
159
+ if (this.afterAction.nodes !== undefined)
160
+ this.snapshotNodes = this.afterAction.nodes;
161
+ }
144
162
  async click() {
163
+ this.landed();
145
164
  return ok;
146
165
  }
147
166
  async type() {
@@ -149,18 +168,22 @@ class StubExecutor {
149
168
  this.remainingWriteDisconnects--;
150
169
  throw new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'stub: service worker recycled mid-command');
151
170
  }
171
+ this.landed();
152
172
  return ok;
153
173
  }
154
174
  async fill() {
175
+ this.landed();
155
176
  return ok;
156
177
  }
157
178
  async press() {
179
+ this.landed();
158
180
  return ok;
159
181
  }
160
182
  async hover() {
161
183
  return ok;
162
184
  }
163
185
  async selectOption() {
186
+ this.landed();
164
187
  return ok;
165
188
  }
166
189
  async scroll() {
@@ -175,6 +198,7 @@ class StubExecutor {
175
198
  return { html: this.htmlPayload };
176
199
  }
177
200
  async snapshot() {
201
+ this.snapshotCalls++;
178
202
  return { url: this.url, title: 'Stub Page', nodes: this.snapshotNodes, truncated: false };
179
203
  }
180
204
  async getCookies() {
@@ -195,6 +219,8 @@ class StubExecutor {
195
219
  return { ok: true, value: 'stub-value', type: 'string' };
196
220
  }
197
221
  async waitFor() {
222
+ if (this.waitForTimesOut)
223
+ throw new types_1.ExecutorError('TIMEOUT', 'stub: wait_for timed out');
198
224
  return { matched: true, waitedMs: 0 };
199
225
  }
200
226
  async download(args) {
@@ -349,7 +349,9 @@ export interface Executor {
349
349
  * (which only carries codes that originate inside the extension); these extra
350
350
  * codes describe failures on the server half (no backend, launch failed, etc.).
351
351
  */
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';
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'
353
+ /** The page is a sign-in wall (session expired mid-run). Raised only when the caller opts in via `failOnAuthWall`. */
354
+ | 'AUTH_REQUIRED';
353
355
  export declare class ExecutorError extends Error {
354
356
  readonly code: ExecutorErrorCodeLocal;
355
357
  constructor(code: ExecutorErrorCodeLocal, message: string);
@@ -29,6 +29,7 @@ const helpers_1 = require("./helpers");
29
29
  const redact_1 = require("./redact");
30
30
  const locate_1 = require("./locate");
31
31
  const snapdiff_1 = require("./snapdiff");
32
+ const auth_wall_1 = require("../../shared/auth-wall");
32
33
  const audit_1 = require("./audit");
33
34
  const log_1 = require("./log");
34
35
  const tasks_1 = require("../bridge/tasks");
@@ -40,6 +41,7 @@ const TARGET_PROPS = {
40
41
  ref: zod_1.z.string().describe('Element ref from a prior read (exactly one of selector|ref)').optional(),
41
42
  };
42
43
  const tabIdField = zod_1.z.string().describe('Target tab id (defaults to the active tab)').optional();
44
+ const authWallField = zod_1.z.boolean().describe('Fail with [AUTH_REQUIRED] when the page this call lands on is a high-confidence sign-in wall (session expired). Off by default unless the server runs with --fail-on-auth-wall; snapshot still reports the verdict as `authWall` either way.').optional();
43
45
  /**
44
46
  * Frame targeting. Omitted = the top frame, which is what every call did before
45
47
  * frames were addressable. `allFrames` is the one to reach for when a selector
@@ -79,27 +81,27 @@ exports.TOOL_DEFINITIONS = [
79
81
  { name: 'tab_select', description: 'Make a tab active by tabId.', inputSchema: { tabId: zod_1.z.string() } },
80
82
  { name: 'tab_new', description: 'Open a NEW tab (optionally at a URL) and focus it. Prefer this over `navigate` when the user says "open"/"go to" a site — `navigate` REPLACES the current tab. Pass active:false to open in the background (used by parallel batches).', inputSchema: { url: zod_1.z.string().optional(), active: zod_1.z.boolean().optional() } },
81
83
  { name: 'tab_close', description: 'Close a tab by tabId.', inputSchema: { tabId: zod_1.z.string() } },
82
- { name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema: { url: zod_1.z.string(), tabId: tabIdField, waitUntil: waitUntilField } },
83
- { name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField } },
84
- { name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField } },
85
- { name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField } },
86
- { name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField } },
87
- { name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField } },
88
- { name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField } },
89
- { name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: { key: zod_1.z.string(), modifiers: zod_1.z.array(zod_1.z.string()).optional(), tabId: tabIdField } },
84
+ { name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema: { url: zod_1.z.string(), tabId: tabIdField, waitUntil: waitUntilField, failOnAuthWall: authWallField } },
85
+ { name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
86
+ { name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
87
+ { name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField, failOnAuthWall: authWallField } },
88
+ { name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
89
+ { name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
90
+ { name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
91
+ { name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: { key: zod_1.z.string(), modifiers: zod_1.z.array(zod_1.z.string()).optional(), tabId: tabIdField, failOnAuthWall: authWallField } },
90
92
  { name: 'hover', description: 'Hover over an element.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, snapshotAfter: snapshotAfterField } },
91
93
  { name: 'scroll', description: 'Scroll the page or to an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, x: zod_1.z.number().optional(), y: zod_1.z.number().optional(), deltaX: zod_1.z.number().optional(), deltaY: zod_1.z.number().optional(), tabId: tabIdField } },
92
94
  { name: 'screenshot', description: 'Capture a PNG screenshot (page or element).', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, fullPage: zod_1.z.boolean().optional(), tabId: tabIdField } },
93
95
  { name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
94
96
  { name: 'get_html', description: 'Get HTML of the page or an element. Output is capped (see maxBytes) and cut at a tag boundary; narrow it with `selector` rather than raising the cap when you can. Password field values are always blanked.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, outer: zod_1.z.boolean().optional(), tabId: tabIdField, maxBytes: maxBytesField } },
95
- { name: 'snapshot', description: 'Accessibility snapshot: interactive elements with refs to target by `ref` (more reliable than guessing CSS selectors). Pass diff:true to get only what changed since the last snapshot of this tab - far cheaper in a click/read loop. Password fields appear as secret:true with no value.', inputSchema: { interactiveOnly: zod_1.z.boolean().optional(), max: zod_1.z.number().optional(), diff: zod_1.z.boolean().describe('Return added/removed/changed elements since the previous snapshot of this tab instead of the whole tree').optional(), ...FRAME_PROPS, tabId: tabIdField } },
97
+ { name: 'snapshot', description: 'Accessibility snapshot: interactive elements with refs to target by `ref` (more reliable than guessing CSS selectors). Pass diff:true to get only what changed since the last snapshot of this tab - far cheaper in a click/read loop. Password fields appear as secret:true with no value.', inputSchema: { interactiveOnly: zod_1.z.boolean().optional(), max: zod_1.z.number().optional(), diff: zod_1.z.boolean().describe('Return added/removed/changed elements since the previous snapshot of this tab instead of the whole tree').optional(), failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
96
98
  { name: 'get_cookies', description: "Read cookies visible to the tab's URL (or a given url).", inputSchema: { url: zod_1.z.string().optional(), tabId: tabIdField } },
97
99
  { name: 'storage', description: 'Read/write localStorage (or sessionStorage). op: get|set|remove|clear.', inputSchema: { op: zod_1.z.enum(['get', 'set', 'remove', 'clear']), key: zod_1.z.string().optional(), value: zod_1.z.string().optional(), session: zod_1.z.boolean().optional(), tabId: tabIdField } },
98
100
  { name: 'eval', description: 'Evaluate JavaScript in the page (disabled in safe-mode).', inputSchema: { expression: zod_1.z.string(), awaitPromise: zod_1.z.boolean().optional(), ...FRAME_PROPS, tabId: tabIdField } },
99
- { name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: { selector: zod_1.z.string().optional(), textContains: zod_1.z.string().optional(), gone: zod_1.z.boolean().optional(), timeoutMs: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField } },
101
+ { name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: { selector: zod_1.z.string().optional(), textContains: zod_1.z.string().optional(), gone: zod_1.z.boolean().optional(), timeoutMs: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField, failOnAuthWall: authWallField } },
100
102
  { name: 'extract_links', description: 'Extract anchors from the page or a subtree. dedupe=true collapses links sharing an href (nav/footer noise); limit caps the count.', inputSchema: { selector: zod_1.z.string().optional(), sameOriginOnly: zod_1.z.boolean().optional(), dedupe: zod_1.z.boolean().optional(), limit: zod_1.z.number().optional(), ...FRAME_PROPS, tabId: tabIdField } },
101
103
  { name: 'read_as_markdown', description: 'Read the page (or subtree) as readable markdown.', inputSchema: { selector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
102
- { name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: { fields: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.boolean()])), submitSelector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField } },
104
+ { name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: { fields: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.boolean()])), submitSelector: zod_1.z.string().optional(), ...FRAME_PROPS, tabId: tabIdField, failOnAuthWall: authWallField } },
103
105
  { name: 'download_file', description: 'Download a file by URL or from a link element.', inputSchema: { url: zod_1.z.string().optional(), ...TARGET_PROPS, suggestedName: zod_1.z.string().optional(), tabId: tabIdField } },
104
106
  { name: 'upload_file', description: 'Set local file(s) on a file <input> (target by selector or ref) — uploads without the OS dialog. Requires --enable-uploads. `files` are absolute local paths.', inputSchema: { ...TARGET_PROPS, files: zod_1.z.array(zod_1.z.string()), tabId: tabIdField } },
105
107
  {
@@ -161,6 +163,7 @@ exports.TOOL_DEFINITIONS = [
161
163
  },
162
164
  },
163
165
  { name: 'chrome_status', description: 'Report backend/session status.', inputSchema: {} },
166
+ { name: 'auth_check', description: 'Is the tab sitting on a sign-in wall? Reads the page (URL, title, password fields, sign-in controls) and returns { authRequired, confidence, signals }. Use it after a navigate, or whenever a step fails unexpectedly, to tell "the session expired" apart from "the agent got lost". Pass failOnAuthWall:true to get an [AUTH_REQUIRED] error instead of a verdict, so a harness can bucket the run as an auth failure.', inputSchema: { failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
164
167
  { name: 'profile_use', description: 'Switch the active browser profile (identity). Subsequent downloads, results, screenshots, and the action log are stored under profiles/<name>/. Resets the active task to "default" unless you then call task_new.', inputSchema: { name: zod_1.z.string().describe('Profile name (becomes a folder; sanitized to a safe path segment).') } },
165
168
  { name: 'task_new', description: 'Start a new task (run) under the active profile. Creates profiles/<profile>/tasks/<name>/ with downloads/, results/, screenshots/ and makes it the active task so all captured artifacts land there.', inputSchema: { name: zod_1.z.string().describe('Task name (becomes a folder; sanitized to a safe path segment).') } },
166
169
  { name: 'tasks_list', description: 'List every task across all profiles under the data dir, with sizes and download counts.', inputSchema: {} },
@@ -317,6 +320,51 @@ function redaction(policy) {
317
320
  return cfg;
318
321
  }
319
322
  /** The scope a tab's remembered snapshot lives under. */
323
+ /**
324
+ * Throw `[AUTH_REQUIRED]` when the caller asked for it and the page is a
325
+ * high-confidence sign-in wall. Medium-confidence verdicts never fail a call: a
326
+ * lone password field on an otherwise ordinary page is not worth aborting over.
327
+ */
328
+ function failIfAuthWall(wall, url, a, policy) {
329
+ if (!wall || wall.confidence !== 'high')
330
+ return;
331
+ if (!authGuardOn(a, policy))
332
+ return;
333
+ throw new types_1.ExecutorError('AUTH_REQUIRED', (0, auth_wall_1.describeAuthWall)(wall, url));
334
+ }
335
+ /** The guard is on for this call when the caller asked, or the server runs with `--fail-on-auth-wall`. */
336
+ function authGuardOn(a, policy) {
337
+ return (0, validators_1.optionalBoolean)(a, 'failOnAuthWall') === true || policy.failOnAuthWall === true;
338
+ }
339
+ /**
340
+ * After an action or history move: when the guard is on, look at the page the
341
+ * tab landed on and fail with `AUTH_REQUIRED` if it is a sign-in wall. Costs
342
+ * one snapshot round-trip, and only when the guard is on. The snapshot is
343
+ * remembered for this tab so a later `snapshot { diff: true }` stays coherent.
344
+ */
345
+ async function guardAuthWall(ctx, a) {
346
+ if (!authGuardOn(a, ctx.policy))
347
+ return;
348
+ const snap = await ctx.ex.snapshot({ tabId: tabId(a), interactiveOnly: true, max: 200 });
349
+ (0, snapdiff_1.rememberSnapshot)(snapScope(a), snap);
350
+ failIfAuthWall((0, auth_wall_1.detectAuthWall)(snap), snap.url, a, ctx.policy);
351
+ }
352
+ /**
353
+ * A wait that timed out on a page that has become a sign-in wall is an auth
354
+ * failure, not a slow page. With the guard on, reclassify it.
355
+ */
356
+ async function reclassifyTimeout(ctx, a, err) {
357
+ if (err instanceof types_1.ExecutorError && err.code === 'TIMEOUT' && authGuardOn(a, ctx.policy)) {
358
+ await guardAuthWall(ctx, a);
359
+ }
360
+ throw err;
361
+ }
362
+ /** The `authWall` field to spread onto a page-read result: present only when detected. */
363
+ function authWallOf(ctx, snap, a) {
364
+ const wall = (0, auth_wall_1.detectAuthWall)(snap);
365
+ failIfAuthWall(wall, snap.url, a, ctx.policy);
366
+ return wall ? { authWall: wall } : {};
367
+ }
320
368
  const snapScope = (a) => (0, snapdiff_1.scopeOf)((0, workspace_1.peekActiveWorkspace)()?.profile ?? 'default', tabId(a));
321
369
  /**
322
370
  * Render an action's result, optionally with what the action CHANGED on the
@@ -324,13 +372,20 @@ const snapScope = (a) => (0, snapdiff_1.scopeOf)((0, workspace_1.peekActiveWorks
324
372
  * re-read, which is the expensive half of every click-then-look loop.
325
373
  */
326
374
  async function actionResult(ctx, a, payload) {
327
- if ((0, validators_1.optionalBoolean)(a, 'snapshotAfter') !== true)
375
+ const wantDiff = (0, validators_1.optionalBoolean)(a, 'snapshotAfter') === true;
376
+ const guard = authGuardOn(a, ctx.policy);
377
+ if (!wantDiff && !guard)
328
378
  return (0, envelopes_1.jsonResult)(payload);
329
379
  const scope = snapScope(a);
330
380
  const previous = (0, snapdiff_1.lastSnapshot)(scope);
331
381
  const snap = await ctx.ex.snapshot({ tabId: tabId(a), max: 200, ...frameOpts(a) });
332
- const diff = (0, snapdiff_1.diffSnapshots)(previous, snap);
382
+ // One snapshot serves both: the auth guard reads it first (and aborts the
383
+ // call if the action landed on a sign-in wall), then the diff is built.
384
+ failIfAuthWall((0, auth_wall_1.detectAuthWall)(snap), snap.url, a, ctx.policy);
333
385
  const stored = (0, snapdiff_1.rememberSnapshot)(scope, snap);
386
+ if (!wantDiff)
387
+ return (0, envelopes_1.jsonResult)(payload);
388
+ const diff = (0, snapdiff_1.diffSnapshots)(previous, snap);
334
389
  return (0, envelopes_1.jsonResult)({ ...payload, changed: { ...diff, snapshotId: stored.id, url: snap.url } });
335
390
  }
336
391
  /**
@@ -403,19 +458,31 @@ exports.TOOL_HANDLERS = {
403
458
  navigate: async (a, ctx) => {
404
459
  const url = (0, validators_1.requireString)(a, 'url');
405
460
  await gate(ctx, 'navigate', { url });
406
- return (0, envelopes_1.jsonResult)(await ctx.ex.navigate({ url, tabId: tabId(a), waitUntil: waitUntil(a) }));
461
+ const nav = await ctx.ex.navigate({ url, tabId: tabId(a), waitUntil: waitUntil(a) });
462
+ if (!authGuardOn(a, ctx.policy))
463
+ return (0, envelopes_1.jsonResult)(nav);
464
+ // Guard on: the check costs one snapshot round-trip after the navigation.
465
+ const snap = await ctx.ex.snapshot({ tabId: tabId(a), interactiveOnly: true, max: 200 });
466
+ (0, snapdiff_1.rememberSnapshot)(snapScope(a), snap);
467
+ return (0, envelopes_1.jsonResult)({ ...nav, ...authWallOf(ctx, snap, a) });
407
468
  },
408
469
  back: async (a, ctx) => {
409
470
  await gate(ctx, 'back', { tabId: tabId(a) });
410
- return (0, envelopes_1.jsonResult)(await ctx.ex.back(tabId(a)));
471
+ const res = await ctx.ex.back(tabId(a));
472
+ await guardAuthWall(ctx, a);
473
+ return (0, envelopes_1.jsonResult)(res);
411
474
  },
412
475
  forward: async (a, ctx) => {
413
476
  await gate(ctx, 'forward', { tabId: tabId(a) });
414
- return (0, envelopes_1.jsonResult)(await ctx.ex.forward(tabId(a)));
477
+ const res = await ctx.ex.forward(tabId(a));
478
+ await guardAuthWall(ctx, a);
479
+ return (0, envelopes_1.jsonResult)(res);
415
480
  },
416
481
  reload: async (a, ctx) => {
417
482
  await gate(ctx, 'reload', { tabId: tabId(a) });
418
- return (0, envelopes_1.jsonResult)(await ctx.ex.reload({ tabId: tabId(a), waitUntil: waitUntil(a) }));
483
+ const res = await ctx.ex.reload({ tabId: tabId(a), waitUntil: waitUntil(a) });
484
+ await guardAuthWall(ctx, a);
485
+ return (0, envelopes_1.jsonResult)(res);
419
486
  },
420
487
  click: async (a, ctx) => {
421
488
  await gate(ctx, 'click', { tabId: tabId(a) });
@@ -453,10 +520,12 @@ exports.TOOL_HANDLERS = {
453
520
  },
454
521
  press: async (a, ctx) => {
455
522
  await gate(ctx, 'press', { tabId: tabId(a) });
456
- return (0, envelopes_1.jsonResult)(await ctx.ex.press((0, validators_1.requireString)(a, 'key'), {
523
+ const res = await ctx.ex.press((0, validators_1.requireString)(a, 'key'), {
457
524
  tabId: tabId(a),
458
525
  modifiers: (0, validators_1.optionalStringArray)(a, 'modifiers'),
459
- }));
526
+ });
527
+ await guardAuthWall(ctx, a); // Enter on a form is the classic way to land on a wall
528
+ return (0, envelopes_1.jsonResult)(res);
460
529
  },
461
530
  hover: async (a, ctx) => {
462
531
  await gate(ctx, 'hover', { tabId: tabId(a) });
@@ -536,14 +605,16 @@ exports.TOOL_HANDLERS = {
536
605
  const scope = snapScope(a);
537
606
  const previous = (0, snapdiff_1.lastSnapshot)(scope);
538
607
  const stored = (0, snapdiff_1.rememberSnapshot)(scope, snap);
608
+ const authWall = authWallOf(ctx, snap, a);
539
609
  if ((0, validators_1.optionalBoolean)(a, 'diff') !== true) {
540
- return (0, envelopes_1.jsonResult)({ ...snap, snapshotId: stored.id });
610
+ return (0, envelopes_1.jsonResult)({ ...snap, snapshotId: stored.id, ...authWall });
541
611
  }
542
612
  const diff = (0, snapdiff_1.diffSnapshots)(previous, snap);
543
613
  return (0, envelopes_1.jsonResult)({
544
614
  url: snap.url,
545
615
  title: snap.title,
546
616
  snapshotId: stored.id,
617
+ ...authWall,
547
618
  ...diff,
548
619
  ...(diff.since === null
549
620
  ? { note: 'no previous snapshot for this tab, so everything is reported as added' }
@@ -593,14 +664,19 @@ exports.TOOL_HANDLERS = {
593
664
  },
594
665
  wait_for: async (a, ctx) => {
595
666
  await gate(ctx, 'wait_for', { tabId: tabId(a) });
596
- return (0, envelopes_1.jsonResult)(await ctx.ex.waitFor({
597
- tabId: tabId(a),
598
- ...frameOpts(a),
599
- selector: (0, validators_1.optionalString)(a, 'selector'),
600
- textContains: (0, validators_1.optionalString)(a, 'textContains'),
601
- gone: (0, validators_1.optionalBoolean)(a, 'gone'),
602
- timeoutMs: (0, validators_1.optionalNumber)(a, 'timeoutMs', { min: 0, max: 120_000 }),
603
- }));
667
+ try {
668
+ return (0, envelopes_1.jsonResult)(await ctx.ex.waitFor({
669
+ tabId: tabId(a),
670
+ ...frameOpts(a),
671
+ selector: (0, validators_1.optionalString)(a, 'selector'),
672
+ textContains: (0, validators_1.optionalString)(a, 'textContains'),
673
+ gone: (0, validators_1.optionalBoolean)(a, 'gone'),
674
+ timeoutMs: (0, validators_1.optionalNumber)(a, 'timeoutMs', { min: 0, max: 120_000 }),
675
+ }));
676
+ }
677
+ catch (err) {
678
+ return reclassifyTimeout(ctx, a, err);
679
+ }
604
680
  },
605
681
  extract_links: async (a, ctx) => {
606
682
  await gate(ctx, 'get_text', { tabId: tabId(a) }); // read of page content
@@ -643,12 +719,14 @@ exports.TOOL_HANDLERS = {
643
719
  if (typeof val === 'string')
644
720
  (0, validators_1.requireWithinLength)(val, `fields["${sel}"]`, validators_1.MAX_TEXT_LEN);
645
721
  }
646
- return (0, envelopes_1.jsonResult)(await (0, helpers_1.fillForm)(ctx.ex, {
722
+ const res = await (0, helpers_1.fillForm)(ctx.ex, {
647
723
  ...frameOpts(a),
648
724
  fields: fields,
649
725
  submitSelector: (0, validators_1.optionalString)(a, 'submitSelector'),
650
726
  tabId: tabId(a),
651
- }));
727
+ });
728
+ await guardAuthWall(ctx, a);
729
+ return (0, envelopes_1.jsonResult)(res);
652
730
  },
653
731
  download_file: async (a, ctx) => {
654
732
  await gate(ctx, 'download_file');
@@ -768,6 +846,18 @@ exports.TOOL_HANDLERS = {
768
846
  });
769
847
  },
770
848
  chrome_status: async (_a, ctx) => (0, envelopes_1.jsonResult)(ctx.ex.status()),
849
+ auth_check: async (a, ctx) => {
850
+ await gate(ctx, 'get_text', { tabId: tabId(a) }); // read of page structure
851
+ const snap = await ctx.ex.snapshot({ tabId: tabId(a), ...frameOpts(a), interactiveOnly: true, max: 200 });
852
+ const wall = (0, auth_wall_1.detectAuthWall)(snap);
853
+ failIfAuthWall(wall, snap.url, a, ctx.policy);
854
+ return (0, envelopes_1.jsonResult)({
855
+ url: snap.url,
856
+ title: snap.title,
857
+ authRequired: wall !== null,
858
+ ...(wall ? { confidence: wall.confidence, signals: wall.signals } : {}),
859
+ });
860
+ },
771
861
  // --- task workspace management (server-side; no browser needed) ---
772
862
  profile_use: async (a) => {
773
863
  // Snapshots are keyed by profile+tab, and a profile switch routes to a
@@ -877,7 +967,7 @@ function recordHistory(tool, rawArgs, ok, extra = {}) {
877
967
  */
878
968
  const RETRY_SAFE_TOOLS = new Set([
879
969
  'tabs_list', 'chrome_status',
880
- 'get_text', 'get_html', 'snapshot', 'get_cookies',
970
+ 'get_text', 'get_html', 'snapshot', 'get_cookies', 'auth_check',
881
971
  'extract_links', 'read_as_markdown', 'screenshot',
882
972
  'wait_for', 'navigate', 'reload',
883
973
  'frames_list', 'print_pdf',
@@ -32,6 +32,10 @@ export interface Policy extends WirePolicy {
32
32
  redact?: boolean;
33
33
  /** Extra redaction patterns (regex sources) supplied by the operator. */
34
34
  redactPatterns?: string[];
35
+ /** `--fail-on-auth-wall`: every navigating tool and action fails with
36
+ * `AUTH_REQUIRED` when the page it lands on is a high-confidence sign-in
37
+ * wall, so an expired session is never scored as some other failure. */
38
+ failOnAuthWall?: boolean;
35
39
  }
36
40
  /** The SAFE default: deny everything until the user opts in. */
37
41
  export declare const DEFAULT_POLICY: Readonly<Policy>;
@@ -43,6 +43,7 @@ exports.DEFAULT_POLICY = Object.freeze({
43
43
  allowObservers: false,
44
44
  redact: false,
45
45
  redactPatterns: [],
46
+ failOnAuthWall: false,
46
47
  });
47
48
  /** Merge a partial (from a policy file and/or CLI flags) over the safe default. */
48
49
  function resolvePolicy(partial) {
@@ -57,6 +58,7 @@ function resolvePolicy(partial) {
57
58
  allowObservers: partial?.allowObservers ?? exports.DEFAULT_POLICY.allowObservers,
58
59
  redact: partial?.redact ?? exports.DEFAULT_POLICY.redact,
59
60
  redactPatterns: partial?.redactPatterns ?? [...(exports.DEFAULT_POLICY.redactPatterns ?? [])],
61
+ failOnAuthWall: partial?.failOnAuthWall ?? exports.DEFAULT_POLICY.failOnAuthWall,
60
62
  };
61
63
  }
62
64
  // ---------------------------------------------------------------------------
package/docs/BLUEPRINT.md CHANGED
@@ -350,7 +350,7 @@ export type ExecutorErrorCode =
350
350
 
351
351
  ## 5. The Complete MCP Tool Surface
352
352
 
353
- 38 tools. `readOnly` is metadata (not a JSON-Schema field) consumed by the host
353
+ 39 tools. `readOnly` is metadata (not a JSON-Schema field) consumed by the host
354
354
  and by **safe-mode** (shipped in v1, default ON). Every handler:
355
355
  `withReadyExecutor()` → validate args (`requireTarget` for selector|ref) →
356
356
  **`policy.assertUrlAllowed(currentTabUrl, method)`** → call executor/helper →
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "MCP Extension for Chrome",
4
- "version": "0.9.1",
4
+ "version": "0.9.2",
5
5
  "description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
6
6
  "homepage_url": "https://github.com/Mehmoodqureshi/chrome-mcp",
7
7
  "minimum_chrome_version": "116",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehmoodqureshi/chrome-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Drive your real Chrome browser over MCP — real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.",
5
5
  "author": "Mehmood Ur Rehman Qureshi",
6
6
  "license": "MIT",