@tickernelz/paperclip-pro-shared 2026.1025.4 → 2026.1025.6

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.
@@ -1,50 +1,70 @@
1
1
  import { z } from "zod";
2
- export const LOCAL_BROWSER_PROTOCOL_VERSION = 1;
2
+ import { LOCAL_BROWSER_ACTIONS, LOCAL_BROWSER_API_PATH_PATTERN, LOCAL_BROWSER_CLIENTS, LOCAL_BROWSER_CONTENT_SCRIPT_STATES, LOCAL_BROWSER_COOKIE_SAME_SITE, LOCAL_BROWSER_CURSOR_PATTERN, LOCAL_BROWSER_ERROR_CODES, LOCAL_BROWSER_GUARD_DEFAULTS, LOCAL_BROWSER_INTERNAL_TOOL_NAMES, LOCAL_BROWSER_KEY_ACTIONS, LOCAL_BROWSER_LIMIT_KEYS, LOCAL_BROWSER_LIMITS, LOCAL_BROWSER_MOUSE_ACTIONS, LOCAL_BROWSER_NAVIGATION_TYPES, LOCAL_BROWSER_NETWORK_BUFFER_RANGE, LOCAL_BROWSER_ORIGIN_EXEMPT_TOOL_NAMES, LOCAL_BROWSER_PERMISSION_IDS, LOCAL_BROWSER_PERMISSIONS, LOCAL_BROWSER_SITE_ACCESS, LOCAL_BROWSER_TIERS, LOCAL_BROWSER_TOOL_CAPABILITIES, LOCAL_BROWSER_TOOL_TIERS, LOCAL_BROWSER_UI_OPS, LOCAL_BROWSER_VIEWER_STATES, } from "./local-browser-contract.js";
3
+ export * from "./local-browser-contract.js";
3
4
  export const LOCAL_BROWSER_CONNECTOR_KEY = "local-browser";
4
5
  export const LOCAL_BROWSER_SKILL_NAME = "local-browser";
5
6
  export const LOCAL_BROWSER_PAIRING_TTL_MS = 10 * 60 * 1000;
6
- export const LOCAL_BROWSER_CALL_TIMEOUT_MS = 30_000;
7
- export const LOCAL_BROWSER_SNAPSHOT_DEFAULT_NODES = 120;
8
- export const LOCAL_BROWSER_SNAPSHOT_MAX_NODES = 400;
9
- export const LOCAL_BROWSER_TEXT_DEFAULT_CHARS = 8_000;
10
- export const LOCAL_BROWSER_TEXT_MAX_CHARS = 40_000;
11
- export const LOCAL_BROWSER_SCREENSHOT_MAX_BYTES = 2 * 1024 * 1024;
12
- export const LOCAL_BROWSER_VIEWER_FRAME_INTERVAL_MS = 1_000;
13
7
  /** Viewer frame and input coordinates are CSS pixels of the page viewport, never device pixels. */
14
8
  export const LOCAL_BROWSER_VIEWER_COORDINATE_SPACE = "css-pixels";
15
- export const LOCAL_BROWSER_TIERS = ["refine", "full"];
9
+ export const LOCAL_BROWSER_COOKIE_CAPABILITY = "cookies.v2";
10
+ export const LOCAL_BROWSER_UPDATE_HINT = "Update the Paperclip Local Browser extension";
16
11
  export const localBrowserTierSchema = z.enum(LOCAL_BROWSER_TIERS);
17
- export const LOCAL_BROWSER_CLIENTS = ["chromium", "firefox"];
18
12
  export const localBrowserClientSchema = z.enum(LOCAL_BROWSER_CLIENTS);
19
- export const LOCAL_BROWSER_ERROR_CODES = [
20
- "local_browser_not_paired",
21
- "local_browser_extension_unavailable",
22
- "local_browser_origin_denied",
23
- "local_browser_tier_denied",
24
- "local_browser_tab_missing",
25
- "local_browser_ref_stale",
26
- "local_browser_element_missing",
27
- "local_browser_timeout",
28
- "local_browser_navigation_blocked",
29
- "local_browser_protocol_error",
30
- "local_browser_result_too_large",
31
- "local_browser_unsupported",
32
- ];
13
+ const permissionIdSchema = z.string().min(1).max(200);
33
14
  export const localBrowserToolErrorSchema = z
34
15
  .object({
35
16
  code: z.enum(LOCAL_BROWSER_ERROR_CODES),
36
17
  message: z.string().min(1),
37
18
  hint: z.string().min(1).optional(),
38
19
  retryable: z.boolean().optional(),
20
+ permissions: z.array(permissionIdSchema).max(64).optional(),
39
21
  })
40
22
  .strict();
41
- export const LOCAL_BROWSER_CURSOR_PATTERN = /^\d+$/;
42
23
  export const localBrowserCursorSchema = z
43
24
  .string()
44
25
  .regex(LOCAL_BROWSER_CURSOR_PATTERN, "Cursors are the decimal node offset returned by the previous snapshot");
45
26
  export const localBrowserRefSchema = z
46
27
  .string()
47
28
  .regex(/^e\d+$/, "Element refs look like e12; take a fresh snapshot when a ref is rejected");
29
+ export const localBrowserAccessSchema = z.object({
30
+ siteAccess: z.enum(LOCAL_BROWSER_SITE_ACCESS),
31
+ granted: z.array(permissionIdSchema).max(200),
32
+ missing: z.array(permissionIdSchema).max(200),
33
+ pending: z
34
+ .array(z.object({
35
+ permission: permissionIdSchema,
36
+ reason: z.string().max(300),
37
+ tool: z.string().min(1).max(80).optional(),
38
+ at: z.number(),
39
+ }))
40
+ .max(50),
41
+ });
42
+ export const localBrowserGuardsSchema = z.object({
43
+ protectPaperclipTab: z.boolean().default(LOCAL_BROWSER_GUARD_DEFAULTS.protectPaperclipTab),
44
+ protectFocusedTab: z.boolean().default(LOCAL_BROWSER_GUARD_DEFAULTS.protectFocusedTab),
45
+ networkBufferSize: z
46
+ .number()
47
+ .int()
48
+ .min(LOCAL_BROWSER_NETWORK_BUFFER_RANGE.min)
49
+ .max(LOCAL_BROWSER_NETWORK_BUFFER_RANGE.max)
50
+ .default(LOCAL_BROWSER_GUARD_DEFAULTS.networkBufferSize),
51
+ });
52
+ export const localBrowserDocumentStateSchema = z.object({
53
+ documentId: z.string(),
54
+ url: z.string(),
55
+ readyState: z.string(),
56
+ visibilityState: z.string(),
57
+ navigationType: z.enum(LOCAL_BROWSER_NAVIGATION_TYPES),
58
+ restoredFromBfcache: z.boolean(),
59
+ wasDiscarded: z.boolean(),
60
+ prerendering: z.boolean(),
61
+ loadedAt: z.number(),
62
+ ageMs: z.number(),
63
+ requestsSinceLoad: z.number(),
64
+ xhrSinceLoad: z.number(),
65
+ stale: z.boolean(),
66
+ staleReasons: z.array(z.string()),
67
+ });
48
68
  export const localBrowserTabSchema = z
49
69
  .object({
50
70
  tabId: z.number().int().nonnegative(),
@@ -53,6 +73,13 @@ export const localBrowserTabSchema = z
53
73
  url: z.string(),
54
74
  active: z.boolean(),
55
75
  status: z.string().optional(),
76
+ cookieStoreId: z.string().optional(),
77
+ containerName: z.string().optional(),
78
+ incognito: z.boolean().optional(),
79
+ discarded: z.boolean().optional(),
80
+ frozen: z.boolean().optional(),
81
+ pinned: z.boolean().optional(),
82
+ audible: z.boolean().optional(),
56
83
  })
57
84
  .strict();
58
85
  export const localBrowserSessionSchema = z
@@ -69,19 +96,55 @@ export const localBrowserSessionSchema = z
69
96
  lastSeenAt: z.string(),
70
97
  tabs: z.array(localBrowserTabSchema),
71
98
  controlledByViewer: z.boolean(),
99
+ capabilities: z.array(z.string()).default([]),
100
+ access: localBrowserAccessSchema.optional(),
101
+ guards: localBrowserGuardsSchema.optional(),
72
102
  })
73
103
  .strict();
104
+ function limitField(key) {
105
+ const range = LOCAL_BROWSER_LIMITS[key];
106
+ return z.number().int().min(range.min).max(range.ceiling).default(range.default);
107
+ }
108
+ export const localBrowserLimitsSchema = z
109
+ .object({
110
+ resultMaxChars: limitField("resultMaxChars"),
111
+ snapshotMaxNodes: limitField("snapshotMaxNodes"),
112
+ readMaxChars: limitField("readMaxChars"),
113
+ screenshotMaxBytes: limitField("screenshotMaxBytes"),
114
+ transferMaxBytes: limitField("transferMaxBytes"),
115
+ callMaxMs: limitField("callMaxMs"),
116
+ networkMaxEntries: limitField("networkMaxEntries"),
117
+ })
118
+ .strict();
119
+ export const LOCAL_BROWSER_LIMIT_DEFAULTS = Object.fromEntries(LOCAL_BROWSER_LIMIT_KEYS.map((key) => [key, LOCAL_BROWSER_LIMITS[key].default]));
120
+ export const localBrowserLimitedSchema = z.array(z.object({ field: z.string(), requested: z.number(), applied: z.number() }).strict());
121
+ const ceiling = (key) => LOCAL_BROWSER_LIMITS[key].ceiling;
122
+ const timeoutSchema = z.number().int().min(100).max(ceiling("callMaxMs"));
74
123
  export const localBrowserTargetSchema = z
75
124
  .object({
76
- sessionId: z.string().uuid().optional(),
77
- tabId: z.number().int().nonnegative().optional(),
125
+ sessionId: z.string().uuid().optional().describe("Paired browser session id; omit when one browser is paired"),
126
+ tabId: z.number().int().nonnegative().optional().describe("Tab id from local_browser_sessions; omit for the active tab"),
78
127
  })
79
128
  .strict();
129
+ const sessionOnlySchema = z
130
+ .object({
131
+ sessionId: z.string().uuid().optional().describe("Paired browser session id; omit when one browser is paired"),
132
+ })
133
+ .strict();
134
+ export const localBrowserSessionsInputSchema = localBrowserTargetSchema;
80
135
  export const localBrowserOpenInputSchema = localBrowserTargetSchema
81
136
  .extend({
82
- url: z.string().url().optional(),
83
- newTab: z.boolean().optional(),
84
- active: z.boolean().optional(),
137
+ url: z.string().url().optional().describe("Absolute http(s) URL"),
138
+ newTab: z.boolean().optional().describe("Accepted for compatibility; a url reuses your own tab when you have one"),
139
+ active: z.boolean().optional().describe("Focus the tab when opening it"),
140
+ cookieStoreId: z
141
+ .string()
142
+ .min(1)
143
+ .max(200)
144
+ .optional()
145
+ .describe("Open the tab in this cookie jar, for example a Firefox container id from local_browser_context"),
146
+ incognito: z.boolean().optional().describe("Open in a private window; the extension must be allowed in private windows"),
147
+ newWindow: z.boolean().optional().describe("Open in a new window instead of a tab"),
85
148
  })
86
149
  .strict();
87
150
  export const localBrowserNavigateInputSchema = localBrowserTargetSchema
@@ -91,44 +154,41 @@ export const localBrowserNavigateInputSchema = localBrowserTargetSchema
91
154
  forward: z.boolean().optional(),
92
155
  reload: z.boolean().optional(),
93
156
  waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional(),
94
- timeoutMs: z.number().int().min(100).max(120_000).optional(),
157
+ timeoutMs: timeoutSchema.optional().describe("Default 15000; capped by the company call limit"),
95
158
  })
96
159
  .strict();
97
160
  export const localBrowserSnapshotInputSchema = localBrowserTargetSchema
98
161
  .extend({
99
- selector: z.string().min(1).optional(),
100
- maxNodes: z.number().int().min(10).max(LOCAL_BROWSER_SNAPSHOT_MAX_NODES).optional(),
101
- includeText: z.boolean().optional(),
162
+ selector: z.string().min(1).optional().describe("Scope the snapshot to a subtree"),
163
+ maxNodes: z
164
+ .number()
165
+ .int()
166
+ .min(10)
167
+ .max(ceiling("snapshotMaxNodes"))
168
+ .optional()
169
+ .describe("Default 120; capped by the company snapshot limit"),
170
+ includeText: z.boolean().optional().describe("Include short text content per node"),
102
171
  includeHidden: z.boolean().optional(),
103
- cursor: z.string().min(1).optional(),
172
+ cursor: z.string().min(1).optional().describe("Continue after the previous page"),
104
173
  })
105
174
  .strict();
106
- export const LOCAL_BROWSER_ACTIONS = [
107
- "click",
108
- "dblclick",
109
- "type",
110
- "press",
111
- "hover",
112
- "focus",
113
- "check",
114
- "uncheck",
115
- "select",
116
- "scroll",
117
- "scroll_into_view",
118
- ];
119
175
  export const localBrowserActInputSchema = localBrowserTargetSchema
120
176
  .extend({
121
177
  action: z.enum(LOCAL_BROWSER_ACTIONS),
122
- ref: localBrowserRefSchema.optional(),
123
- selector: z.string().min(1).optional(),
124
- text: z.string().optional(),
125
- key: z.string().min(1).optional(),
126
- value: z.string().optional(),
127
- values: z.array(z.string()).max(50).optional(),
128
- deltaY: z.number().optional(),
178
+ ref: localBrowserRefSchema.optional().describe("Element ref from the latest snapshot, for example e12"),
179
+ selector: z.string().min(1).optional().describe("CSS selector when you have no ref"),
180
+ text: z.string().optional().describe("Text for type; replaces the field value unless submit is set"),
181
+ key: z.string().min(1).optional().describe("Key for press, for example Enter"),
182
+ value: z.string().optional().describe("Option value for select"),
183
+ values: z.array(z.string()).max(1_000).optional().describe("Multiple option values for select"),
184
+ deltaY: z.number().optional().describe("Scroll distance; positive is down"),
129
185
  deltaX: z.number().optional(),
130
- submit: z.boolean().optional(),
131
- timeoutMs: z.number().int().min(100).max(60_000).optional(),
186
+ submit: z.boolean().optional().describe("Press Enter after typing"),
187
+ force: z
188
+ .boolean()
189
+ .optional()
190
+ .describe("Act even when another element covers the target; use after a refusal that says the element is covered"),
191
+ timeoutMs: timeoutSchema.optional(),
132
192
  })
133
193
  .strict()
134
194
  .superRefine((value, ctx) => {
@@ -149,7 +209,13 @@ export const localBrowserReadInputSchema = localBrowserTargetSchema
149
209
  .extend({
150
210
  selector: z.string().min(1).optional(),
151
211
  format: z.enum(["text", "markdown", "html"]).optional(),
152
- maxChars: z.number().int().min(200).max(LOCAL_BROWSER_TEXT_MAX_CHARS).optional(),
212
+ maxChars: z
213
+ .number()
214
+ .int()
215
+ .min(200)
216
+ .max(ceiling("readMaxChars"))
217
+ .optional()
218
+ .describe("Default 8000; capped by the company read limit"),
153
219
  })
154
220
  .strict();
155
221
  export const localBrowserScreenshotInputSchema = localBrowserTargetSchema
@@ -164,20 +230,19 @@ export const localBrowserWaitInputSchema = localBrowserTargetSchema
164
230
  text: z.string().min(1).optional(),
165
231
  urlIncludes: z.string().min(1).optional(),
166
232
  idle: z.boolean().optional(),
167
- timeoutMs: z.number().int().min(100).max(120_000).optional(),
233
+ timeoutMs: timeoutSchema.optional().describe("Default 10000; capped by the company call limit"),
168
234
  })
169
235
  .strict();
170
236
  export const localBrowserEvaluateInputSchema = localBrowserTargetSchema
171
237
  .extend({
172
- expression: z.string().min(1),
238
+ expression: z.string().min(1).describe("An expression or a function body; return a JSON-serializable value"),
173
239
  world: z.enum(["main", "isolated"]).optional(),
174
240
  awaitPromise: z.boolean().optional(),
175
- timeoutMs: z.number().int().min(100).max(60_000).optional(),
241
+ timeoutMs: timeoutSchema.optional(),
176
242
  })
177
243
  .strict();
178
- export const LOCAL_BROWSER_COOKIE_SAME_SITE = ["no_restriction", "lax", "strict", "unspecified"];
179
- export const LOCAL_BROWSER_COOKIE_CAPABILITY = "cookies.v2";
180
244
  const localBrowserDomainPattern = /^\.?(?!-)[a-z0-9-]{1,63}(?:\.(?!-)[a-z0-9-]{1,63})*\.?$/i;
245
+ const COOKIE_ENTRY_KEYS = ["domain", "path", "secure", "httpOnly", "sameSite", "expirationDate", "storeId", "session"];
181
246
  const localBrowserCookieEntrySchema = z
182
247
  .object({
183
248
  name: z.string().min(1),
@@ -187,7 +252,8 @@ const localBrowserCookieEntrySchema = z
187
252
  secure: z.boolean().optional(),
188
253
  httpOnly: z.boolean().optional(),
189
254
  sameSite: z.enum(LOCAL_BROWSER_COOKIE_SAME_SITE).optional(),
190
- expirationDate: z.number().positive().optional(),
255
+ expirationDate: z.number().positive().optional().describe("Seconds since the Unix epoch; omit for a session cookie"),
256
+ session: z.boolean().optional().describe("true writes a session cookie that ends with the browser; omit expirationDate"),
191
257
  storeId: z.string().min(1).optional(),
192
258
  })
193
259
  .strict();
@@ -201,13 +267,14 @@ export const localBrowserStorageInputSchema = localBrowserTargetSchema
201
267
  .min(1)
202
268
  .max(253)
203
269
  .regex(localBrowserDomainPattern, "domain must be a bare host name, without a scheme, wildcard or path")
204
- .optional(),
205
- allDomains: z.boolean().optional(),
206
- storeId: z.string().min(1).optional(),
270
+ .optional()
271
+ .describe("Bare host such as example.com or .example.com; includes subdomains"),
272
+ allDomains: z.boolean().optional().describe("Whole cookie jar; needs confirmAll with op clear"),
273
+ storeId: z.string().min(1).optional().describe("Cookie store for reads and the default for writes"),
207
274
  name: z.string().min(1).optional(),
208
- limit: z.number().int().min(1).max(500).optional(),
275
+ limit: z.number().int().min(1).max(100_000).optional().describe("Maximum cookies returned by get; default all"),
209
276
  confirmAll: z.boolean().optional(),
210
- entries: z.array(localBrowserCookieEntrySchema).max(200).optional(),
277
+ entries: z.array(localBrowserCookieEntrySchema).max(1_000).optional(),
211
278
  })
212
279
  .strict()
213
280
  .superRefine((value, ctx) => {
@@ -218,7 +285,7 @@ export const localBrowserStorageInputSchema = localBrowserTargetSchema
218
285
  }
219
286
  }
220
287
  for (const [index, entry] of (value.entries ?? []).entries()) {
221
- for (const key of ["domain", "path", "secure", "httpOnly", "sameSite", "expirationDate", "storeId"]) {
288
+ for (const key of COOKIE_ENTRY_KEYS) {
222
289
  if (entry[key] !== undefined) {
223
290
  ctx.addIssue({ code: "custom", path: ["entries", index, key], message: key + " applies to cookies only" });
224
291
  }
@@ -226,6 +293,15 @@ export const localBrowserStorageInputSchema = localBrowserTargetSchema
226
293
  }
227
294
  return;
228
295
  }
296
+ for (const [index, entry] of (value.entries ?? []).entries()) {
297
+ if (entry.session === true && entry.expirationDate !== undefined) {
298
+ ctx.addIssue({
299
+ code: "custom",
300
+ path: ["entries", index, "expirationDate"],
301
+ message: "A session cookie has no expirationDate",
302
+ });
303
+ }
304
+ }
229
305
  if (value.limit !== undefined && value.op !== "get") {
230
306
  ctx.addIssue({ code: "custom", path: ["limit"], message: "limit is for get only" });
231
307
  }
@@ -257,8 +333,21 @@ export const localBrowserStorageInputSchema = localBrowserTargetSchema
257
333
  export const localBrowserNetworkInputSchema = localBrowserTargetSchema
258
334
  .extend({
259
335
  op: z.enum(["list", "clear"]),
260
- limit: z.number().int().min(1).max(200).optional(),
336
+ limit: z
337
+ .number()
338
+ .int()
339
+ .min(1)
340
+ .max(ceiling("networkMaxEntries"))
341
+ .optional()
342
+ .describe("Maximum requests returned; capped by the company network limit"),
261
343
  includeBodies: z.boolean().optional(),
344
+ includeHeaders: z.boolean().optional().describe("Return request and response headers; default true"),
345
+ since: z.number().nonnegative().optional().describe("Only requests that started at or after this epoch millisecond"),
346
+ types: z
347
+ .array(z.string().min(1).max(40))
348
+ .max(40)
349
+ .optional()
350
+ .describe("Resource types to keep, for example xmlhttprequest, fetch, document, script"),
262
351
  urlIncludes: z.string().min(1).optional(),
263
352
  method: z.string().min(1).optional(),
264
353
  })
@@ -267,7 +356,7 @@ export const localBrowserUploadInputSchema = localBrowserTargetSchema
267
356
  .extend({
268
357
  ref: localBrowserRefSchema.optional(),
269
358
  selector: z.string().min(1).optional(),
270
- attachmentIds: z.array(z.string().uuid()).min(1).max(20),
359
+ attachmentIds: z.array(z.string().uuid()).min(1).max(100),
271
360
  })
272
361
  .strict();
273
362
  export const localBrowserDownloadInputSchema = localBrowserTargetSchema
@@ -279,279 +368,191 @@ export const localBrowserDownloadInputSchema = localBrowserTargetSchema
279
368
  export const localBrowserCloseInputSchema = localBrowserTargetSchema
280
369
  .extend({ closeSession: z.boolean().optional() })
281
370
  .strict();
371
+ export const localBrowserContextInputSchema = localBrowserTargetSchema
372
+ .extend({
373
+ cookies: z.boolean().optional().describe("List the tab's cookies without their values; default true"),
374
+ })
375
+ .strict();
376
+ export const localBrowserPermissionIdSchema = z.enum(LOCAL_BROWSER_PERMISSION_IDS);
377
+ export const localBrowserPermissionsInputSchema = sessionOnlySchema
378
+ .extend({
379
+ op: z.enum(["status", "request"]),
380
+ permissions: z
381
+ .array(localBrowserPermissionIdSchema)
382
+ .min(1)
383
+ .max(40)
384
+ .optional()
385
+ .describe("Permission ids to request, from the missing list of op status"),
386
+ reason: z.string().min(1).max(300).optional().describe("One sentence the user sees next to the request"),
387
+ })
388
+ .strict()
389
+ .superRefine((value, ctx) => {
390
+ if (value.op === "request" && !value.permissions?.length) {
391
+ ctx.addIssue({ code: "custom", path: ["permissions"], message: "request needs permissions; call op status to see what is missing" });
392
+ }
393
+ if (value.op === "status" && value.permissions !== undefined) {
394
+ ctx.addIssue({ code: "custom", path: ["permissions"], message: "permissions is for op request only" });
395
+ }
396
+ });
397
+ export const localBrowserApiInputSchema = sessionOnlySchema
398
+ .extend({
399
+ path: z
400
+ .string()
401
+ .max(200)
402
+ .regex(LOCAL_BROWSER_API_PATH_PATTERN, "path is a dotted browser API name such as history.search")
403
+ .describe("Browser extension API function, for example history.search or tabs.query"),
404
+ args: z.array(z.unknown()).max(20).optional().describe("Arguments passed to the function, as JSON"),
405
+ })
406
+ .strict();
407
+ export const localBrowserUiInputSchema = z.object({ op: z.enum(LOCAL_BROWSER_UI_OPS) }).strict();
408
+ export const localBrowserNetworkRequestSchema = z.object({
409
+ requestHeaders: z.record(z.string(), z.string()).optional(),
410
+ responseHeaders: z.record(z.string(), z.string()).optional(),
411
+ hasCookie: z.boolean(),
412
+ hasAuthorization: z.boolean(),
413
+ fromCache: z.boolean().optional(),
414
+ initiator: z.string().optional(),
415
+ error: z.string().optional(),
416
+ documentId: z.string().optional(),
417
+ });
418
+ export const localBrowserNetworkCaptureSchema = z.object({
419
+ mode: z.enum(["continuous", "window"]),
420
+ since: z.number(),
421
+ windowMs: z.number().optional(),
422
+ });
423
+ export const localBrowserCookieSchema = z.object({
424
+ name: z.string(),
425
+ value: z.string(),
426
+ domain: z.string(),
427
+ path: z.string(),
428
+ secure: z.boolean(),
429
+ httpOnly: z.boolean(),
430
+ sameSite: z.string(),
431
+ session: z.boolean(),
432
+ expirationDate: z.number().optional(),
433
+ hostOnly: z.boolean(),
434
+ storeId: z.string(),
435
+ partitionKey: z.unknown().optional(),
436
+ });
437
+ export const localBrowserContextResultSchema = z.object({
438
+ tab: localBrowserTabSchema,
439
+ container: z.object({
440
+ cookieStoreId: z.string(),
441
+ name: z.string().optional(),
442
+ color: z.string().optional(),
443
+ isDefault: z.boolean(),
444
+ }),
445
+ cookies: z.object({
446
+ storeId: z.string(),
447
+ url: z.string(),
448
+ count: z.number(),
449
+ items: z.array(z.object({
450
+ name: z.string(),
451
+ domain: z.string(),
452
+ path: z.string(),
453
+ session: z.boolean(),
454
+ expirationDate: z.number().optional(),
455
+ httpOnly: z.boolean(),
456
+ secure: z.boolean(),
457
+ sameSite: z.string(),
458
+ hostOnly: z.boolean(),
459
+ })),
460
+ }),
461
+ document: localBrowserDocumentStateSchema.nullable(),
462
+ contentScript: z.enum(LOCAL_BROWSER_CONTENT_SCRIPT_STATES),
463
+ network: z.object({
464
+ capturing: z.boolean(),
465
+ lastRequestAt: z.number().optional(),
466
+ requestsSinceLoad: z.number().optional(),
467
+ }),
468
+ access: localBrowserAccessSchema,
469
+ });
470
+ export const localBrowserPermissionsResultSchema = z.object({
471
+ granted: z.array(z.string()),
472
+ missing: z.array(z.string()),
473
+ pending: localBrowserAccessSchema.shape.pending,
474
+ siteAccess: z.enum(LOCAL_BROWSER_SITE_ACCESS),
475
+ requested: z.array(z.string()).optional(),
476
+ alreadyGranted: z.array(z.string()).optional(),
477
+ note: z.string(),
478
+ });
479
+ export const localBrowserApiResultSchema = z.object({ value: z.unknown(), type: z.string() });
480
+ const ORIGIN_EXEMPT = new Set(LOCAL_BROWSER_ORIGIN_EXEMPT_TOOL_NAMES);
481
+ function defineTool(name, description, schema) {
482
+ const { $schema: _schema, ...inputSchema } = z.toJSONSchema(schema, { io: "input" });
483
+ return {
484
+ name,
485
+ description,
486
+ schema,
487
+ inputSchema,
488
+ tier: LOCAL_BROWSER_TOOL_TIERS[name],
489
+ capability: LOCAL_BROWSER_TOOL_CAPABILITIES[name] ?? null,
490
+ originExempt: ORIGIN_EXEMPT.has(name),
491
+ };
492
+ }
282
493
  const targetHint = "Omit sessionId when exactly one browser is paired; omit tabId to use the active tab.";
283
494
  export const LOCAL_BROWSER_TOOLS = [
284
- {
285
- name: "local_browser_sessions",
286
- description: `List the paired browsers and their open tabs. ${targetHint} Start here when you do not know the current tab.`,
287
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
288
- tier: "refine",
289
- },
290
- {
291
- name: "local_browser_open",
292
- description: "Open a URL in the user's real browser. It reuses the tab you already opened for this work, and creates one only when you have none, so repeated opens never pile up tabs. Never navigates the tab the user is looking at or the Paperclip tab itself.",
293
- inputSchema: {
294
- type: "object",
295
- properties: {
296
- sessionId: { type: "string", description: "Paired browser session id" },
297
- tabId: { type: "integer", description: "Target this tab instead of your own; refused for the tab the user is looking at and for the Paperclip tab" },
298
- url: { type: "string", description: "Absolute http(s) URL" },
299
- newTab: { type: "boolean", description: "Accepted for compatibility; a url reuses your own tab when you have one" },
300
- active: { type: "boolean", description: "Focus the tab when opening it" },
301
- },
302
- additionalProperties: false,
303
- },
304
- tier: "refine",
305
- },
306
- {
307
- name: "local_browser_navigate",
308
- description: "Navigate the current tab: pass url, or one of back, forward, reload. Prefer this over local_browser_open when the tab is already on the site you need. Refused for the tab the user is looking at and for the Paperclip tab itself; call local_browser_open for those.",
309
- inputSchema: {
310
- type: "object",
311
- properties: {
312
- sessionId: { type: "string" },
313
- tabId: { type: "integer" },
314
- url: { type: "string" },
315
- back: { type: "boolean" },
316
- forward: { type: "boolean" },
317
- reload: { type: "boolean" },
318
- waitUntil: { type: "string", enum: ["load", "domcontentloaded", "networkidle"] },
319
- timeoutMs: { type: "integer", description: "Default 15000, max 120000" },
320
- },
321
- additionalProperties: false,
322
- },
323
- tier: "refine",
324
- },
325
- {
326
- name: "local_browser_snapshot",
327
- description: "Read the page as a compact accessibility tree where every interactive element has a ref like e12. This is the primary way to see a page: call it after every navigation or click, then pass those refs to local_browser_act. Output is capped; pass cursor from the previous result to continue.",
328
- inputSchema: {
329
- type: "object",
330
- properties: {
331
- sessionId: { type: "string" },
332
- tabId: { type: "integer" },
333
- selector: { type: "string", description: "Scope the snapshot to a subtree" },
334
- maxNodes: { type: "integer", description: "Default 120, max 400" },
335
- includeText: { type: "boolean", description: "Include short text content per node" },
336
- includeHidden: { type: "boolean" },
337
- cursor: { type: "string", description: "Continue after the previous page" },
338
- },
339
- additionalProperties: false,
340
- },
341
- tier: "refine",
342
- },
343
- {
344
- name: "local_browser_act",
345
- description: "Do one thing on the page: click, dblclick, type, press, hover, focus, check, uncheck, select, scroll, scroll_into_view. Target an element with ref from the latest snapshot, or a CSS selector. After a click or press that changes the page, take a new snapshot because old refs go stale.",
346
- inputSchema: {
347
- type: "object",
348
- properties: {
349
- sessionId: { type: "string" },
350
- tabId: { type: "integer" },
351
- action: {
352
- type: "string",
353
- enum: ["click", "dblclick", "type", "press", "hover", "focus", "check", "uncheck", "select", "scroll", "scroll_into_view"],
354
- },
355
- ref: { type: "string", description: "Element ref from the latest snapshot, for example e12" },
356
- selector: { type: "string", description: "CSS selector when you have no ref" },
357
- text: { type: "string", description: "Text for type; replaces the field value unless submit is set" },
358
- key: { type: "string", description: "Key for press, for example Enter" },
359
- value: { type: "string", description: "Option value for select" },
360
- values: { type: "array", items: { type: "string" }, description: "Multiple option values for select" },
361
- deltaY: { type: "number", description: "Scroll distance; positive is down" },
362
- deltaX: { type: "number" },
363
- submit: { type: "boolean", description: "Press Enter after typing" },
364
- timeoutMs: { type: "integer" },
365
- },
366
- additionalProperties: false,
367
- },
368
- tier: "refine",
369
- },
370
- {
371
- name: "local_browser_read",
372
- description: "Extract text, markdown or html from the page or one element. Use this instead of a snapshot when you need the content itself rather than the controls. Output is capped at maxChars; the result says when it was truncated.",
373
- inputSchema: {
374
- type: "object",
375
- properties: {
376
- sessionId: { type: "string" },
377
- tabId: { type: "integer" },
378
- selector: { type: "string" },
379
- format: { type: "string", enum: ["text", "markdown", "html"] },
380
- maxChars: { type: "integer", description: "Default 8000, max 40000" },
381
- },
382
- additionalProperties: false,
383
- },
384
- tier: "refine",
385
- },
386
- {
387
- name: "local_browser_screenshot",
388
- description: "Capture the visible tab, or one element, as an image attachment for the human. Use it for visual checks, layout problems and canvas or image content that a snapshot cannot describe.",
389
- inputSchema: {
390
- type: "object",
391
- properties: {
392
- sessionId: { type: "string" },
393
- tabId: { type: "integer" },
394
- selector: { type: "string" },
395
- fullPage: { type: "boolean" },
396
- },
397
- additionalProperties: false,
398
- },
399
- tier: "refine",
400
- },
401
- {
402
- name: "local_browser_wait",
403
- description: "Wait for a condition before acting: a selector appears, text appears, the URL contains a string, or the page is idle. Prefer this over repeating snapshots in a loop.",
404
- inputSchema: {
405
- type: "object",
406
- properties: {
407
- sessionId: { type: "string" },
408
- tabId: { type: "integer" },
409
- selector: { type: "string" },
410
- text: { type: "string" },
411
- urlIncludes: { type: "string" },
412
- idle: { type: "boolean" },
413
- timeoutMs: { type: "integer", description: "Default 10000, max 120000" },
414
- },
415
- additionalProperties: false,
416
- },
417
- tier: "refine",
418
- },
419
- {
420
- name: "local_browser_evaluate",
421
- description: "Run JavaScript in the page and return its value as JSON. Full-power tier only. The page is the user's real browser: read what you need, do not exfiltrate credentials, and prefer snapshot and act for ordinary interaction.",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- sessionId: { type: "string" },
426
- tabId: { type: "integer" },
427
- expression: { type: "string", description: "An expression or a function body; return a JSON-serializable value" },
428
- world: { type: "string", enum: ["main", "isolated"] },
429
- awaitPromise: { type: "boolean" },
430
- timeoutMs: { type: "integer" },
431
- },
432
- required: ["expression"],
433
- additionalProperties: false,
434
- },
435
- tier: "full",
436
- },
437
- {
438
- name: "local_browser_storage",
439
- description: "Read or write cookies, localStorage or sessionStorage for the page. Cookies can be scoped by url, by domain (includes subdomains) or with allDomains for the whole jar, and can be written with domain, path, secure, httpOnly, sameSite and expiry. Full-power tier only. Never print secret values into the task; report only what the task needs.",
440
- inputSchema: {
441
- type: "object",
442
- properties: {
443
- sessionId: { type: "string" },
444
- tabId: { type: "integer" },
445
- kind: { type: "string", enum: ["cookies", "local", "session"] },
446
- op: { type: "string", enum: ["get", "set", "clear"] },
447
- url: { type: "string" },
448
- domain: { type: "string", description: "Bare host such as example.com or .example.com; includes subdomains" },
449
- allDomains: { type: "boolean", description: "Whole cookie jar; needs confirmAll with op clear" },
450
- storeId: { type: "string", description: "Cookie store for reads and the default for writes" },
451
- name: { type: "string" },
452
- limit: { type: "integer", description: "Maximum cookies returned by get (default 200)" },
453
- confirmAll: { type: "boolean" },
454
- entries: {
455
- type: "array",
456
- items: {
457
- type: "object",
458
- properties: {
459
- name: { type: "string" },
460
- value: { type: "string" },
461
- domain: { type: "string" },
462
- path: { type: "string" },
463
- secure: { type: "boolean" },
464
- httpOnly: { type: "boolean" },
465
- sameSite: { type: "string", enum: [...LOCAL_BROWSER_COOKIE_SAME_SITE] },
466
- expirationDate: { type: "number", description: "Seconds since the Unix epoch; omit for a session cookie" },
467
- storeId: { type: "string" },
468
- },
469
- required: ["name", "value"],
470
- additionalProperties: false,
471
- },
472
- },
473
- },
474
- required: ["kind", "op"],
475
- additionalProperties: false,
476
- },
477
- tier: "full",
478
- },
479
- {
480
- name: "local_browser_network",
481
- description: "List recent network requests of the tab, optionally with response bodies. Full-power tier only. Use it to diagnose a failing call instead of guessing from the page.",
482
- inputSchema: {
483
- type: "object",
484
- properties: {
485
- sessionId: { type: "string" },
486
- tabId: { type: "integer" },
487
- op: { type: "string", enum: ["list", "clear"] },
488
- limit: { type: "integer", description: "Default 50, max 200" },
489
- includeBodies: { type: "boolean" },
490
- urlIncludes: { type: "string" },
491
- method: { type: "string" },
492
- },
493
- required: ["op"],
494
- additionalProperties: false,
495
- },
496
- tier: "full",
497
- },
498
- {
499
- name: "local_browser_upload",
500
- description: "Attach files from the task to a file input on the page. Full-power tier only. Pass attachmentIds from the task's attachments.",
501
- inputSchema: {
502
- type: "object",
503
- properties: {
504
- sessionId: { type: "string" },
505
- tabId: { type: "integer" },
506
- ref: { type: "string" },
507
- selector: { type: "string" },
508
- attachmentIds: { type: "array", items: { type: "string" } },
509
- },
510
- required: ["attachmentIds"],
511
- additionalProperties: false,
512
- },
513
- tier: "full",
514
- },
515
- {
516
- name: "local_browser_download",
517
- description: "List the tab's downloads, or save one into the task as an attachment. Full-power tier only.",
518
- inputSchema: {
519
- type: "object",
520
- properties: {
521
- sessionId: { type: "string" },
522
- tabId: { type: "integer" },
523
- op: { type: "string", enum: ["list", "save"] },
524
- downloadId: { type: "integer" },
525
- },
526
- required: ["op"],
527
- additionalProperties: false,
528
- },
529
- tier: "full",
530
- },
531
- {
532
- name: "local_browser_close",
533
- description: "Close a tab, or the whole paired browser session, when you are done. Closing a session does not sign the user out; it disconnects Paperclip from that browser.",
534
- inputSchema: {
535
- type: "object",
536
- properties: {
537
- sessionId: { type: "string" },
538
- tabId: { type: "integer" },
539
- closeSession: { type: "boolean" },
540
- },
541
- additionalProperties: false,
542
- },
543
- tier: "refine",
544
- },
495
+ defineTool("local_browser_sessions", "List the paired browsers with their open tabs, capabilities, granted permissions and guards. " + targetHint + " Start here when you do not know the current tab.", localBrowserSessionsInputSchema),
496
+ defineTool("local_browser_open", "Open a URL in the user's real browser. It reuses the tab you already opened for this work and creates one only when you have none, so repeated opens never pile up tabs. Pass cookieStoreId to open in a container or another cookie jar, incognito for a private window, newWindow for a separate window. Never navigates the tab the user is looking at or the Paperclip tab itself. Returns the tab and a first snapshot.", localBrowserOpenInputSchema),
497
+ defineTool("local_browser_navigate", "Navigate the current tab: pass url, or one of back, forward, reload. Prefer this over local_browser_open when the tab is already on the site you need. Refused for the tab the user is looking at and for the Paperclip tab itself; call local_browser_open for those.", localBrowserNavigateInputSchema),
498
+ defineTool("local_browser_snapshot", "Read the page as a compact accessibility tree where every interactive element has a ref like e12. This is the primary way to see a page: call it after every navigation or click, then pass those refs to local_browser_act. Pass cursor from the previous result to continue. The result carries document; when document.stale is true, reload before you trust it.", localBrowserSnapshotInputSchema),
499
+ defineTool("local_browser_act", "Do one thing on the page: click, dblclick, type, press, hover, focus, check, uncheck, select, scroll, scroll_into_view. Target an element with ref from the latest snapshot, or a CSS selector. Pass force to act on an element another element covers. After a click or press that changes the page, take a new snapshot because old refs go stale.", localBrowserActInputSchema),
500
+ defineTool("local_browser_read", "Extract text, markdown or html from the page or one element. Use this instead of a snapshot when you need the content itself rather than the controls. The result says when it was truncated and carries document state.", localBrowserReadInputSchema),
501
+ defineTool("local_browser_screenshot", "Capture the visible tab, or one element, as an image attachment for the human. Use it for visual checks, layout problems and canvas or image content that a snapshot cannot describe.", localBrowserScreenshotInputSchema),
502
+ defineTool("local_browser_wait", "Wait for a condition before acting: a selector appears, text appears, the URL contains a string, or the page is idle. Prefer this over repeating snapshots in a loop.", localBrowserWaitInputSchema),
503
+ defineTool("local_browser_evaluate", "Run JavaScript in the page and return its value as JSON. Full tier only. The page is the user's real browser: read what you need, do not exfiltrate credentials, and prefer snapshot and act for ordinary interaction.", localBrowserEvaluateInputSchema),
504
+ defineTool("local_browser_storage", "Read or write cookies, localStorage or sessionStorage. Cookies can be scoped by url, by domain (includes subdomains) or with allDomains for the whole jar, and each cookie reports session, expirationDate, httpOnly, secure, sameSite, hostOnly, storeId and partitionKey. Full tier only. Never print secret values into the task; report only what the task needs.", localBrowserStorageInputSchema),
505
+ defineTool("local_browser_network", "List the requests the tab made, with request and response headers, status, timing and optionally bodies. Capture runs continuously, so pass since or urlIncludes to narrow it. Full tier only. Use it to diagnose a failing call instead of guessing from the page.", localBrowserNetworkInputSchema),
506
+ defineTool("local_browser_upload", "Attach files from the task to a file input on the page. Full tier only. Pass attachmentIds from the task's attachments.", localBrowserUploadInputSchema),
507
+ defineTool("local_browser_download", "List the tab's downloads, or save one into the task as an attachment. Full tier only.", localBrowserDownloadInputSchema),
508
+ defineTool("local_browser_close", "Close a tab, or the whole paired browser session, when you are done. Closing a session does not sign the user out; it disconnects Paperclip from that browser.", localBrowserCloseInputSchema),
509
+ defineTool("local_browser_context", "Probe one tab before you work in it: its container and cookie jar, the cookies for its URL without values, whether the page is stale or came back from the back-forward cache, whether the content script is connected, network capture state and the browser permissions. Call it when a page behaves oddly or before cookie and network work.", localBrowserContextInputSchema),
510
+ defineTool("local_browser_permissions", "See which browser permissions the extension has, or ask the user for missing ones. op request cannot grant anything by itself: it shows the request in the extension, and you must tell the user to press Allow there. Call op status again after the user answers.", localBrowserPermissionsInputSchema),
511
+ defineTool("local_browser_api", "Call any browser extension API function directly, for example history.search, bookmarks.search, tabs.query or windows.getAll, and get its result as JSON. Full tier only. A function whose permission is missing fails with local_browser_permission_required; the extension's own storage is off limits.", localBrowserApiInputSchema),
545
512
  ];
546
513
  export const LOCAL_BROWSER_TOOL_NAMES = LOCAL_BROWSER_TOOLS.map((tool) => tool.name);
547
514
  export const LOCAL_BROWSER_FULL_TIER_TOOL_NAMES = LOCAL_BROWSER_TOOLS.filter((tool) => tool.tier === "full").map((tool) => tool.name);
515
+ const TOOLS_BY_NAME = new Map(LOCAL_BROWSER_TOOLS.map((tool) => [tool.name, tool]));
516
+ const INTERNAL_TOOLS = new Set(LOCAL_BROWSER_INTERNAL_TOOL_NAMES);
517
+ export function localBrowserTool(name) {
518
+ return TOOLS_BY_NAME.get(name) ?? null;
519
+ }
520
+ export function localBrowserIsInternalTool(name) {
521
+ return INTERNAL_TOOLS.has(name);
522
+ }
548
523
  export function localBrowserToolRequiresFullTier(name) {
549
- return LOCAL_BROWSER_FULL_TIER_TOOL_NAMES.includes(name);
524
+ return TOOLS_BY_NAME.get(name)?.tier === "full";
525
+ }
526
+ export function localBrowserPermissionsForClient(client) {
527
+ return LOCAL_BROWSER_PERMISSIONS.filter((permission) => permission[client] !== "unsupported");
550
528
  }
551
529
  export const localBrowserSettingsSchema = z
552
530
  .object({
553
531
  tier: localBrowserTierSchema.default("refine"),
554
532
  allowedOrigins: z.array(z.string().min(1)).max(200).default([]),
533
+ limits: localBrowserLimitsSchema.prefault({}),
534
+ })
535
+ .strict();
536
+ function limitRange(key) {
537
+ const range = LOCAL_BROWSER_LIMITS[key];
538
+ return z.number().int().min(range.min).max(range.ceiling).optional();
539
+ }
540
+ export const localBrowserSettingsUpdateSchema = z
541
+ .object({
542
+ tier: localBrowserTierSchema.optional(),
543
+ allowedOrigins: z.array(z.string().min(1)).max(200).optional(),
544
+ limits: z
545
+ .object({
546
+ resultMaxChars: limitRange("resultMaxChars"),
547
+ snapshotMaxNodes: limitRange("snapshotMaxNodes"),
548
+ readMaxChars: limitRange("readMaxChars"),
549
+ screenshotMaxBytes: limitRange("screenshotMaxBytes"),
550
+ transferMaxBytes: limitRange("transferMaxBytes"),
551
+ callMaxMs: limitRange("callMaxMs"),
552
+ networkMaxEntries: limitRange("networkMaxEntries"),
553
+ })
554
+ .strict()
555
+ .optional(),
555
556
  })
556
557
  .strict();
557
558
  export function localBrowserOriginOf(rawUrl) {
@@ -586,8 +587,6 @@ export function localBrowserOriginAllowed(settings, rawUrl) {
586
587
  return normalized.toLowerCase() === origin.toLowerCase();
587
588
  });
588
589
  }
589
- export const LOCAL_BROWSER_MOUSE_ACTIONS = ["move", "down", "up"];
590
- export const LOCAL_BROWSER_KEY_ACTIONS = ["down", "up"];
591
590
  export const localBrowserInputSchema = z.discriminatedUnion("kind", [
592
591
  z
593
592
  .object({
@@ -622,6 +621,7 @@ export const localBrowserInputSchema = z.discriminatedUnion("kind", [
622
621
  })
623
622
  .strict(),
624
623
  ]);
624
+ const viewerStateSchema = z.enum(LOCAL_BROWSER_VIEWER_STATES);
625
625
  export const localBrowserViewerFrameEventSchema = z
626
626
  .object({
627
627
  type: z.literal("frame"),
@@ -634,7 +634,7 @@ export const localBrowserViewerFrameEventSchema = z
634
634
  export const localBrowserViewerStateEventSchema = z
635
635
  .object({
636
636
  type: z.literal("state"),
637
- state: z.enum(["connected", "viewer_unavailable", "agent_streaming", "agent_idle", "disconnected"]),
637
+ state: viewerStateSchema,
638
638
  tabId: z.number().int().nonnegative().optional(),
639
639
  message: z.string().max(400).optional(),
640
640
  })
@@ -670,17 +670,33 @@ export const localBrowserStateEventSchema = z
670
670
  event: z.literal("state"),
671
671
  data: z
672
672
  .object({
673
- state: z.enum(["connected", "viewer_unavailable", "agent_streaming", "agent_idle", "disconnected"]),
673
+ state: viewerStateSchema,
674
674
  tabId: z.number().int().nonnegative().optional(),
675
675
  message: z.string().max(400).optional(),
676
676
  })
677
677
  .strict(),
678
678
  })
679
679
  .strict();
680
+ export const localBrowserAccessEventSchema = z
681
+ .object({
682
+ type: z.literal("event"),
683
+ event: z.literal("access"),
684
+ data: localBrowserAccessSchema,
685
+ })
686
+ .strict();
687
+ export const localBrowserGuardsEventSchema = z
688
+ .object({
689
+ type: z.literal("event"),
690
+ event: z.literal("guards"),
691
+ data: localBrowserGuardsSchema,
692
+ })
693
+ .strict();
680
694
  export const localBrowserExtensionEventFrameSchema = z.discriminatedUnion("event", [
681
695
  localBrowserTabsEventSchema,
682
696
  localBrowserFrameEventSchema,
683
697
  localBrowserStateEventSchema,
698
+ localBrowserAccessEventSchema,
699
+ localBrowserGuardsEventSchema,
684
700
  ]);
685
701
  export const LOCAL_BROWSER_PAIRING_CODE_LENGTH = 8;
686
702
  export const localBrowserPairingStartSchema = z
@@ -697,10 +713,47 @@ export const localBrowserPairingStartResultSchema = z
697
713
  })
698
714
  .strict();
699
715
  export const localBrowserBridgeFrameSchema = z.discriminatedUnion("type", [
700
- z.object({ type: z.literal("hello"), protocol: z.number().int(), client: localBrowserClientSchema, clientVersion: z.string(), label: z.string(), profileName: z.string().nullable(), tabs: z.array(localBrowserTabSchema), capabilities: z.array(z.string().min(1).max(40)).max(20).optional() }).strict(),
701
- z.object({ type: z.literal("hello_ack"), sessionId: z.string().uuid(), sessionToken: z.string(), tier: localBrowserTierSchema, allowedOrigins: z.array(z.string()) }).strict(),
702
- z.object({ type: z.literal("call"), id: z.string(), tool: z.string(), args: z.unknown() }).strict(),
703
- z.object({ type: z.literal("result"), id: z.string(), ok: z.boolean(), result: z.unknown().optional(), error: localBrowserToolErrorSchema.optional() }).strict(),
716
+ z
717
+ .object({
718
+ type: z.literal("hello"),
719
+ protocol: z.number().int(),
720
+ client: localBrowserClientSchema,
721
+ clientVersion: z.string(),
722
+ label: z.string(),
723
+ profileName: z.string().nullable(),
724
+ tabs: z.array(localBrowserTabSchema),
725
+ capabilities: z.array(z.string().min(1).max(40)).max(40).optional(),
726
+ access: localBrowserAccessSchema.optional().catch(undefined),
727
+ guards: localBrowserGuardsSchema.optional().catch(undefined),
728
+ })
729
+ .strict(),
730
+ z
731
+ .object({
732
+ type: z.literal("hello_ack"),
733
+ sessionId: z.string().uuid(),
734
+ sessionToken: z.string(),
735
+ tier: localBrowserTierSchema,
736
+ allowedOrigins: z.array(z.string()),
737
+ })
738
+ .strict(),
739
+ z
740
+ .object({
741
+ type: z.literal("call"),
742
+ id: z.string(),
743
+ tool: z.string(),
744
+ args: z.unknown(),
745
+ budgetMs: z.number().int().positive().optional(),
746
+ })
747
+ .strict(),
748
+ z
749
+ .object({
750
+ type: z.literal("result"),
751
+ id: z.string(),
752
+ ok: z.boolean(),
753
+ result: z.unknown().optional(),
754
+ error: localBrowserToolErrorSchema.optional(),
755
+ })
756
+ .strict(),
704
757
  z.object({ type: z.literal("event"), event: z.string(), data: z.unknown() }).strict(),
705
758
  z.object({ type: z.literal("ping"), at: z.string() }).strict(),
706
759
  z.object({ type: z.literal("pong"), at: z.string() }).strict(),