browser-pilot-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command, CommanderError } from "commander";
5
- import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
6
- import { resolve as resolvePath } from "path";
5
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
6
+ import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
7
7
 
8
8
  // src/version.ts
9
- var BROWSER_PILOT_VERSION = "0.4.0";
9
+ var BROWSER_PILOT_VERSION = "0.5.0";
10
10
 
11
11
  // src/protocol/errors.ts
12
12
  var ERROR_CODES = [
@@ -28,6 +28,7 @@ var ERROR_CODES = [
28
28
  "action_not_verified",
29
29
  "command_cancelled",
30
30
  "command_expired",
31
+ "wait_timeout",
31
32
  "unknown_outcome",
32
33
  "artifact_not_found",
33
34
  "artifact_expired",
@@ -73,23 +74,6 @@ function invalidArgument(message, field, rpcCode = -32602) {
73
74
  rpcCode
74
75
  });
75
76
  }
76
- function asBrowserPilotError(error) {
77
- if (error instanceof BrowserPilotError) return error;
78
- if (error instanceof Error && typeof error.code === "string" && ERROR_CODES.includes(error.code)) {
79
- const stable = error;
80
- return new BrowserPilotError(stable.code, error.message, {
81
- retryable: stable.retryable,
82
- context: stable.context,
83
- remediation: stable.remediation,
84
- rpcCode: stable.rpcCode,
85
- cause: error
86
- });
87
- }
88
- return new BrowserPilotError("internal_error", "Internal Browser Pilot error", {
89
- retryable: false,
90
- cause: error
91
- });
92
- }
93
77
  function browserPilotErrorFromJsonRpc(error) {
94
78
  const data = error.data;
95
79
  if (!data || typeof data !== "object" || Array.isArray(data)) {
@@ -110,154 +94,445 @@ function browserPilotErrorFromJsonRpc(error) {
110
94
  });
111
95
  }
112
96
 
113
- // src/session.ts
114
- import { spawn } from "child_process";
115
-
116
- // src/client.ts
117
- import http from "http";
118
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
119
-
120
- // src/broker-locator.ts
121
- import { createHash as createHash2, randomUUID } from "crypto";
122
- import {
123
- chmodSync,
124
- closeSync,
125
- existsSync,
126
- lstatSync,
127
- mkdirSync,
128
- openSync,
129
- readFileSync,
130
- renameSync,
131
- realpathSync,
132
- statSync,
133
- unlinkSync,
134
- writeFileSync,
135
- writeSync
136
- } from "fs";
137
- import { basename, dirname, isAbsolute as isAbsolute2, join as join2 } from "path";
138
-
139
- // src/paths.ts
97
+ // src/chrome.ts
140
98
  import { createHash } from "crypto";
141
- import { homedir, platform, tmpdir, userInfo } from "os";
142
- import { isAbsolute, join, win32 } from "path";
143
- var MAX_PORTABLE_UNIX_SOCKET_BYTES = 96;
144
- function identityDigest(value) {
145
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
146
- }
147
- function resolveBrowserPilotPaths(options = {}) {
148
- const os = options.platform ?? platform();
149
- const home = options.homeDir ?? homedir();
150
- const env = options.env ?? process.env;
151
- if (os === "win32") {
152
- const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
153
- const stateDir2 = env.BROWSER_PILOT_HOME ?? win32.join(localAppData, "Browser Pilot");
154
- if (!win32.isAbsolute(stateDir2)) throw new Error("BROWSER_PILOT_HOME must be absolute");
155
- const username = options.username ?? (() => {
156
- try {
157
- return userInfo().username;
158
- } catch {
159
- return home;
160
- }
161
- })();
162
- const identity = identityDigest(
163
- `${username.toLowerCase()}\0${home.toLowerCase()}\0${stateDir2.toLowerCase()}`
164
- );
99
+ import { execFileSync } from "child_process";
100
+ import { existsSync, lstatSync, readFileSync } from "fs";
101
+ import { connect as connectTcp } from "net";
102
+ import { delimiter, join } from "path";
103
+ import { homedir, platform } from "os";
104
+ import WebSocket from "ws";
105
+ function readChromeInfo(browser, dataDir) {
106
+ const portFile = join(dataDir, "DevToolsActivePort");
107
+ if (!existsSync(portFile)) return null;
108
+ try {
109
+ const lines = readFileSync(portFile, "utf-8").trim().split(/\r?\n/);
110
+ if (lines.length < 2) return null;
111
+ const port = Number.parseInt(lines[0], 10);
112
+ const wsPath = lines[1];
113
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535 || !wsPath.startsWith("/")) return null;
165
114
  return {
166
- stateDir: stateDir2,
167
- runtimeDir: stateDir2,
168
- endpoint: `\\\\.\\pipe\\browser-pilot-${identity}`,
169
- transport: "windows_pipe",
170
- locatorFile: win32.join(stateDir2, "broker-locator.json"),
171
- pidFile: win32.join(stateDir2, "daemon.pid"),
172
- startupLockFile: win32.join(stateDir2, "startup.lock"),
173
- versionHistoryFile: win32.join(stateDir2, "broker-versions.json"),
174
- artifactDir: win32.join(stateDir2, "artifacts"),
175
- downloadDir: win32.join(stateDir2, "downloads")
115
+ port,
116
+ wsPath,
117
+ wsUrl: `ws://127.0.0.1:${port}${wsPath}`,
118
+ browser,
119
+ dataDir
176
120
  };
121
+ } catch {
122
+ return null;
177
123
  }
178
- const stateDir = env.BROWSER_PILOT_HOME ?? join(home, ".browser-pilot");
179
- if (!isAbsolute(stateDir)) throw new Error("BROWSER_PILOT_HOME must be absolute");
180
- const defaultEndpoint = join(stateDir, "daemon.sock");
181
- const uid = options.uid ?? process.getuid?.() ?? 0;
182
- const useShortRuntimePath = Buffer.byteLength(defaultEndpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES;
183
- let runtimeDir = useShortRuntimePath ? join(options.tempDir ?? tmpdir(), `browser-pilot-${uid}-${identityDigest(stateDir)}`) : stateDir;
184
- let endpoint = join(runtimeDir, "daemon.sock");
185
- if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
186
- runtimeDir = join("/tmp", `browser-pilot-${uid}-${identityDigest(stateDir)}`);
187
- endpoint = join(runtimeDir, "daemon.sock");
188
- }
189
- if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
190
- throw new Error("Browser Pilot cannot resolve a portable Unix socket path");
191
- }
192
- if (!isAbsolute(endpoint)) throw new Error("Browser Pilot endpoint must be absolute");
193
- return {
194
- stateDir,
195
- runtimeDir,
196
- endpoint,
197
- transport: "unix_socket",
198
- locatorFile: join(stateDir, "broker-locator.json"),
199
- pidFile: join(stateDir, "daemon.pid"),
200
- startupLockFile: join(stateDir, "startup.lock"),
201
- versionHistoryFile: join(stateDir, "broker-versions.json"),
202
- artifactDir: join(stateDir, "artifacts"),
203
- downloadDir: join(stateDir, "downloads")
204
- };
205
124
  }
206
- var BROWSER_PILOT_PATHS = resolveBrowserPilotPaths();
207
- var STATE_DIR = BROWSER_PILOT_PATHS.stateDir;
208
- var RUNTIME_DIR = BROWSER_PILOT_PATHS.runtimeDir;
209
- var SOCKET_PATH = BROWSER_PILOT_PATHS.endpoint;
210
- var BROKER_TRANSPORT = BROWSER_PILOT_PATHS.transport;
211
- var LOCATOR_FILE = BROWSER_PILOT_PATHS.locatorFile;
212
- var PID_FILE = BROWSER_PILOT_PATHS.pidFile;
213
- var STARTUP_LOCK_FILE = BROWSER_PILOT_PATHS.startupLockFile;
214
- var ARTIFACT_DIR = BROWSER_PILOT_PATHS.artifactDir;
215
- var DOWNLOAD_DIR = BROWSER_PILOT_PATHS.downloadDir;
216
-
217
- // src/broker-locator.ts
218
- var PRIVATE_DIRECTORY_MODE = 448;
219
- var PRIVATE_FILE_MODE = 384;
220
- var MAX_METADATA_BYTES = 16 * 1024;
221
- function currentUid() {
222
- return process.getuid?.();
125
+ function executablePaths(env) {
126
+ return (env.PATH ?? "").split(delimiter).filter(Boolean);
223
127
  }
224
- function validateOwnedPath(path, expectDirectory) {
225
- const stats = lstatSync(path);
226
- if (stats.isSymbolicLink()) throw new Error(`Browser Pilot path must not be a symbolic link: ${path}`);
227
- if (expectDirectory ? !stats.isDirectory() : !stats.isFile()) {
228
- throw new Error(`Browser Pilot path has an unexpected type: ${path}`);
128
+ function linuxInstallPaths(env, names) {
129
+ return executablePaths(env).flatMap((directory) => names.map((name) => join(directory, name)));
130
+ }
131
+ function supportedBrowserProfiles(options = {}) {
132
+ const home = options.homeDir ?? homedir();
133
+ const os = options.platform ?? platform();
134
+ const env = options.env ?? process.env;
135
+ if (os === "darwin") {
136
+ const base = join(home, "Library", "Application Support");
137
+ const applications = ["/Applications", join(home, "Applications")];
138
+ const appPaths = (name) => applications.map((directory) => join(directory, name));
139
+ return [
140
+ {
141
+ key: "chrome-stable",
142
+ product: "Chrome",
143
+ channel: "stable",
144
+ dataDir: join(base, "Google", "Chrome"),
145
+ installPaths: appPaths("Google Chrome.app"),
146
+ executableNames: ["Google Chrome"]
147
+ },
148
+ {
149
+ key: "chrome-beta",
150
+ product: "Chrome",
151
+ channel: "beta",
152
+ dataDir: join(base, "Google", "Chrome Beta"),
153
+ installPaths: appPaths("Google Chrome Beta.app"),
154
+ executableNames: ["Google Chrome Beta"]
155
+ },
156
+ {
157
+ key: "chrome-canary",
158
+ product: "Chrome",
159
+ channel: "canary",
160
+ dataDir: join(base, "Google", "Chrome Canary"),
161
+ installPaths: appPaths("Google Chrome Canary.app"),
162
+ executableNames: ["Google Chrome Canary"]
163
+ },
164
+ {
165
+ key: "brave-stable",
166
+ product: "Brave",
167
+ channel: "stable",
168
+ dataDir: join(base, "BraveSoftware", "Brave-Browser"),
169
+ installPaths: appPaths("Brave Browser.app"),
170
+ executableNames: ["Brave Browser"]
171
+ },
172
+ {
173
+ key: "edge-stable",
174
+ product: "Edge",
175
+ channel: "stable",
176
+ dataDir: join(base, "Microsoft Edge"),
177
+ installPaths: appPaths("Microsoft Edge.app"),
178
+ executableNames: ["Microsoft Edge"]
179
+ },
180
+ {
181
+ key: "chromium-stable",
182
+ product: "Chromium",
183
+ channel: "stable",
184
+ dataDir: join(base, "Chromium"),
185
+ installPaths: appPaths("Chromium.app"),
186
+ executableNames: ["Chromium"]
187
+ }
188
+ ];
229
189
  }
230
- const uid = currentUid();
231
- if (uid !== void 0 && stats.uid !== uid) {
232
- throw new Error(`Browser Pilot path is not owned by the current user: ${path}`);
190
+ if (os === "linux") {
191
+ return [
192
+ {
193
+ key: "chrome-stable",
194
+ product: "Chrome",
195
+ channel: "stable",
196
+ dataDir: join(home, ".config", "google-chrome"),
197
+ installPaths: linuxInstallPaths(env, ["google-chrome", "google-chrome-stable"]),
198
+ executableNames: ["google-chrome", "google-chrome-stable"]
199
+ },
200
+ {
201
+ key: "chrome-beta",
202
+ product: "Chrome",
203
+ channel: "beta",
204
+ dataDir: join(home, ".config", "google-chrome-beta"),
205
+ installPaths: linuxInstallPaths(env, ["google-chrome-beta"]),
206
+ executableNames: ["google-chrome-beta"]
207
+ },
208
+ {
209
+ key: "chromium-stable",
210
+ product: "Chromium",
211
+ channel: "stable",
212
+ dataDir: join(home, ".config", "chromium"),
213
+ installPaths: linuxInstallPaths(env, ["chromium", "chromium-browser"]),
214
+ executableNames: ["chromium", "chromium-browser"]
215
+ },
216
+ {
217
+ key: "brave-stable",
218
+ product: "Brave",
219
+ channel: "stable",
220
+ dataDir: join(home, ".config", "BraveSoftware", "Brave-Browser"),
221
+ installPaths: linuxInstallPaths(env, ["brave-browser", "brave-browser-stable"]),
222
+ executableNames: ["brave-browser", "brave-browser-stable"]
223
+ },
224
+ {
225
+ key: "edge-stable",
226
+ product: "Edge",
227
+ channel: "stable",
228
+ dataDir: join(home, ".config", "microsoft-edge"),
229
+ installPaths: linuxInstallPaths(env, ["microsoft-edge", "microsoft-edge-stable"]),
230
+ executableNames: ["microsoft-edge", "microsoft-edge-stable"]
231
+ }
232
+ ];
233
233
  }
234
- if (!expectDirectory && process.platform !== "win32" && (stats.mode & 63) !== 0) {
235
- throw new Error(`Browser Pilot metadata is accessible by another OS user: ${path}`);
234
+ if (os === "win32") {
235
+ const local = env.LOCALAPPDATA ?? join(home, "AppData", "Local");
236
+ const programFiles = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((value) => Boolean(value));
237
+ return [
238
+ {
239
+ key: "chrome-stable",
240
+ product: "Chrome",
241
+ channel: "stable",
242
+ dataDir: join(local, "Google", "Chrome", "User Data"),
243
+ installPaths: [join(local, "Google", "Chrome", "Application", "chrome.exe"), ...programFiles.map((path) => join(path, "Google", "Chrome", "Application", "chrome.exe"))],
244
+ executableNames: ["chrome.exe"]
245
+ },
246
+ {
247
+ key: "brave-stable",
248
+ product: "Brave",
249
+ channel: "stable",
250
+ dataDir: join(local, "BraveSoftware", "Brave-Browser", "User Data"),
251
+ installPaths: [join(local, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), ...programFiles.map((path) => join(path, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"))],
252
+ executableNames: ["brave.exe"]
253
+ },
254
+ {
255
+ key: "edge-stable",
256
+ product: "Edge",
257
+ channel: "stable",
258
+ dataDir: join(local, "Microsoft", "Edge", "User Data"),
259
+ installPaths: [join(local, "Microsoft", "Edge", "Application", "msedge.exe"), ...programFiles.map((path) => join(path, "Microsoft", "Edge", "Application", "msedge.exe"))],
260
+ executableNames: ["msedge.exe"]
261
+ }
262
+ ];
236
263
  }
264
+ return [];
237
265
  }
238
- function ensurePrivateDirectorySync(path) {
239
- if (!existsSync(path)) mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
240
- validateOwnedPath(path, true);
241
- if (process.platform !== "win32") chmodSync(path, PRIVATE_DIRECTORY_MODE);
242
- }
243
- function ensureBrokerDirectoriesSync(paths = BROWSER_PILOT_PATHS) {
244
- ensurePrivateDirectorySync(paths.stateDir);
245
- if (paths.runtimeDir !== paths.stateDir) ensurePrivateDirectorySync(paths.runtimeDir);
246
- }
247
- function parseJsonFile(path) {
248
- validateOwnedPath(path, false);
249
- const stats = statSync(path);
250
- if (stats.size < 2 || stats.size > MAX_METADATA_BYTES) throw new Error("Broker metadata has an invalid size");
251
- return JSON.parse(readFileSync(path, "utf8"));
266
+ function stableBrowserId(definition, os) {
267
+ const digest = createHash("sha256").update(`${os}\0${definition.key}\0${definition.dataDir}`).digest("base64url").slice(0, 20);
268
+ return `browser:${definition.key}:${digest}`;
252
269
  }
253
- function isSafePid(value) {
254
- return Number.isSafeInteger(value) && Number(value) > 0;
270
+ function pathExistsWithoutFollowingSymlink(path) {
271
+ try {
272
+ lstatSync(path);
273
+ return true;
274
+ } catch {
275
+ return false;
276
+ }
255
277
  }
256
- function isSafeTimestamp(value) {
257
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
278
+ function installed(definition) {
279
+ return pathExistsWithoutFollowingSymlink(definition.dataDir) || definition.installPaths.some(pathExistsWithoutFollowingSymlink);
258
280
  }
259
- function validExecutable(value) {
260
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
281
+ function readRunningCommands(os) {
282
+ try {
283
+ if (os === "win32") {
284
+ return execFileSync("tasklist.exe", ["/FO", "CSV", "/NH"], {
285
+ encoding: "utf8",
286
+ timeout: 2e3,
287
+ windowsHide: true
288
+ }).split(/\r?\n/).filter(Boolean);
289
+ }
290
+ return execFileSync("ps", ["-axo", "command="], {
291
+ encoding: "utf8",
292
+ timeout: 2e3
293
+ }).split(/\r?\n/).filter(Boolean);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+ function browserProcessState(definition, commands) {
299
+ for (const lockName of ["SingletonLock", "SingletonSocket", "lockfile"]) {
300
+ if (pathExistsWithoutFollowingSymlink(join(definition.dataDir, lockName))) return "running";
301
+ }
302
+ if (commands === null) return "unknown";
303
+ const names = definition.executableNames.map((name) => name.toLowerCase());
304
+ return commands.some((command) => {
305
+ const normalized = command.toLowerCase();
306
+ return names.some((name) => {
307
+ const macExecutable = `/${name}.app/contents/macos/${name}`;
308
+ if (normalized.includes(macExecutable)) return true;
309
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
310
+ return new RegExp(`(^|[/\\\\"'])${escaped}(?=$|[\\s"',])`, "i").test(normalized);
311
+ });
312
+ }) ? "running" : "not_running";
313
+ }
314
+ function remediation(state, endpointAvailable = false) {
315
+ switch (state) {
316
+ case "not_running":
317
+ return {
318
+ code: "start_browser",
319
+ message: "Start this browser profile, then enable remote debugging from chrome://inspect/#remote-debugging.",
320
+ actionRequired: true
321
+ };
322
+ case "remote_debugging_disabled":
323
+ return {
324
+ code: "enable_remote_debugging",
325
+ message: "Open chrome://inspect/#remote-debugging in this browser and turn on remote debugging.",
326
+ actionRequired: true
327
+ };
328
+ case "authorization_required":
329
+ return {
330
+ code: "authorize_remote_debugging",
331
+ message: "Approve the browser's remote debugging authorization prompt.",
332
+ actionRequired: true
333
+ };
334
+ case "disconnected":
335
+ if (endpointAvailable) {
336
+ return {
337
+ code: "connect_browser",
338
+ message: "Run an explicit browser connect operation to request remote debugging authorization.",
339
+ actionRequired: true
340
+ };
341
+ }
342
+ return {
343
+ code: "restart_remote_debugging",
344
+ message: "The recorded remote debugging endpoint is stale. Restart this browser profile and enable remote debugging again.",
345
+ actionRequired: true
346
+ };
347
+ case "ready":
348
+ return void 0;
349
+ }
350
+ }
351
+ async function discoverBrowserCandidates(options = {}) {
352
+ const os = options.platform ?? platform();
353
+ const definitions = options.profiles ?? supportedBrowserProfiles(options);
354
+ const runningCommands = options.runningCommands === void 0 ? readRunningCommands(os) : options.runningCommands;
355
+ const results = [];
356
+ for (const definition of definitions) {
357
+ if (!installed(definition)) continue;
358
+ const endpoint = readChromeInfo(definition.product, definition.dataDir);
359
+ let processState = browserProcessState(definition, runningCommands);
360
+ let remoteDebuggingState = "disabled";
361
+ let authorizationState = "not_applicable";
362
+ let state;
363
+ if (!endpoint) {
364
+ state = processState === "running" ? "remote_debugging_disabled" : "not_running";
365
+ } else if (processState === "not_running") {
366
+ remoteDebuggingState = "stale";
367
+ authorizationState = "unknown";
368
+ state = "disconnected";
369
+ } else {
370
+ processState = "running";
371
+ remoteDebuggingState = "enabled";
372
+ authorizationState = "unknown";
373
+ state = "disconnected";
374
+ }
375
+ const candidate = {
376
+ id: stableBrowserId(definition, os),
377
+ product: definition.product,
378
+ channel: definition.channel,
379
+ userDataRoot: definition.dataDir,
380
+ processState,
381
+ remoteDebuggingState,
382
+ authorizationState,
383
+ state,
384
+ ...remediation(state, endpoint !== null && remoteDebuggingState === "enabled") ? { remediation: remediation(state, endpoint !== null && remoteDebuggingState === "enabled") } : {}
385
+ };
386
+ results.push({ candidate, dataDir: definition.dataDir, ...endpoint ? { endpoint } : {} });
387
+ }
388
+ return results;
389
+ }
390
+
391
+ // src/client.ts
392
+ import http from "http";
393
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
394
+
395
+ // src/broker-locator.ts
396
+ import { createHash as createHash3, randomUUID } from "crypto";
397
+ import {
398
+ chmodSync,
399
+ closeSync,
400
+ existsSync as existsSync2,
401
+ lstatSync as lstatSync2,
402
+ mkdirSync,
403
+ openSync,
404
+ readFileSync as readFileSync2,
405
+ renameSync,
406
+ realpathSync,
407
+ statSync,
408
+ unlinkSync,
409
+ writeFileSync,
410
+ writeSync
411
+ } from "fs";
412
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
413
+
414
+ // src/paths.ts
415
+ import { createHash as createHash2 } from "crypto";
416
+ import { homedir as homedir2, platform as platform2, tmpdir, userInfo } from "os";
417
+ import { isAbsolute, join as join2, win32 } from "path";
418
+ var MAX_PORTABLE_UNIX_SOCKET_BYTES = 96;
419
+ function identityDigest(value) {
420
+ return createHash2("sha256").update(value).digest("hex").slice(0, 16);
421
+ }
422
+ function resolveBrowserPilotPaths(options = {}) {
423
+ const os = options.platform ?? platform2();
424
+ const home = options.homeDir ?? homedir2();
425
+ const env = options.env ?? process.env;
426
+ if (os === "win32") {
427
+ const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
428
+ const stateDir2 = env.BROWSER_PILOT_HOME ?? win32.join(localAppData, "Browser Pilot");
429
+ if (!win32.isAbsolute(stateDir2)) throw new Error("BROWSER_PILOT_HOME must be absolute");
430
+ const username = options.username ?? (() => {
431
+ try {
432
+ return userInfo().username;
433
+ } catch {
434
+ return home;
435
+ }
436
+ })();
437
+ const identity = identityDigest(
438
+ `${username.toLowerCase()}\0${home.toLowerCase()}\0${stateDir2.toLowerCase()}`
439
+ );
440
+ return {
441
+ stateDir: stateDir2,
442
+ runtimeDir: stateDir2,
443
+ endpoint: `\\\\.\\pipe\\browser-pilot-${identity}`,
444
+ transport: "windows_pipe",
445
+ locatorFile: win32.join(stateDir2, "broker-locator.json"),
446
+ pidFile: win32.join(stateDir2, "daemon.pid"),
447
+ startupLockFile: win32.join(stateDir2, "startup.lock"),
448
+ versionHistoryFile: win32.join(stateDir2, "broker-versions.json"),
449
+ artifactDir: win32.join(stateDir2, "artifacts"),
450
+ downloadDir: win32.join(stateDir2, "downloads")
451
+ };
452
+ }
453
+ const stateDir = env.BROWSER_PILOT_HOME ?? join2(home, ".browser-pilot");
454
+ if (!isAbsolute(stateDir)) throw new Error("BROWSER_PILOT_HOME must be absolute");
455
+ const defaultEndpoint = join2(stateDir, "daemon.sock");
456
+ const uid = options.uid ?? process.getuid?.() ?? 0;
457
+ const useShortRuntimePath = Buffer.byteLength(defaultEndpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES;
458
+ let runtimeDir = useShortRuntimePath ? join2(options.tempDir ?? tmpdir(), `browser-pilot-${uid}-${identityDigest(stateDir)}`) : stateDir;
459
+ let endpoint = join2(runtimeDir, "daemon.sock");
460
+ if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
461
+ runtimeDir = join2("/tmp", `browser-pilot-${uid}-${identityDigest(stateDir)}`);
462
+ endpoint = join2(runtimeDir, "daemon.sock");
463
+ }
464
+ if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
465
+ throw new Error("Browser Pilot cannot resolve a portable Unix socket path");
466
+ }
467
+ if (!isAbsolute(endpoint)) throw new Error("Browser Pilot endpoint must be absolute");
468
+ return {
469
+ stateDir,
470
+ runtimeDir,
471
+ endpoint,
472
+ transport: "unix_socket",
473
+ locatorFile: join2(stateDir, "broker-locator.json"),
474
+ pidFile: join2(stateDir, "daemon.pid"),
475
+ startupLockFile: join2(stateDir, "startup.lock"),
476
+ versionHistoryFile: join2(stateDir, "broker-versions.json"),
477
+ artifactDir: join2(stateDir, "artifacts"),
478
+ downloadDir: join2(stateDir, "downloads")
479
+ };
480
+ }
481
+ var BROWSER_PILOT_PATHS = resolveBrowserPilotPaths();
482
+ var STATE_DIR = BROWSER_PILOT_PATHS.stateDir;
483
+ var RUNTIME_DIR = BROWSER_PILOT_PATHS.runtimeDir;
484
+ var SOCKET_PATH = BROWSER_PILOT_PATHS.endpoint;
485
+ var BROKER_TRANSPORT = BROWSER_PILOT_PATHS.transport;
486
+ var LOCATOR_FILE = BROWSER_PILOT_PATHS.locatorFile;
487
+ var PID_FILE = BROWSER_PILOT_PATHS.pidFile;
488
+ var STARTUP_LOCK_FILE = BROWSER_PILOT_PATHS.startupLockFile;
489
+ var ARTIFACT_DIR = BROWSER_PILOT_PATHS.artifactDir;
490
+ var DOWNLOAD_DIR = BROWSER_PILOT_PATHS.downloadDir;
491
+
492
+ // src/broker-locator.ts
493
+ var PRIVATE_DIRECTORY_MODE = 448;
494
+ var PRIVATE_FILE_MODE = 384;
495
+ var MAX_METADATA_BYTES = 16 * 1024;
496
+ function currentUid() {
497
+ return process.getuid?.();
498
+ }
499
+ function validateOwnedPath(path, expectDirectory) {
500
+ const stats = lstatSync2(path);
501
+ if (stats.isSymbolicLink()) throw new Error(`Browser Pilot path must not be a symbolic link: ${path}`);
502
+ if (expectDirectory ? !stats.isDirectory() : !stats.isFile()) {
503
+ throw new Error(`Browser Pilot path has an unexpected type: ${path}`);
504
+ }
505
+ const uid = currentUid();
506
+ if (uid !== void 0 && stats.uid !== uid) {
507
+ throw new Error(`Browser Pilot path is not owned by the current user: ${path}`);
508
+ }
509
+ if (!expectDirectory && process.platform !== "win32" && (stats.mode & 63) !== 0) {
510
+ throw new Error(`Browser Pilot metadata is accessible by another OS user: ${path}`);
511
+ }
512
+ }
513
+ function ensurePrivateDirectorySync(path) {
514
+ if (!existsSync2(path)) mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
515
+ validateOwnedPath(path, true);
516
+ if (process.platform !== "win32") chmodSync(path, PRIVATE_DIRECTORY_MODE);
517
+ }
518
+ function ensureBrokerDirectoriesSync(paths = BROWSER_PILOT_PATHS) {
519
+ ensurePrivateDirectorySync(paths.stateDir);
520
+ if (paths.runtimeDir !== paths.stateDir) ensurePrivateDirectorySync(paths.runtimeDir);
521
+ }
522
+ function parseJsonFile(path) {
523
+ validateOwnedPath(path, false);
524
+ const stats = statSync(path);
525
+ if (stats.size < 2 || stats.size > MAX_METADATA_BYTES) throw new Error("Broker metadata has an invalid size");
526
+ return JSON.parse(readFileSync2(path, "utf8"));
527
+ }
528
+ function isSafePid(value) {
529
+ return Number.isSafeInteger(value) && Number(value) > 0;
530
+ }
531
+ function isSafeTimestamp(value) {
532
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
533
+ }
534
+ function validExecutable(value) {
535
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
261
536
  const record = value;
262
537
  return typeof record.version === "string" && record.version.length > 0 && record.version.length <= 128 && typeof record.path === "string" && record.path.length > 0 && record.path.length <= 4096 && isAbsolute2(record.path) && typeof record.identity === "string" && /^executable:[a-f0-9]{64}$/.test(record.identity);
263
538
  }
@@ -283,14 +558,14 @@ function validLocator(value, paths) {
283
558
  function createExecutableMetadataSync(version, path) {
284
559
  if (!version || version.length > 128) throw new Error("Invalid Browser Pilot executable version");
285
560
  const resolvedPath = realpathSync(path);
286
- const digest = createHash2("sha256").update(`${resolvedPath}\0${version}\0`);
561
+ const digest = createHash3("sha256").update(`${resolvedPath}\0${version}\0`);
287
562
  for (const artifactPath of [
288
563
  resolvedPath,
289
- join2(dirname(resolvedPath), "daemon.js"),
290
- join2(dirname(resolvedPath), "managed-target-janitor.js")
564
+ join3(dirname(resolvedPath), "daemon.js"),
565
+ join3(dirname(resolvedPath), "managed-target-janitor.js")
291
566
  ]) {
292
- if (!existsSync(artifactPath)) continue;
293
- digest.update(basename(artifactPath)).update("\0").update(readFileSync(artifactPath)).update("\0");
567
+ if (!existsSync2(artifactPath)) continue;
568
+ digest.update(basename(artifactPath)).update("\0").update(readFileSync2(artifactPath)).update("\0");
294
569
  }
295
570
  return {
296
571
  version,
@@ -299,7 +574,7 @@ function createExecutableMetadataSync(version, path) {
299
574
  };
300
575
  }
301
576
  function readBrokerLocatorSync(paths = BROWSER_PILOT_PATHS) {
302
- if (!existsSync(paths.locatorFile)) return void 0;
577
+ if (!existsSync2(paths.locatorFile)) return void 0;
303
578
  try {
304
579
  const value = parseJsonFile(paths.locatorFile);
305
580
  return validLocator(value, paths) ? value : void 0;
@@ -310,7 +585,7 @@ function readBrokerLocatorSync(paths = BROWSER_PILOT_PATHS) {
310
585
  function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
311
586
  const locator = readBrokerLocatorSync(paths);
312
587
  if (locator) return locator.pid;
313
- if (!existsSync(paths.pidFile)) return void 0;
588
+ if (!existsSync2(paths.pidFile)) return void 0;
314
589
  try {
315
590
  const value = parseJsonFile(paths.pidFile);
316
591
  if (isSafePid(value)) return value;
@@ -321,7 +596,7 @@ function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
321
596
  } catch {
322
597
  try {
323
598
  validateOwnedPath(paths.pidFile, false);
324
- const pid = Number.parseInt(readFileSync(paths.pidFile, "utf8").trim(), 10);
599
+ const pid = Number.parseInt(readFileSync2(paths.pidFile, "utf8").trim(), 10);
325
600
  return isSafePid(pid) ? pid : void 0;
326
601
  } catch {
327
602
  }
@@ -329,7 +604,7 @@ function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
329
604
  return void 0;
330
605
  }
331
606
  function readBrokerStartingSync(paths = BROWSER_PILOT_PATHS) {
332
- if (!existsSync(paths.pidFile) || existsSync(paths.locatorFile)) return void 0;
607
+ if (!existsSync2(paths.pidFile) || existsSync2(paths.locatorFile)) return void 0;
333
608
  try {
334
609
  const value = parseJsonFile(paths.pidFile);
335
610
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -350,7 +625,7 @@ function processIsAlive(pid) {
350
625
  }
351
626
  }
352
627
  function readStartupLock(path) {
353
- if (!existsSync(path)) return void 0;
628
+ if (!existsSync2(path)) return void 0;
354
629
  try {
355
630
  const value = parseJsonFile(path);
356
631
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -426,7 +701,7 @@ async function acquireBrokerStartupLock(options = {}) {
426
701
  stale = !alive(owner.pid);
427
702
  } else {
428
703
  try {
429
- stale = now() - lstatSync(paths.startupLockFile).mtimeMs > staleAfterMs;
704
+ stale = now() - lstatSync2(paths.startupLockFile).mtimeMs > staleAfterMs;
430
705
  } catch {
431
706
  stale = true;
432
707
  }
@@ -453,14 +728,60 @@ function removeStaleBrokerFilesSync(paths = BROWSER_PILOT_PATHS) {
453
728
  }
454
729
  }
455
730
 
731
+ // src/services/broker-request-timeout.ts
732
+ var MIN_BROKER_REQUEST_TIMEOUT_MS = 6e4;
733
+ var BROKER_REQUEST_TIMEOUT_MARGIN_MS = 5e3;
734
+ function isRecord(value) {
735
+ return typeof value === "object" && value !== null && !Array.isArray(value);
736
+ }
737
+ function brokerRequestTimeoutMs(method, params) {
738
+ if (method !== "tools/call" || !isRecord(params)) return MIN_BROKER_REQUEST_TIMEOUT_MS;
739
+ const deadlineMs = params.deadlineMs;
740
+ if (!Number.isSafeInteger(deadlineMs) || Number(deadlineMs) < 1) {
741
+ return MIN_BROKER_REQUEST_TIMEOUT_MS;
742
+ }
743
+ return Math.max(
744
+ MIN_BROKER_REQUEST_TIMEOUT_MS,
745
+ Number(deadlineMs) + BROKER_REQUEST_TIMEOUT_MARGIN_MS
746
+ );
747
+ }
748
+ function brokerRequestTimeoutError(method, params) {
749
+ if (method !== "tools/call" || !isRecord(params)) {
750
+ return new BrowserPilotError(
751
+ "browser_disconnected",
752
+ "Timed out waiting for the Browser Pilot Broker",
753
+ { retryable: true }
754
+ );
755
+ }
756
+ const commandId = typeof params.commandId === "string" ? params.commandId : void 0;
757
+ const toolName = typeof params.name === "string" ? params.name : void 0;
758
+ return new BrowserPilotError(
759
+ "unknown_outcome",
760
+ "Timed out waiting for the Browser Pilot command result",
761
+ {
762
+ retryable: true,
763
+ context: {
764
+ ...commandId ? { commandId } : {},
765
+ ...toolName ? { method: toolName } : {}
766
+ },
767
+ remediation: {
768
+ code: "inspect_command_before_retry",
769
+ message: commandId ? `Run bp command ${commandId} and inspect browser state before retrying.` : "Inspect recent commands and browser state before retrying.",
770
+ actionRequired: true
771
+ }
772
+ }
773
+ );
774
+ }
775
+
456
776
  // src/client.ts
457
777
  var BROKER_SHUTDOWN_TIMEOUT_MS = 15e3;
778
+ var BROKER_RPC_VERSION = 2;
458
779
  function isDaemonRunning() {
459
780
  const locator = readBrokerLocatorSync();
460
781
  if (locator) return processIsAlive(locator.pid);
461
- if (!existsSync2(BROWSER_PILOT_PATHS.pidFile)) return false;
782
+ if (!existsSync3(BROWSER_PILOT_PATHS.pidFile)) return false;
462
783
  try {
463
- const raw = readFileSync2(BROWSER_PILOT_PATHS.pidFile, "utf-8").trim();
784
+ const raw = readFileSync3(BROWSER_PILOT_PATHS.pidFile, "utf-8").trim();
464
785
  const parsed = raw.startsWith("{") ? JSON.parse(raw) : Number.parseInt(raw, 10);
465
786
  const pid = typeof parsed === "number" ? parsed : parsed && typeof parsed === "object" && !Array.isArray(parsed) ? Number(parsed.pid) : Number.NaN;
466
787
  return processIsAlive(pid);
@@ -473,7 +794,7 @@ var DaemonClient = class {
473
794
  constructor() {
474
795
  this.endpoint = readBrokerLocatorSync()?.endpoint ?? BROWSER_PILOT_PATHS.endpoint;
475
796
  }
476
- request(path, body, signal) {
797
+ request(path, body, signal, timeoutMs = MIN_BROKER_REQUEST_TIMEOUT_MS) {
477
798
  return new Promise((resolve, reject) => {
478
799
  const req = http.request(
479
800
  {
@@ -481,7 +802,7 @@ var DaemonClient = class {
481
802
  path,
482
803
  method: body !== void 0 ? "POST" : "GET",
483
804
  headers: body !== void 0 ? { "Content-Type": "application/json" } : {},
484
- timeout: 6e4,
805
+ timeout: timeoutMs,
485
806
  signal
486
807
  },
487
808
  (res) => {
@@ -507,7 +828,13 @@ var DaemonClient = class {
507
828
  });
508
829
  req.on("timeout", () => {
509
830
  req.destroy();
510
- reject(new Error("Daemon request timeout"));
831
+ reject(
832
+ path === "/broker/rpc" && body && typeof body.method === "string" ? brokerRequestTimeoutError(body.method, body.params) : new BrowserPilotError(
833
+ "browser_disconnected",
834
+ "Timed out waiting for the Browser Pilot Broker",
835
+ { retryable: true }
836
+ )
837
+ );
511
838
  });
512
839
  if (body !== void 0) req.write(JSON.stringify(body));
513
840
  req.end();
@@ -553,22 +880,12 @@ var DaemonClient = class {
553
880
  }
554
881
  );
555
882
  }
556
- async brokerCall(bridgeSessionId, method, params) {
883
+ async brokerCall(clientSessionId, method, params) {
557
884
  return this.request("/broker/rpc", {
558
- bridgeSessionId,
885
+ clientSessionId,
559
886
  method,
560
887
  ...params !== void 0 ? { params } : {}
561
- });
562
- }
563
- async brokerDisconnect(bridgeSessionId) {
564
- await this.request("/broker/disconnect", { bridgeSessionId });
565
- }
566
- async brokerNextNotification(bridgeSessionId, waitMs, signal) {
567
- const result = await this.request("/broker/events/next", {
568
- bridgeSessionId,
569
- waitMs
570
- }, signal);
571
- return result.notification;
888
+ }, void 0, brokerRequestTimeoutMs(method, params));
572
889
  }
573
890
  async discoveredTargets() {
574
891
  const res = await this.request("/discovered");
@@ -604,324 +921,36 @@ var DaemonClient = class {
604
921
  if (opts?.status) p.set("status", opts.status);
605
922
  if (opts?.type) p.set("type", opts.type);
606
923
  if (opts?.after) p.set("after", String(opts.after));
607
- const qs = p.toString();
608
- return this.request(`/net/requests${qs ? "?" + qs : ""}`);
609
- }
610
- async netRequestDetail(id) {
611
- return this.request(`/net/request/${id}`);
612
- }
613
- async netBody(id) {
614
- return this.request(`/net/body/${id}`);
615
- }
616
- async netClear() {
617
- await this.request("/net/clear", {});
618
- }
619
- async netAddRule(rule) {
620
- return this.request("/net/rules", rule);
621
- }
622
- async netRules() {
623
- return this.request("/net/rules");
624
- }
625
- async netRemoveRule(id) {
626
- await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
627
- }
628
- close() {
629
- }
630
- };
631
-
632
- // src/chrome.ts
633
- import { createHash as createHash3 } from "crypto";
634
- import { execFileSync } from "child_process";
635
- import { existsSync as existsSync3, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "fs";
636
- import { connect as connectTcp } from "net";
637
- import { delimiter, join as join3 } from "path";
638
- import { homedir as homedir2, platform as platform2 } from "os";
639
- import WebSocket from "ws";
640
- function readChromeInfo(browser, dataDir) {
641
- const portFile = join3(dataDir, "DevToolsActivePort");
642
- if (!existsSync3(portFile)) return null;
643
- try {
644
- const lines = readFileSync3(portFile, "utf-8").trim().split(/\r?\n/);
645
- if (lines.length < 2) return null;
646
- const port = Number.parseInt(lines[0], 10);
647
- const wsPath = lines[1];
648
- if (!Number.isSafeInteger(port) || port < 1 || port > 65535 || !wsPath.startsWith("/")) return null;
649
- return {
650
- port,
651
- wsPath,
652
- wsUrl: `ws://127.0.0.1:${port}${wsPath}`,
653
- browser,
654
- dataDir
655
- };
656
- } catch {
657
- return null;
658
- }
659
- }
660
- function executablePaths(env) {
661
- return (env.PATH ?? "").split(delimiter).filter(Boolean);
662
- }
663
- function linuxInstallPaths(env, names) {
664
- return executablePaths(env).flatMap((directory) => names.map((name) => join3(directory, name)));
665
- }
666
- function supportedBrowserProfiles(options = {}) {
667
- const home = options.homeDir ?? homedir2();
668
- const os = options.platform ?? platform2();
669
- const env = options.env ?? process.env;
670
- if (os === "darwin") {
671
- const base = join3(home, "Library", "Application Support");
672
- const applications = ["/Applications", join3(home, "Applications")];
673
- const appPaths = (name) => applications.map((directory) => join3(directory, name));
674
- return [
675
- {
676
- key: "chrome-stable",
677
- product: "Chrome",
678
- channel: "stable",
679
- dataDir: join3(base, "Google", "Chrome"),
680
- installPaths: appPaths("Google Chrome.app"),
681
- executableNames: ["Google Chrome"]
682
- },
683
- {
684
- key: "chrome-beta",
685
- product: "Chrome",
686
- channel: "beta",
687
- dataDir: join3(base, "Google", "Chrome Beta"),
688
- installPaths: appPaths("Google Chrome Beta.app"),
689
- executableNames: ["Google Chrome Beta"]
690
- },
691
- {
692
- key: "chrome-canary",
693
- product: "Chrome",
694
- channel: "canary",
695
- dataDir: join3(base, "Google", "Chrome Canary"),
696
- installPaths: appPaths("Google Chrome Canary.app"),
697
- executableNames: ["Google Chrome Canary"]
698
- },
699
- {
700
- key: "brave-stable",
701
- product: "Brave",
702
- channel: "stable",
703
- dataDir: join3(base, "BraveSoftware", "Brave-Browser"),
704
- installPaths: appPaths("Brave Browser.app"),
705
- executableNames: ["Brave Browser"]
706
- },
707
- {
708
- key: "edge-stable",
709
- product: "Edge",
710
- channel: "stable",
711
- dataDir: join3(base, "Microsoft Edge"),
712
- installPaths: appPaths("Microsoft Edge.app"),
713
- executableNames: ["Microsoft Edge"]
714
- },
715
- {
716
- key: "chromium-stable",
717
- product: "Chromium",
718
- channel: "stable",
719
- dataDir: join3(base, "Chromium"),
720
- installPaths: appPaths("Chromium.app"),
721
- executableNames: ["Chromium"]
722
- }
723
- ];
724
- }
725
- if (os === "linux") {
726
- return [
727
- {
728
- key: "chrome-stable",
729
- product: "Chrome",
730
- channel: "stable",
731
- dataDir: join3(home, ".config", "google-chrome"),
732
- installPaths: linuxInstallPaths(env, ["google-chrome", "google-chrome-stable"]),
733
- executableNames: ["google-chrome", "google-chrome-stable"]
734
- },
735
- {
736
- key: "chrome-beta",
737
- product: "Chrome",
738
- channel: "beta",
739
- dataDir: join3(home, ".config", "google-chrome-beta"),
740
- installPaths: linuxInstallPaths(env, ["google-chrome-beta"]),
741
- executableNames: ["google-chrome-beta"]
742
- },
743
- {
744
- key: "chromium-stable",
745
- product: "Chromium",
746
- channel: "stable",
747
- dataDir: join3(home, ".config", "chromium"),
748
- installPaths: linuxInstallPaths(env, ["chromium", "chromium-browser"]),
749
- executableNames: ["chromium", "chromium-browser"]
750
- },
751
- {
752
- key: "brave-stable",
753
- product: "Brave",
754
- channel: "stable",
755
- dataDir: join3(home, ".config", "BraveSoftware", "Brave-Browser"),
756
- installPaths: linuxInstallPaths(env, ["brave-browser", "brave-browser-stable"]),
757
- executableNames: ["brave-browser", "brave-browser-stable"]
758
- },
759
- {
760
- key: "edge-stable",
761
- product: "Edge",
762
- channel: "stable",
763
- dataDir: join3(home, ".config", "microsoft-edge"),
764
- installPaths: linuxInstallPaths(env, ["microsoft-edge", "microsoft-edge-stable"]),
765
- executableNames: ["microsoft-edge", "microsoft-edge-stable"]
766
- }
767
- ];
768
- }
769
- if (os === "win32") {
770
- const local = env.LOCALAPPDATA ?? join3(home, "AppData", "Local");
771
- const programFiles = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((value) => Boolean(value));
772
- return [
773
- {
774
- key: "chrome-stable",
775
- product: "Chrome",
776
- channel: "stable",
777
- dataDir: join3(local, "Google", "Chrome", "User Data"),
778
- installPaths: [join3(local, "Google", "Chrome", "Application", "chrome.exe"), ...programFiles.map((path) => join3(path, "Google", "Chrome", "Application", "chrome.exe"))],
779
- executableNames: ["chrome.exe"]
780
- },
781
- {
782
- key: "brave-stable",
783
- product: "Brave",
784
- channel: "stable",
785
- dataDir: join3(local, "BraveSoftware", "Brave-Browser", "User Data"),
786
- installPaths: [join3(local, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), ...programFiles.map((path) => join3(path, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"))],
787
- executableNames: ["brave.exe"]
788
- },
789
- {
790
- key: "edge-stable",
791
- product: "Edge",
792
- channel: "stable",
793
- dataDir: join3(local, "Microsoft", "Edge", "User Data"),
794
- installPaths: [join3(local, "Microsoft", "Edge", "Application", "msedge.exe"), ...programFiles.map((path) => join3(path, "Microsoft", "Edge", "Application", "msedge.exe"))],
795
- executableNames: ["msedge.exe"]
796
- }
797
- ];
798
- }
799
- return [];
800
- }
801
- function stableBrowserId(definition, os) {
802
- const digest = createHash3("sha256").update(`${os}\0${definition.key}\0${definition.dataDir}`).digest("base64url").slice(0, 20);
803
- return `browser:${definition.key}:${digest}`;
804
- }
805
- function pathExistsWithoutFollowingSymlink(path) {
806
- try {
807
- lstatSync2(path);
808
- return true;
809
- } catch {
810
- return false;
811
- }
812
- }
813
- function installed(definition) {
814
- return pathExistsWithoutFollowingSymlink(definition.dataDir) || definition.installPaths.some(pathExistsWithoutFollowingSymlink);
815
- }
816
- function readRunningCommands(os) {
817
- try {
818
- if (os === "win32") {
819
- return execFileSync("tasklist.exe", ["/FO", "CSV", "/NH"], {
820
- encoding: "utf8",
821
- timeout: 2e3,
822
- windowsHide: true
823
- }).split(/\r?\n/).filter(Boolean);
824
- }
825
- return execFileSync("ps", ["-axo", "command="], {
826
- encoding: "utf8",
827
- timeout: 2e3
828
- }).split(/\r?\n/).filter(Boolean);
829
- } catch {
830
- return null;
831
- }
832
- }
833
- function browserProcessState(definition, commands) {
834
- for (const lockName of ["SingletonLock", "SingletonSocket", "lockfile"]) {
835
- if (pathExistsWithoutFollowingSymlink(join3(definition.dataDir, lockName))) return "running";
836
- }
837
- if (commands === null) return "unknown";
838
- const names = definition.executableNames.map((name) => name.toLowerCase());
839
- return commands.some((command) => {
840
- const normalized = command.toLowerCase();
841
- return names.some((name) => {
842
- const macExecutable = `/${name}.app/contents/macos/${name}`;
843
- if (normalized.includes(macExecutable)) return true;
844
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
845
- return new RegExp(`(^|[/\\\\"'])${escaped}(?=$|[\\s"',])`, "i").test(normalized);
846
- });
847
- }) ? "running" : "not_running";
848
- }
849
- function remediation(state, endpointAvailable = false) {
850
- switch (state) {
851
- case "not_running":
852
- return {
853
- code: "start_browser",
854
- message: "Start this browser profile, then enable remote debugging from chrome://inspect/#remote-debugging.",
855
- actionRequired: true
856
- };
857
- case "remote_debugging_disabled":
858
- return {
859
- code: "enable_remote_debugging",
860
- message: "Open chrome://inspect/#remote-debugging in this browser and turn on remote debugging.",
861
- actionRequired: true
862
- };
863
- case "authorization_required":
864
- return {
865
- code: "authorize_remote_debugging",
866
- message: "Approve the browser's remote debugging authorization prompt.",
867
- actionRequired: true
868
- };
869
- case "disconnected":
870
- if (endpointAvailable) {
871
- return {
872
- code: "connect_browser",
873
- message: "Run an explicit browser connect operation to request remote debugging authorization.",
874
- actionRequired: true
875
- };
876
- }
877
- return {
878
- code: "restart_remote_debugging",
879
- message: "The recorded remote debugging endpoint is stale. Restart this browser profile and enable remote debugging again.",
880
- actionRequired: true
881
- };
882
- case "ready":
883
- return void 0;
884
- }
885
- }
886
- async function discoverBrowserCandidates(options = {}) {
887
- const os = options.platform ?? platform2();
888
- const definitions = options.profiles ?? supportedBrowserProfiles(options);
889
- const runningCommands = options.runningCommands === void 0 ? readRunningCommands(os) : options.runningCommands;
890
- const results = [];
891
- for (const definition of definitions) {
892
- if (!installed(definition)) continue;
893
- const endpoint = readChromeInfo(definition.product, definition.dataDir);
894
- let processState = browserProcessState(definition, runningCommands);
895
- let remoteDebuggingState = "disabled";
896
- let authorizationState = "not_applicable";
897
- let state;
898
- if (!endpoint) {
899
- state = processState === "running" ? "remote_debugging_disabled" : "not_running";
900
- } else if (processState === "not_running") {
901
- remoteDebuggingState = "stale";
902
- authorizationState = "unknown";
903
- state = "disconnected";
904
- } else {
905
- processState = "running";
906
- remoteDebuggingState = "enabled";
907
- authorizationState = "unknown";
908
- state = "disconnected";
909
- }
910
- const candidate = {
911
- id: stableBrowserId(definition, os),
912
- product: definition.product,
913
- channel: definition.channel,
914
- userDataRoot: definition.dataDir,
915
- processState,
916
- remoteDebuggingState,
917
- authorizationState,
918
- state,
919
- ...remediation(state, endpoint !== null && remoteDebuggingState === "enabled") ? { remediation: remediation(state, endpoint !== null && remoteDebuggingState === "enabled") } : {}
920
- };
921
- results.push({ candidate, dataDir: definition.dataDir, ...endpoint ? { endpoint } : {} });
924
+ const qs = p.toString();
925
+ return this.request(`/net/requests${qs ? "?" + qs : ""}`);
922
926
  }
923
- return results;
924
- }
927
+ async netRequestDetail(id) {
928
+ return this.request(`/net/request/${id}`);
929
+ }
930
+ async netBody(id) {
931
+ return this.request(`/net/body/${id}`);
932
+ }
933
+ async netClear() {
934
+ await this.request("/net/clear", {});
935
+ }
936
+ async netAddRule(rule) {
937
+ return this.request("/net/rules", rule);
938
+ }
939
+ async netRules() {
940
+ return this.request("/net/rules");
941
+ }
942
+ async netRemoveRule(id) {
943
+ await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
944
+ }
945
+ close() {
946
+ }
947
+ };
948
+
949
+ // src/compatibility-broker-client.ts
950
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
951
+
952
+ // src/session.ts
953
+ import { spawn } from "child_process";
925
954
 
926
955
  // src/runtime-layout.ts
927
956
  import { fileURLToPath } from "url";
@@ -1005,1941 +1034,119 @@ function assertBrowserSelection(browserFilter, info) {
1005
1034
  }
1006
1035
  async function waitForStartingDaemon(pid, browserFilter, timeoutMs = 6e4) {
1007
1036
  const deadline = Date.now() + timeoutMs;
1008
- while (Date.now() < deadline && processIsAlive(pid)) {
1009
- const client = new DaemonClient();
1010
- const info = await client.healthInfo();
1011
- if (info.ok) {
1012
- assertBrowserSelection(browserFilter, info);
1013
- return client;
1014
- }
1015
- await new Promise((resolve) => setTimeout(resolve, 200));
1016
- }
1017
- return void 0;
1018
- }
1019
- async function connectDaemon(browserFilter) {
1020
- const existing = new DaemonClient();
1021
- const existingInfo = await existing.healthInfo();
1022
- if (existingInfo.ok) {
1023
- assertBrowserSelection(browserFilter, existingInfo);
1024
- return existing;
1025
- }
1026
- const startupLock = await acquireBrokerStartupLock();
1027
- try {
1028
- const winner = new DaemonClient();
1029
- const winnerInfo = await winner.healthInfo();
1030
- if (winnerInfo.ok) {
1031
- assertBrowserSelection(browserFilter, winnerInfo);
1032
- return winner;
1033
- }
1034
- const locator = readBrokerLocatorSync();
1035
- const starting = readBrokerStartingSync();
1036
- const brokerPid = locator?.pid ?? starting?.pid ?? readBrokerPidSync();
1037
- if (brokerPid && processIsAlive(brokerPid)) {
1038
- if (starting?.pid === brokerPid) {
1039
- const started = await waitForStartingDaemon(brokerPid, browserFilter);
1040
- if (started) return started;
1041
- throw new BrowserPilotError("browser_disconnected", "Browser Pilot Broker did not finish starting", {
1042
- retryable: true,
1043
- remediation: {
1044
- code: "inspect_broker_startup",
1045
- message: "Inspect the Broker startup state; browser authorization is never requested during Broker startup.",
1046
- actionRequired: false
1047
- }
1048
- });
1049
- }
1050
- throw new BrowserPilotError("browser_disconnected", "The Browser Pilot Broker process is alive but its endpoint is unresponsive", {
1051
- retryable: true,
1052
- remediation: {
1053
- code: "restart_unresponsive_broker",
1054
- message: "Stop the unresponsive Browser Pilot Broker explicitly, then retry. Live processes are never replaced automatically.",
1055
- actionRequired: true
1056
- }
1057
- });
1058
- }
1059
- removeStaleBrokerFilesSync();
1060
- const candidates = await discoverBrowserCandidates();
1061
- const filter = browserFilter?.toLowerCase();
1062
- const matches = candidates.filter(({ candidate }) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
1063
- if (browserFilter && matches.length === 0) {
1064
- throw new BrowserPilotError(
1065
- "browser_not_found",
1066
- "No installed supported browser matches the requested selection.",
1067
- {
1068
- remediation: {
1069
- code: "select_supported_browser",
1070
- message: "Run browser discovery and select one of the returned browser IDs or product names.",
1071
- actionRequired: true
1072
- }
1073
- }
1074
- );
1075
- }
1076
- const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
1077
- return await startDaemon(selected);
1078
- } finally {
1079
- startupLock.release();
1080
- }
1081
- }
1082
-
1083
- // src/bridge/daemon-bridge-backend.ts
1084
- var DaemonBridgeBackend = class {
1085
- constructor(browserFilter) {
1086
- this.browserFilter = browserFilter;
1087
- }
1088
- client;
1089
- async call(bridgeSessionId, method, params) {
1090
- const client = await this.getClient();
1091
- return client.brokerCall(bridgeSessionId, method, params);
1092
- }
1093
- async disconnect(bridgeSessionId) {
1094
- if (!this.client) return;
1095
- await this.client.brokerDisconnect(bridgeSessionId);
1096
- }
1097
- async *notifications(bridgeSessionId, signal) {
1098
- const client = await this.getClient();
1099
- while (!signal.aborted) {
1100
- try {
1101
- const notification = await client.brokerNextNotification(bridgeSessionId, 25e3, signal);
1102
- if (notification) yield notification;
1103
- } catch (error) {
1104
- if (signal.aborted) return;
1105
- throw error;
1106
- }
1107
- }
1108
- }
1109
- async getClient() {
1110
- if (!this.client) this.client = await connectDaemon(this.browserFilter);
1111
- const health = await this.client.healthInfo();
1112
- if (health.brokerProtocol !== 1) {
1113
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from an older executable", {
1114
- remediation: {
1115
- code: "use_compatible_executable_or_isolate",
1116
- message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
1117
- actionRequired: true
1118
- }
1119
- });
1120
- }
1121
- return this.client;
1122
- }
1123
- };
1124
-
1125
- // src/bridge/stdio-bridge.ts
1126
- import { once } from "events";
1127
- import { randomUUID as randomUUID5 } from "crypto";
1128
-
1129
- // src/protocol/model.ts
1130
- var CAPABILITIES = [
1131
- "browser.discovery",
1132
- "browser.control",
1133
- "workspace.manage",
1134
- "observation.read",
1135
- "action.input",
1136
- "artifact.read",
1137
- "event.read",
1138
- "network.observe",
1139
- "network.modify",
1140
- "auth.manage",
1141
- "cookies.read",
1142
- "developer.eval"
1143
- ];
1144
- var PROFILE_IDENTITY_ERROR_CODES = [
1145
- "profile_path_unavailable",
1146
- "profile_path_unverified",
1147
- "local_state_unavailable",
1148
- "profile_metadata_missing"
1149
- ];
1150
- var OBSERVATION_TRUNCATION_REASONS = [
1151
- "element_limit",
1152
- "text_limit",
1153
- "depth_limit",
1154
- "byte_limit"
1155
- ];
1156
- var OBSERVATION_V1_LIMITS = {
1157
- defaultElements: 50,
1158
- maxElements: 1e4,
1159
- maxTitleCharacters: 4096,
1160
- maxUrlCharacters: 16384,
1161
- maxElementNameCharacters: 4096,
1162
- maxElementValueCharacters: 65536,
1163
- maxTextCharacters: 1e6,
1164
- maxTreeDepth: 128,
1165
- maxSerializedBytes: 2 * 1024 * 1024,
1166
- ttlMs: 5 * 6e4,
1167
- maxStoredObservations: 2048
1168
- };
1169
- var SENSITIVITIES = [
1170
- "public",
1171
- "browser_data",
1172
- "credential",
1173
- "user_file"
1174
- ];
1175
-
1176
- // src/protocol/validation.ts
1177
- var MAX_ID_LENGTH = 256;
1178
- var MIN_NEGOTIATED_TRANSPORT_BYTES = 64 * 1024;
1179
- var MAX_DECLARED_TRANSPORT_BYTES = 1024 * 1024 * 1024;
1180
- function isRecord(value) {
1181
- return typeof value === "object" && value !== null && !Array.isArray(value);
1182
- }
1183
- function assertOnlyKeys(record, allowed, field = "params") {
1184
- const allowedSet = new Set(allowed);
1185
- const unknown = Object.keys(record).filter((key) => !allowedSet.has(key));
1186
- if (unknown.length > 0) {
1187
- throw invalidArgument(`Unknown ${field} field: ${unknown[0]}`, `${field}.${unknown[0]}`, field === "message" ? -32600 : -32602);
1188
- }
1189
- }
1190
- function isJsonValue(value) {
1191
- if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
1192
- return typeof value !== "number" || Number.isFinite(value);
1193
- }
1194
- if (Array.isArray(value)) return value.every(isJsonValue);
1195
- if (isRecord(value)) return Object.values(value).every(isJsonValue);
1196
- return false;
1197
- }
1198
- function requireString(record, key, options = {}) {
1199
- const value = record[key];
1200
- const maxLength = options.maxLength ?? MAX_ID_LENGTH;
1201
- if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
1202
- throw invalidArgument(`${key} must be a non-empty string no longer than ${maxLength} characters`, key);
1203
- }
1204
- if (options.pattern && !options.pattern.test(value)) {
1205
- throw invalidArgument(`${key} has an invalid format`, key);
1206
- }
1207
- return value;
1208
- }
1209
- function parseId(value, allowNull) {
1210
- if (allowNull && value === null) return null;
1211
- if (typeof value === "string" && value.length > 0 && value.length <= MAX_ID_LENGTH) return value;
1212
- if (typeof value === "number" && Number.isSafeInteger(value)) return value;
1213
- throw invalidArgument("JSON-RPC id must be a non-empty string or safe integer", "id", -32600);
1214
- }
1215
- function parseParams(value) {
1216
- if (value === void 0) return void 0;
1217
- if (!Array.isArray(value) && !isRecord(value) || !isJsonValue(value)) {
1218
- throw invalidArgument("JSON-RPC params must be a JSON object or array", "params", -32600);
1219
- }
1220
- return value;
1221
- }
1222
- function parseErrorObject(value) {
1223
- if (!isRecord(value)) throw invalidArgument("JSON-RPC error must be an object", "error", -32600);
1224
- if (!Number.isSafeInteger(value.code)) throw invalidArgument("JSON-RPC error.code must be an integer", "error.code", -32600);
1225
- const message = requireString(value, "message", { maxLength: 4096 });
1226
- if (value.data !== void 0 && !isJsonValue(value.data)) {
1227
- throw invalidArgument("JSON-RPC error.data must be valid JSON", "error.data", -32600);
1228
- }
1229
- return { code: value.code, message, ...value.data !== void 0 ? { data: value.data } : {} };
1230
- }
1231
- function parseJsonRpcMessage(line, maxMessageBytes = 1024 * 1024) {
1232
- if (Buffer.byteLength(line, "utf8") > maxMessageBytes) {
1233
- throw new BrowserPilotError("result_too_large", `Protocol message exceeds ${maxMessageBytes} bytes`, {
1234
- context: { maxMessageBytes },
1235
- rpcCode: -32600
1236
- });
1237
- }
1238
- if (line.trim().length === 0) throw invalidArgument("Protocol message is empty", void 0, -32700);
1239
- let parsed;
1240
- try {
1241
- parsed = JSON.parse(line);
1242
- } catch (cause) {
1243
- throw new BrowserPilotError("invalid_argument", "Invalid JSON", { rpcCode: -32700, cause });
1244
- }
1245
- if (!isRecord(parsed) || parsed.jsonrpc !== "2.0") {
1246
- throw invalidArgument("Message must be a JSON-RPC 2.0 object", "jsonrpc", -32600);
1247
- }
1248
- if (typeof parsed.method === "string") {
1249
- assertOnlyKeys(parsed, ["jsonrpc", "id", "method", "params"], "message");
1250
- const method = requireString(parsed, "method", { maxLength: 256 });
1251
- const params = parseParams(parsed.params);
1252
- if (parsed.id === void 0) {
1253
- return { jsonrpc: "2.0", method, ...params !== void 0 ? { params } : {} };
1254
- }
1255
- return {
1256
- jsonrpc: "2.0",
1257
- id: parseId(parsed.id, false),
1258
- method,
1259
- ...params !== void 0 ? { params } : {}
1260
- };
1261
- }
1262
- if (parsed.id === void 0) {
1263
- throw invalidArgument("JSON-RPC response must include id", "id", -32600);
1264
- }
1265
- const id = parseId(parsed.id, true);
1266
- const hasResult = Object.hasOwn(parsed, "result");
1267
- const hasError = Object.hasOwn(parsed, "error");
1268
- if (hasResult === hasError) {
1269
- throw invalidArgument("JSON-RPC response must contain exactly one of result or error", void 0, -32600);
1270
- }
1271
- if (hasResult) {
1272
- assertOnlyKeys(parsed, ["jsonrpc", "id", "result"], "message");
1273
- if (!isJsonValue(parsed.result)) throw invalidArgument("JSON-RPC result must be valid JSON", "result", -32600);
1274
- return { jsonrpc: "2.0", id, result: parsed.result };
1275
- }
1276
- assertOnlyKeys(parsed, ["jsonrpc", "id", "error"], "message");
1277
- return { jsonrpc: "2.0", id, error: parseErrorObject(parsed.error) };
1278
- }
1279
-
1280
- // src/services/broker-runtime.ts
1281
- import { randomUUID as randomUUID4 } from "crypto";
1282
-
1283
- // src/protocol/schema.ts
1284
- function isRecord2(value) {
1285
- return typeof value === "object" && value !== null && !Array.isArray(value);
1286
- }
1287
- function assertSchemaDefinition(schema, path = "$schema") {
1288
- const sensitivity = schema["x-browser-pilot-sensitivity"];
1289
- if (sensitivity) {
1290
- if (sensitivity.length === 0 || new Set(sensitivity).size !== sensitivity.length) {
1291
- throw new Error(`${path}.x-browser-pilot-sensitivity must be non-empty and unique`);
1292
- }
1293
- for (const value of sensitivity) {
1294
- if (!SENSITIVITIES.includes(value)) {
1295
- throw new Error(`${path}.x-browser-pilot-sensitivity contains unknown value ${value}`);
1296
- }
1297
- }
1298
- }
1299
- if (schema.type === "object") {
1300
- const properties = schema.properties ?? {};
1301
- for (const required of schema.required ?? []) {
1302
- if (!Object.hasOwn(properties, required)) {
1303
- throw new Error(`${path}.required references unknown property ${required}`);
1304
- }
1305
- }
1306
- for (const [name, property] of Object.entries(properties)) {
1307
- assertSchemaDefinition(property, `${path}.properties.${name}`);
1308
- }
1309
- if (isRecord2(schema.additionalProperties)) {
1310
- assertSchemaDefinition(schema.additionalProperties, `${path}.additionalProperties`);
1311
- }
1312
- }
1313
- if (schema.type === "array" && schema.items) {
1314
- assertSchemaDefinition(schema.items, `${path}.items`);
1315
- }
1316
- for (const [index, alternative] of (schema.oneOf ?? []).entries()) {
1317
- assertSchemaDefinition(alternative, `${path}.oneOf[${index}]`);
1318
- }
1319
- if (schema.pattern) new RegExp(schema.pattern, "u");
1320
- }
1321
- var stringSchema = (options = {}) => ({ type: "string", ...options });
1322
- var booleanSchema = (options = {}) => ({ type: "boolean", ...options });
1323
- var integerSchema = (options = {}) => ({ type: "integer", ...options });
1324
- var numberSchema = (options = {}) => ({ type: "number", ...options });
1325
- var arraySchema = (items, options = {}) => ({
1326
- type: "array",
1327
- items,
1328
- ...options
1329
- });
1330
- var objectSchema = (properties, required = [], options = {}) => ({
1331
- type: "object",
1332
- properties,
1333
- required,
1334
- additionalProperties: false,
1335
- ...options
1336
- });
1337
-
1338
- // src/protocol/tools.ts
1339
- var emptyInput = objectSchema({});
1340
- function sensitive(schema, ...sensitivity) {
1341
- return { ...schema, "x-browser-pilot-sensitivity": sensitivity };
1342
- }
1343
- var boundedText = sensitive(
1344
- stringSchema({ minLength: 1, maxLength: 1e6 }),
1345
- "browser_data",
1346
- "credential"
1347
- );
1348
- var boundedSelector = sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data");
1349
- var boundedUrl = sensitive(stringSchema({ minLength: 1, maxLength: 16384 }), "browser_data");
1350
- var opaqueIdSchema = stringSchema({ minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$" });
1351
- var headerSchema = objectSchema({
1352
- name: sensitive(stringSchema({ minLength: 1, maxLength: 256 }), "browser_data"),
1353
- value: sensitive(stringSchema({ maxLength: 8192 }), "browser_data", "credential")
1354
- }, ["name", "value"]);
1355
- var networkRuleSchema = objectSchema({
1356
- ruleId: opaqueIdSchema,
1357
- type: stringSchema({ enum: ["block", "mock", "headers"] }),
1358
- pattern: boundedUrl,
1359
- status: integerSchema({ minimum: 100, maximum: 999 }),
1360
- headers: arraySchema(headerSchema, { maxItems: 256 }),
1361
- bodySize: integerSchema({ minimum: 0 })
1362
- }, ["ruleId", "type", "pattern"]);
1363
- var networkRequestDetailSchema = objectSchema({
1364
- requestId: opaqueIdSchema,
1365
- sequence: integerSchema({ minimum: 1 }),
1366
- method: stringSchema({ maxLength: 32 }),
1367
- url: boundedUrl,
1368
- type: stringSchema({ maxLength: 128 }),
1369
- requestHeaders: arraySchema(headerSchema, { maxItems: 256 }),
1370
- postData: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential"),
1371
- postDataTruncated: booleanSchema(),
1372
- status: integerSchema({ minimum: 100, maximum: 999 }),
1373
- statusText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1374
- responseHeaders: arraySchema(headerSchema, { maxItems: 256 }),
1375
- mimeType: stringSchema({ maxLength: 256 }),
1376
- size: integerSchema({ minimum: 0 }),
1377
- durationMs: numberSchema({ minimum: 0 }),
1378
- error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1379
- bodyAvailable: booleanSchema()
1380
- }, [
1381
- "requestId",
1382
- "sequence",
1383
- "method",
1384
- "url",
1385
- "type",
1386
- "requestHeaders",
1387
- "postDataTruncated",
1388
- "bodyAvailable"
1389
- ]);
1390
- var elementSchema = objectSchema({
1391
- ref: integerSchema({ minimum: 1 }),
1392
- role: stringSchema({ minLength: 1, maxLength: 128 }),
1393
- name: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementNameCharacters }), "browser_data"),
1394
- value: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementValueCharacters }), "browser_data", "credential"),
1395
- checked: booleanSchema()
1396
- }, ["ref", "role", "name"]);
1397
- var inputEvidenceSchema = objectSchema({
1398
- action: stringSchema({ enum: ["type", "keyboard"] }),
1399
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1400
- kind: stringSchema({ enum: ["input", "contenteditable", "unsupported"] }),
1401
- sensitive: booleanSchema(),
1402
- beforeLength: integerSchema({ minimum: 0 }),
1403
- expectedLength: integerSchema({ minimum: 0 }),
1404
- afterLength: integerSchema({ minimum: 0 }),
1405
- reason: stringSchema({ enum: ["active_element_not_readable", "value_mismatch"] })
1406
- }, ["action", "status", "kind", "sensitive"]);
1407
- var clickEvidenceSchema = objectSchema({
1408
- action: stringSchema({ const: "click" }),
1409
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1410
- kind: stringSchema({ enum: [
1411
- "checkbox",
1412
- "radio",
1413
- "switch",
1414
- "option",
1415
- "select",
1416
- "control",
1417
- "other",
1418
- "coordinates"
1419
- ] }),
1420
- effects: arraySchema(stringSchema({ enum: [
1421
- "checked_changed",
1422
- "selected_changed",
1423
- "pressed_changed",
1424
- "expanded_changed",
1425
- "focus_changed",
1426
- "navigation",
1427
- "document_changed",
1428
- "dialog_opened",
1429
- "popup_opened"
1430
- ] }), { maxItems: 9, uniqueItems: true }),
1431
- checked: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
1432
- selected: booleanSchema(),
1433
- pressed: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
1434
- expanded: booleanSchema(),
1435
- focused: booleanSchema(),
1436
- reason: stringSchema({ enum: [
1437
- "coordinate_target",
1438
- "target_unavailable",
1439
- "expected_state_unchanged",
1440
- "no_observable_effect"
1441
- ] })
1442
- }, ["action", "status", "kind", "effects"]);
1443
- var pressEvidenceSchema = objectSchema({
1444
- action: stringSchema({ const: "press" }),
1445
- status: stringSchema({ enum: ["verified", "unavailable"] }),
1446
- kind: stringSchema({ enum: [
1447
- "input",
1448
- "contenteditable",
1449
- "checkbox",
1450
- "radio",
1451
- "select",
1452
- "control",
1453
- "other"
1454
- ] }),
1455
- effects: arraySchema(stringSchema({ enum: [
1456
- "value_changed",
1457
- "checked_changed",
1458
- "selected_changed",
1459
- "pressed_changed",
1460
- "expanded_changed",
1461
- "focus_changed",
1462
- "navigation",
1463
- "document_changed",
1464
- "dialog_opened",
1465
- "popup_opened"
1466
- ] }), { maxItems: 10, uniqueItems: true }),
1467
- sensitive: booleanSchema(),
1468
- reason: stringSchema({ enum: ["target_unavailable", "no_observable_effect"] })
1469
- }, ["action", "status", "kind", "effects", "sensitive"]);
1470
- var uploadEvidenceSchema = objectSchema({
1471
- action: stringSchema({ const: "upload" }),
1472
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1473
- expectedFileCount: integerSchema({ const: 1 }),
1474
- fileCount: integerSchema({ minimum: 0 }),
1475
- nameMatched: booleanSchema(),
1476
- reason: stringSchema({ enum: [
1477
- "target_unavailable",
1478
- "file_count_mismatch",
1479
- "file_name_mismatch"
1480
- ] })
1481
- }, ["action", "status", "expectedFileCount"]);
1482
- var scrollEvidenceSchema = objectSchema({
1483
- action: stringSchema({ const: "scroll" }),
1484
- status: stringSchema({ enum: ["verified", "mismatch"] }),
1485
- mode: stringSchema({ enum: ["relative", "position", "text"] }),
1486
- target: stringSchema({ enum: ["page", "element", "text"] }),
1487
- moved: booleanSchema(),
1488
- deltaX: numberSchema(),
1489
- deltaY: numberSchema(),
1490
- beforeX: numberSchema({ minimum: 0 }),
1491
- beforeY: numberSchema({ minimum: 0 }),
1492
- afterX: numberSchema({ minimum: 0 }),
1493
- afterY: numberSchema({ minimum: 0 }),
1494
- matchedText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1495
- reason: stringSchema({ enum: ["at_boundary", "text_not_found", "text_not_revealed"] })
1496
- }, [
1497
- "action",
1498
- "status",
1499
- "mode",
1500
- "target",
1501
- "moved",
1502
- "deltaX",
1503
- "deltaY",
1504
- "beforeX",
1505
- "beforeY",
1506
- "afterX",
1507
- "afterY"
1508
- ]);
1509
- var dropdownOptionSchema = objectSchema({
1510
- index: integerSchema({ minimum: 1, maximum: 500 }),
1511
- label: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1512
- value: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1513
- selected: booleanSchema(),
1514
- disabled: booleanSchema()
1515
- }, ["index", "label", "value", "selected", "disabled"]);
1516
- var selectEvidenceSchema = objectSchema({
1517
- action: stringSchema({ const: "select" }),
1518
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1519
- kind: stringSchema({ enum: ["native", "aria"] }),
1520
- selected: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
1521
- reason: stringSchema({ enum: ["option_not_found", "selection_mismatch", "selection_not_exposed"] })
1522
- }, ["action", "status", "kind", "selected"]);
1523
- var artifactSchema = objectSchema({
1524
- id: opaqueIdSchema,
1525
- workspaceId: opaqueIdSchema,
1526
- kind: stringSchema({ enum: ["screenshot", "screenshot_preview", "pdf", "download", "upload_input"] }),
1527
- mimeType: stringSchema({ minLength: 1, maxLength: 256 }),
1528
- byteSize: integerSchema({ minimum: 0 }),
1529
- fileName: stringSchema({ minLength: 1, maxLength: 4096 }),
1530
- width: integerSchema({ minimum: 1 }),
1531
- height: integerSchema({ minimum: 1 }),
1532
- sensitivity: stringSchema({ enum: ["public", "browser_data", "credential", "user_file"] }),
1533
- createdAt: integerSchema({ minimum: 0 }),
1534
- expiresAt: integerSchema({ minimum: 0 }),
1535
- retained: booleanSchema(),
1536
- previewOf: opaqueIdSchema
1537
- }, ["id", "workspaceId", "kind", "mimeType", "byteSize", "sensitivity", "createdAt", "expiresAt", "retained"]);
1538
- var hintRefsSchema = arraySchema(integerSchema({ minimum: 1 }), {
1539
- maxItems: 32,
1540
- uniqueItems: true
1541
- });
1542
- var agentHintSchema = {
1543
- oneOf: [
1544
- objectSchema({
1545
- code: stringSchema({ const: "autocomplete" }),
1546
- source: stringSchema({ const: "observation" }),
1547
- confidence: stringSchema({ enum: ["strong", "possible"] }),
1548
- recommendedAction: stringSchema({ const: "observe_then_select" }),
1549
- refs: hintRefsSchema
1550
- }, ["code", "source", "confidence", "recommendedAction", "refs"]),
1551
- objectSchema({
1552
- code: stringSchema({ const: "modal_overlay" }),
1553
- source: stringSchema({ const: "observation" }),
1554
- confidence: stringSchema({ enum: ["strong", "possible"] }),
1555
- recommendedAction: stringSchema({ const: "resolve_overlay_first" }),
1556
- blocking: booleanSchema(),
1557
- refs: hintRefsSchema
1558
- }, ["code", "source", "confidence", "recommendedAction", "blocking", "refs"]),
1559
- objectSchema({
1560
- code: stringSchema({ const: "filter_controls" }),
1561
- source: stringSchema({ const: "observation" }),
1562
- confidence: stringSchema({ const: "strong" }),
1563
- recommendedAction: stringSchema({ const: "review_refinement_controls" }),
1564
- refs: hintRefsSchema
1565
- }, ["code", "source", "confidence", "recommendedAction", "refs"]),
1566
- objectSchema({
1567
- code: stringSchema({ const: "access_blocked" }),
1568
- source: stringSchema({ const: "network" }),
1569
- confidence: stringSchema({ const: "strong" }),
1570
- recommendedAction: stringSchema({ const: "avoid_same_navigation_retry" }),
1571
- status: integerSchema({ enum: [403, 429] })
1572
- }, ["code", "source", "confidence", "recommendedAction", "status"]),
1573
- objectSchema({
1574
- code: stringSchema({ const: "authentication_surface" }),
1575
- source: stringSchema({ const: "observation" }),
1576
- confidence: stringSchema({ const: "strong" }),
1577
- recommendedAction: stringSchema({ const: "inspect_authentication_state" }),
1578
- state: stringSchema({ enum: ["present", "entered", "left"] })
1579
- }, ["code", "source", "confidence", "recommendedAction", "state"]),
1580
- objectSchema({
1581
- code: stringSchema({ const: "download" }),
1582
- source: stringSchema({ const: "download" }),
1583
- confidence: stringSchema({ const: "strong" }),
1584
- recommendedAction: stringSchema({ const: "wait_for_download" }),
1585
- state: stringSchema({ const: "started" })
1586
- }, ["code", "source", "confidence", "recommendedAction", "state"]),
1587
- objectSchema({
1588
- code: stringSchema({ const: "download" }),
1589
- source: stringSchema({ const: "download" }),
1590
- confidence: stringSchema({ const: "strong" }),
1591
- recommendedAction: stringSchema({ const: "inspect_download_artifact" }),
1592
- state: stringSchema({ const: "completed" }),
1593
- artifactId: opaqueIdSchema
1594
- }, ["code", "source", "confidence", "recommendedAction", "state", "artifactId"]),
1595
- objectSchema({
1596
- code: stringSchema({ const: "download" }),
1597
- source: stringSchema({ const: "download" }),
1598
- confidence: stringSchema({ const: "strong" }),
1599
- recommendedAction: stringSchema({ const: "inspect_download_failure" }),
1600
- state: stringSchema({ enum: ["failed", "cancelled"] }),
1601
- reason: stringSchema({ minLength: 1, maxLength: 128 })
1602
- }, ["code", "source", "confidence", "recommendedAction", "state", "reason"]),
1603
- objectSchema({
1604
- code: stringSchema({ const: "repeated_action" }),
1605
- source: stringSchema({ const: "watchdog" }),
1606
- confidence: stringSchema({ const: "strong" }),
1607
- recommendedAction: stringSchema({ const: "change_strategy" }),
1608
- streak: integerSchema({ minimum: 1, maximum: 1e6 }),
1609
- reason: stringSchema({ minLength: 1, maxLength: 128 })
1610
- }, ["code", "source", "confidence", "recommendedAction", "streak", "reason"])
1611
- ]
1612
- };
1613
- function resultSchema(context, properties = {}, required = []) {
1614
- const contextProperties = {};
1615
- const contextRequired = [];
1616
- if (context === "workspace" || context === "target") {
1617
- contextProperties.workspaceId = opaqueIdSchema;
1618
- contextProperties.leaseId = opaqueIdSchema;
1619
- contextRequired.push("workspaceId", "leaseId");
1620
- }
1621
- if (context === "target") {
1622
- contextProperties.targetId = opaqueIdSchema;
1623
- contextProperties.profileContextId = opaqueIdSchema;
1624
- contextProperties.url = boundedUrl;
1625
- contextRequired.push("targetId", "url");
1626
- }
1627
- return objectSchema({ ...contextProperties, ...properties }, [...contextRequired, ...required]);
1628
- }
1629
- var observationOutput = resultSchema("target", {
1630
- observationId: opaqueIdSchema,
1631
- title: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxTitleCharacters }), "browser_data"),
1632
- page: objectSchema({
1633
- viewportWidth: integerSchema({ minimum: 0 }),
1634
- viewportHeight: integerSchema({ minimum: 0 }),
1635
- documentWidth: integerSchema({ minimum: 0 }),
1636
- documentHeight: integerSchema({ minimum: 0 }),
1637
- scrollX: integerSchema({ minimum: 0 }),
1638
- scrollY: integerSchema({ minimum: 0 }),
1639
- pixelsAbove: integerSchema({ minimum: 0 }),
1640
- pixelsBelow: integerSchema({ minimum: 0 }),
1641
- pixelsLeft: integerSchema({ minimum: 0 }),
1642
- pixelsRight: integerSchema({ minimum: 0 }),
1643
- scrollPercentX: numberSchema({ minimum: 0, maximum: 100 }),
1644
- scrollPercentY: numberSchema({ minimum: 0, maximum: 100 })
1645
- }, [
1646
- "viewportWidth",
1647
- "viewportHeight",
1648
- "documentWidth",
1649
- "documentHeight",
1650
- "scrollX",
1651
- "scrollY",
1652
- "pixelsAbove",
1653
- "pixelsBelow",
1654
- "pixelsLeft",
1655
- "pixelsRight",
1656
- "scrollPercentX",
1657
- "scrollPercentY"
1658
- ]),
1659
- elements: arraySchema(elementSchema, { maxItems: OBSERVATION_V1_LIMITS.maxElements }),
1660
- truncated: booleanSchema(),
1661
- truncationReasons: arraySchema(stringSchema({
1662
- enum: [...OBSERVATION_TRUNCATION_REASONS]
1663
- }), { uniqueItems: true }),
1664
- hints: arraySchema(agentHintSchema, { maxItems: 16 }),
1665
- evidence: { oneOf: [
1666
- inputEvidenceSchema,
1667
- clickEvidenceSchema,
1668
- pressEvidenceSchema,
1669
- uploadEvidenceSchema,
1670
- scrollEvidenceSchema,
1671
- selectEvidenceSchema
1672
- ] }
1673
- }, ["observationId", "title", "elements", "truncated", "truncationReasons"]);
1674
- var artifactOutput = resultSchema("target", {
1675
- artifact: artifactSchema,
1676
- preview: artifactSchema,
1677
- annotationCount: integerSchema({ minimum: 0, maximum: 200 })
1678
- }, ["artifact"]);
1679
- var elementAddressSchema = {
1680
- oneOf: [
1681
- objectSchema({
1682
- observationId: opaqueIdSchema,
1683
- ref: integerSchema({ minimum: 1 })
1684
- }, ["observationId", "ref"]),
1685
- objectSchema({ selector: boundedSelector }, ["selector"])
1686
- ]
1687
- };
1688
- var dropdownChoiceSchema = {
1689
- oneOf: [
1690
- objectSchema({ by: stringSchema({ const: "index" }), index: integerSchema({ minimum: 1, maximum: 500 }) }, ["by", "index"]),
1691
- objectSchema({
1692
- by: stringSchema({ const: "label" }),
1693
- label: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1694
- exact: booleanSchema()
1695
- }, ["by", "label"]),
1696
- objectSchema({
1697
- by: stringSchema({ const: "value" }),
1698
- value: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1699
- exact: booleanSchema()
1700
- }, ["by", "value"])
1701
- ]
1702
- };
1703
- var profileRepresentativeTabSchema = objectSchema({
1704
- targetId: opaqueIdSchema,
1705
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1706
- url: boundedUrl
1707
- }, ["targetId", "title", "url"]);
1708
- var profileContextSchema = objectSchema({
1709
- profileContextId: opaqueIdSchema,
1710
- label: stringSchema({ minLength: 1, maxLength: 128 }),
1711
- identityStatus: stringSchema({ enum: ["unidentified", "verified", "unavailable"] }),
1712
- profileName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1713
- accountName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1714
- accountEmail: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1715
- profileDirectory: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1716
- identityErrorCode: stringSchema({ enum: [...PROFILE_IDENTITY_ERROR_CODES] }),
1717
- displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1718
- tabCount: integerSchema({ minimum: 0 }),
1719
- eligibleTabCount: integerSchema({ minimum: 0 }),
1720
- selected: booleanSchema(),
1721
- representativeTabs: arraySchema(profileRepresentativeTabSchema, { maxItems: 3 })
1722
- }, [
1723
- "profileContextId",
1724
- "label",
1725
- "tabCount",
1726
- "eligibleTabCount",
1727
- "selected",
1728
- "representativeTabs"
1729
- ]);
1730
- function tool(definition) {
1731
- return definition;
1732
- }
1733
- var TOOL_DEFINITIONS = [
1734
- tool({
1735
- name: "browser.discover",
1736
- title: "Discover browsers",
1737
- description: "List supported local browsers and structured setup or authorization state.",
1738
- context: "connection",
1739
- inputSchema: objectSchema({ browser: stringSchema({ minLength: 1, maxLength: 128 }) }),
1740
- outputSchema: resultSchema("connection", {
1741
- browsers: arraySchema(objectSchema({
1742
- id: opaqueIdSchema,
1743
- product: stringSchema({ minLength: 1, maxLength: 128 }),
1744
- channel: stringSchema({ maxLength: 128 }),
1745
- userDataRoot: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1746
- profile: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1747
- processState: stringSchema({ enum: ["running", "not_running", "unknown"] }),
1748
- remoteDebuggingState: stringSchema({ enum: ["enabled", "disabled", "stale"] }),
1749
- authorizationState: stringSchema({ enum: ["authorized", "required", "not_applicable", "unknown"] }),
1750
- state: stringSchema({ enum: ["ready", "not_running", "remote_debugging_disabled", "authorization_required", "disconnected"] }),
1751
- remediation: objectSchema({
1752
- code: stringSchema({ minLength: 1, maxLength: 128 }),
1753
- message: stringSchema({ minLength: 1, maxLength: 4096 }),
1754
- actionRequired: booleanSchema()
1755
- }, ["code", "message", "actionRequired"])
1756
- }, [
1757
- "id",
1758
- "product",
1759
- "processState",
1760
- "remoteDebuggingState",
1761
- "authorizationState",
1762
- "state"
1763
- ]))
1764
- }, ["browsers"]),
1765
- requiredCapabilities: ["browser.discovery"],
1766
- mutating: false,
1767
- idempotency: "read_only",
1768
- cancellation: "not_applicable",
1769
- sensitivity: { input: [], output: ["browser_data"] },
1770
- artifactKinds: []
1771
- }),
1772
- tool({
1773
- name: "browser.connect",
1774
- title: "Connect browser",
1775
- description: "Connect a Workspace to a selected authorized browser instance.",
1776
- context: "workspace",
1777
- inputSchema: objectSchema({ browserId: opaqueIdSchema }, ["browserId"]),
1778
- outputSchema: resultSchema("workspace", {
1779
- browserInstanceId: opaqueIdSchema,
1780
- connectionGeneration: integerSchema({ minimum: 1 }),
1781
- state: stringSchema({ const: "connected" })
1782
- }, ["browserInstanceId", "connectionGeneration", "state"]),
1783
- requiredCapabilities: ["browser.control"],
1784
- mutating: true,
1785
- idempotency: "idempotent",
1786
- cancellation: "before_dispatch",
1787
- sensitivity: { input: [], output: ["browser_data"] },
1788
- artifactKinds: []
1789
- }),
1790
- tool({
1791
- name: "browser.profiles.list",
1792
- title: "List browser Profiles",
1793
- description: "Passively list live Profile contexts and bounded representative tabs.",
1794
- context: "workspace",
1795
- inputSchema: emptyInput,
1796
- outputSchema: resultSchema("workspace", {
1797
- profiles: arraySchema(profileContextSchema, { maxItems: 128 })
1798
- }, ["profiles"]),
1799
- requiredCapabilities: ["browser.control"],
1800
- mutating: false,
1801
- idempotency: "read_only",
1802
- cancellation: "not_applicable",
1803
- sensitivity: { input: [], output: ["browser_data"] },
1804
- artifactKinds: []
1805
- }),
1806
- tool({
1807
- name: "browser.profiles.identify",
1808
- title: "Identify browser Profiles",
1809
- description: "Explicitly identify live Chrome Profiles using temporary visible chrome://version targets.",
1810
- context: "workspace",
1811
- inputSchema: objectSchema({
1812
- profileContextId: opaqueIdSchema,
1813
- refresh: booleanSchema()
1814
- }),
1815
- outputSchema: resultSchema("workspace", {
1816
- profiles: arraySchema(profileContextSchema, { maxItems: 128 })
1817
- }, ["profiles"]),
1818
- requiredCapabilities: ["browser.control"],
1819
- mutating: true,
1820
- idempotency: "non_idempotent",
1821
- cancellation: "best_effort",
1822
- sensitivity: { input: [], output: ["browser_data"] },
1823
- artifactKinds: []
1824
- }),
1825
- tool({
1826
- name: "browser.profiles.select",
1827
- title: "Select browser Profile",
1828
- description: "Select one current Profile context for new managed targets in this Workspace.",
1829
- context: "workspace",
1830
- inputSchema: objectSchema({ profileContextId: opaqueIdSchema }, ["profileContextId"]),
1831
- outputSchema: resultSchema("workspace", {
1832
- profileContextId: opaqueIdSchema,
1833
- label: stringSchema({ minLength: 1, maxLength: 128 }),
1834
- identityStatus: stringSchema({ enum: ["unidentified", "verified", "unavailable"] }),
1835
- profileName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1836
- accountName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1837
- accountEmail: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1838
- profileDirectory: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1839
- identityErrorCode: stringSchema({ enum: [...PROFILE_IDENTITY_ERROR_CODES] }),
1840
- displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data")
1841
- }, ["profileContextId", "label"]),
1842
- requiredCapabilities: ["browser.control"],
1843
- mutating: true,
1844
- idempotency: "idempotent",
1845
- cancellation: "before_dispatch",
1846
- sensitivity: { input: [], output: ["browser_data"] },
1847
- artifactKinds: []
1848
- }),
1849
- tool({
1850
- name: "browser.open",
1851
- title: "Open URL",
1852
- description: "Navigate an authorized controlled target or create a new target in the ManagedTabSet.",
1853
- context: "workspace",
1854
- inputSchema: objectSchema({
1855
- url: boundedUrl,
1856
- targetId: opaqueIdSchema,
1857
- newTarget: booleanSchema(),
1858
- profileContextId: opaqueIdSchema,
1859
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
1860
- }, ["url"]),
1861
- outputSchema: observationOutput,
1862
- requiredCapabilities: ["browser.control", "observation.read"],
1863
- mutating: true,
1864
- idempotency: "non_idempotent",
1865
- cancellation: "best_effort",
1866
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
1867
- artifactKinds: []
1868
- }),
1869
- tool({
1870
- name: "browser.observe",
1871
- title: "Observe page",
1872
- description: "Create an immutable bounded Observation with numbered refs.",
1873
- context: "target",
1874
- inputSchema: objectSchema({
1875
- limit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
1876
- }),
1877
- outputSchema: observationOutput,
1878
- requiredCapabilities: ["observation.read"],
1879
- mutating: false,
1880
- idempotency: "read_only",
1881
- cancellation: "best_effort",
1882
- sensitivity: { input: [], output: ["browser_data", "credential"] },
1883
- artifactKinds: []
1884
- }),
1885
- tool({
1886
- name: "browser.observation.latest",
1887
- title: "Get latest Observation",
1888
- description: "Return the latest live Observation descriptor for a controlled target.",
1889
- context: "target",
1890
- inputSchema: emptyInput,
1891
- outputSchema: resultSchema("target", {
1892
- observationId: opaqueIdSchema,
1893
- createdAt: integerSchema({ minimum: 0 }),
1894
- expiresAt: integerSchema({ minimum: 0 }),
1895
- elementCount: integerSchema({ minimum: 0, maximum: OBSERVATION_V1_LIMITS.maxElements })
1896
- }, ["observationId", "createdAt", "expiresAt", "elementCount"]),
1897
- requiredCapabilities: ["observation.read"],
1898
- mutating: false,
1899
- idempotency: "read_only",
1900
- cancellation: "not_applicable",
1901
- sensitivity: { input: [], output: ["browser_data"] },
1902
- artifactKinds: []
1903
- }),
1904
- tool({
1905
- name: "browser.locate",
1906
- title: "Locate element",
1907
- description: "Return viewport coordinates for a CSS selector in the selected frame.",
1908
- context: "target",
1909
- inputSchema: objectSchema({ selector: boundedSelector }, ["selector"]),
1910
- outputSchema: resultSchema("target", {
1911
- x: numberSchema(),
1912
- y: numberSchema(),
1913
- top: numberSchema(),
1914
- left: numberSchema(),
1915
- width: numberSchema({ minimum: 0 }),
1916
- height: numberSchema({ minimum: 0 })
1917
- }, ["x", "y", "top", "left", "width", "height"]),
1918
- requiredCapabilities: ["observation.read"],
1919
- mutating: false,
1920
- idempotency: "read_only",
1921
- cancellation: "best_effort",
1922
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
1923
- artifactKinds: []
1924
- }),
1925
- tool({
1926
- name: "browser.read",
1927
- title: "Read page text",
1928
- description: "Read bounded text from the page or a selector.",
1929
- context: "target",
1930
- inputSchema: objectSchema({
1931
- selector: boundedSelector,
1932
- limit: integerSchema({ minimum: 1, maximum: 1e6 })
1933
- }),
1934
- outputSchema: resultSchema("target", {
1935
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1936
- text: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data"),
1937
- length: integerSchema({ minimum: 0 }),
1938
- truncated: booleanSchema()
1939
- }, ["title", "text", "length", "truncated"]),
1940
- requiredCapabilities: ["observation.read"],
1941
- mutating: false,
1942
- idempotency: "read_only",
1943
- cancellation: "best_effort",
1944
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
1945
- artifactKinds: []
1946
- }),
1947
- tool({
1948
- name: "browser.search",
1949
- title: "Search page text",
1950
- description: "Find bounded visible text matches without returning the entire page.",
1951
- context: "target",
1952
- inputSchema: objectSchema({
1953
- query: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1954
- selector: boundedSelector,
1955
- caseSensitive: booleanSchema(),
1956
- wholeWord: booleanSchema(),
1957
- limit: integerSchema({ minimum: 1, maximum: 200 })
1958
- }, ["query"]),
1959
- outputSchema: resultSchema("target", {
1960
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1961
- totalMatches: integerSchema({ minimum: 0 }),
1962
- matches: arraySchema(objectSchema({
1963
- index: integerSchema({ minimum: 1 }),
1964
- text: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1965
- context: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
1966
- tagName: stringSchema({ maxLength: 64 }),
1967
- visible: booleanSchema(),
1968
- x: numberSchema(),
1969
- y: numberSchema(),
1970
- width: numberSchema({ minimum: 0 }),
1971
- height: numberSchema({ minimum: 0 })
1972
- }, ["index", "text", "context", "tagName", "visible", "x", "y", "width", "height"]), { maxItems: 200 }),
1973
- truncated: booleanSchema()
1974
- }, ["title", "totalMatches", "matches", "truncated"]),
1975
- requiredCapabilities: ["observation.read"],
1976
- mutating: false,
1977
- idempotency: "read_only",
1978
- cancellation: "best_effort",
1979
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
1980
- artifactKinds: []
1981
- }),
1982
- tool({
1983
- name: "browser.elements.find",
1984
- title: "Find DOM elements",
1985
- description: "Run a bounded CSS query and return safe element metadata without exposing DOM handles.",
1986
- context: "target",
1987
- inputSchema: objectSchema({
1988
- selector: boundedSelector,
1989
- limit: integerSchema({ minimum: 1, maximum: 200 }),
1990
- attributeNames: arraySchema(
1991
- sensitive(stringSchema({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z_:][A-Za-z0-9_.:-]*$" }), "browser_data"),
1992
- { maxItems: 20, uniqueItems: true }
1993
- ),
1994
- pierceShadow: booleanSchema()
1995
- }, ["selector"]),
1996
- outputSchema: resultSchema("target", {
1997
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1998
- totalMatches: integerSchema({ minimum: 0 }),
1999
- elements: arraySchema(objectSchema({
2000
- index: integerSchema({ minimum: 1 }),
2001
- tagName: stringSchema({ maxLength: 64 }),
2002
- role: stringSchema({ maxLength: 128 }),
2003
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2004
- text: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
2005
- visible: booleanSchema(),
2006
- enabled: booleanSchema(),
2007
- x: numberSchema(),
2008
- y: numberSchema(),
2009
- width: numberSchema({ minimum: 0 }),
2010
- height: numberSchema({ minimum: 0 }),
2011
- attributes: arraySchema(objectSchema({
2012
- name: sensitive(stringSchema({ maxLength: 128 }), "browser_data"),
2013
- value: sensitive(stringSchema({ maxLength: 2048 }), "browser_data", "credential")
2014
- }, ["name", "value"]), { maxItems: 20 })
2015
- }, [
2016
- "index",
2017
- "tagName",
2018
- "role",
2019
- "name",
2020
- "text",
2021
- "visible",
2022
- "enabled",
2023
- "x",
2024
- "y",
2025
- "width",
2026
- "height",
2027
- "attributes"
2028
- ]), { maxItems: 200 }),
2029
- truncated: booleanSchema()
2030
- }, ["title", "totalMatches", "elements", "truncated"]),
2031
- requiredCapabilities: ["observation.read"],
2032
- mutating: false,
2033
- idempotency: "read_only",
2034
- cancellation: "best_effort",
2035
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2036
- artifactKinds: []
2037
- }),
2038
- tool({
2039
- name: "browser.scroll",
2040
- title: "Scroll page or element",
2041
- description: "Scroll the page, a ref, a CSS-selected container, or visible text and return a fresh Observation.",
2042
- context: "target",
2043
- inputSchema: objectSchema({
2044
- target: elementAddressSchema,
2045
- direction: stringSchema({ enum: ["up", "down", "left", "right"] }),
2046
- amount: numberSchema({ minimum: 0.01, maximum: 1e5 }),
2047
- unit: stringSchema({ enum: ["pixels", "viewport"] }),
2048
- position: stringSchema({ enum: ["start", "end"] }),
2049
- text: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2050
- exact: booleanSchema(),
2051
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2052
- }),
2053
- outputSchema: observationOutput,
2054
- requiredCapabilities: ["action.input", "observation.read"],
2055
- mutating: true,
2056
- idempotency: "non_idempotent",
2057
- cancellation: "best_effort",
2058
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2059
- artifactKinds: []
2060
- }),
2061
- tool({
2062
- name: "browser.dropdown.options",
2063
- title: "List dropdown options",
2064
- description: "Enumerate bounded native or currently exposed ARIA dropdown options.",
2065
- context: "target",
2066
- inputSchema: objectSchema({ target: elementAddressSchema }, ["target"]),
2067
- outputSchema: resultSchema("target", {
2068
- kind: stringSchema({ enum: ["native", "aria"] }),
2069
- expanded: booleanSchema(),
2070
- multiple: booleanSchema(),
2071
- requiresOpen: booleanSchema(),
2072
- options: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
2073
- truncated: booleanSchema()
2074
- }, ["kind", "expanded", "multiple", "requiresOpen", "options", "truncated"]),
2075
- requiredCapabilities: ["observation.read"],
2076
- mutating: false,
2077
- idempotency: "read_only",
2078
- cancellation: "best_effort",
2079
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2080
- artifactKinds: []
2081
- }),
2082
- tool({
2083
- name: "browser.dropdown.select",
2084
- title: "Select dropdown option",
2085
- description: "Select and verify a native or ARIA dropdown option, opening custom controls when needed.",
2086
- context: "target",
2087
- inputSchema: objectSchema({
2088
- target: elementAddressSchema,
2089
- choice: dropdownChoiceSchema,
2090
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2091
- }, ["target", "choice"]),
2092
- outputSchema: observationOutput,
2093
- requiredCapabilities: ["action.input", "observation.read"],
2094
- mutating: true,
2095
- idempotency: "non_idempotent",
2096
- cancellation: "best_effort",
2097
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2098
- artifactKinds: []
2099
- }),
2100
- tool({
2101
- name: "browser.click",
2102
- title: "Click element",
2103
- description: "Click an Observation ref or viewport coordinates after interactability checks.",
2104
- context: "target",
2105
- inputSchema: objectSchema({
2106
- target: {
2107
- oneOf: [
2108
- objectSchema({ observationId: opaqueIdSchema, ref: integerSchema({ minimum: 1 }) }, ["observationId", "ref"]),
2109
- objectSchema({ x: numberSchema(), y: numberSchema() }, ["x", "y"])
2110
- ]
2111
- },
2112
- button: stringSchema({ enum: ["left", "right"] }),
2113
- clickCount: integerSchema({ minimum: 1, maximum: 2 }),
2114
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2115
- }, ["target"]),
2116
- outputSchema: observationOutput,
2117
- requiredCapabilities: ["action.input", "observation.read"],
2118
- mutating: true,
2119
- idempotency: "non_idempotent",
2120
- cancellation: "best_effort",
2121
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2122
- artifactKinds: []
2123
- }),
2124
- tool({
2125
- name: "browser.type",
2126
- title: "Type into element",
2127
- description: "Enter text into an Observation ref and read back the effective value.",
2128
- context: "target",
2129
- inputSchema: objectSchema({
2130
- observationId: opaqueIdSchema,
2131
- ref: integerSchema({ minimum: 1 }),
2132
- text: boundedText,
2133
- clear: booleanSchema(),
2134
- submit: booleanSchema(),
2135
- verification: stringSchema({ enum: ["report", "require_exact"] }),
2136
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2137
- }, ["observationId", "ref", "text"]),
2138
- outputSchema: observationOutput,
2139
- requiredCapabilities: ["action.input", "observation.read"],
2140
- mutating: true,
2141
- idempotency: "non_idempotent",
2142
- cancellation: "best_effort",
2143
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2144
- artifactKinds: []
2145
- }),
2146
- tool({
2147
- name: "browser.keyboard",
2148
- title: "Type with keyboard",
2149
- description: "Dispatch keyboard input to the currently focused page control.",
2150
- context: "target",
2151
- inputSchema: objectSchema({
2152
- text: boundedText,
2153
- clear: booleanSchema(),
2154
- submit: booleanSchema(),
2155
- delayMs: integerSchema({ minimum: 0, maximum: 6e4 }),
2156
- focusSelector: boundedSelector,
2157
- verification: stringSchema({ enum: ["report", "require_exact"] }),
2158
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2159
- }, ["text"]),
2160
- outputSchema: observationOutput,
2161
- requiredCapabilities: ["action.input", "observation.read"],
2162
- mutating: true,
2163
- idempotency: "non_idempotent",
2164
- cancellation: "best_effort",
2165
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2166
- artifactKinds: []
2167
- }),
2168
- tool({
2169
- name: "browser.press",
2170
- title: "Press key",
2171
- description: "Dispatch a key or key combination and report bounded observable effects.",
2172
- context: "target",
2173
- inputSchema: objectSchema({
2174
- key: stringSchema({ minLength: 1, maxLength: 128 }),
2175
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2176
- }, ["key"]),
2177
- outputSchema: observationOutput,
2178
- requiredCapabilities: ["action.input", "observation.read"],
2179
- mutating: true,
2180
- idempotency: "non_idempotent",
2181
- cancellation: "best_effort",
2182
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2183
- artifactKinds: []
2184
- }),
2185
- tool({
2186
- name: "browser.capture",
2187
- title: "Capture screenshot",
2188
- description: "Capture the viewport, full page, or a selected element as protected Artifacts.",
2189
- context: "target",
2190
- inputSchema: objectSchema({
2191
- fullPage: booleanSchema(),
2192
- selector: boundedSelector,
2193
- includeOriginal: booleanSchema(),
2194
- annotations: objectSchema({
2195
- observationId: opaqueIdSchema,
2196
- refs: arraySchema(integerSchema({ minimum: 1 }), { maxItems: 200, uniqueItems: true })
2197
- }, ["observationId"])
2198
- }),
2199
- outputSchema: artifactOutput,
2200
- requiredCapabilities: ["artifact.read"],
2201
- mutating: false,
2202
- idempotency: "idempotent",
2203
- cancellation: "best_effort",
2204
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2205
- artifactKinds: ["screenshot", "screenshot_preview"]
2206
- }),
2207
- tool({
2208
- name: "browser.pdf",
2209
- title: "Capture PDF",
2210
- description: "Print the controlled target to a protected PDF Artifact.",
2211
- context: "target",
2212
- inputSchema: objectSchema({ landscape: booleanSchema() }),
2213
- outputSchema: artifactOutput,
2214
- requiredCapabilities: ["artifact.read"],
2215
- mutating: false,
2216
- idempotency: "idempotent",
2217
- cancellation: "best_effort",
2218
- sensitivity: { input: [], output: ["browser_data"] },
2219
- artifactKinds: ["pdf"]
2220
- }),
2221
- tool({
2222
- name: "browser.upload",
2223
- title: "Upload file",
2224
- description: "Assign a client-authorized local file and verify the browser selection.",
2225
- context: "target",
2226
- inputSchema: {
2227
- oneOf: [
2228
- objectSchema({
2229
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2230
- observationId: opaqueIdSchema,
2231
- ref: integerSchema({ minimum: 1 }),
2232
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2233
- }, ["artifactId", "observationId", "ref"]),
2234
- objectSchema({
2235
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2236
- inputIndex: integerSchema({ minimum: 1 }),
2237
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2238
- }, ["artifactId", "inputIndex"]),
2239
- objectSchema({
2240
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2241
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2242
- }, ["artifactId"])
2243
- ]
2244
- },
2245
- outputSchema: observationOutput,
2246
- requiredCapabilities: ["action.input", "artifact.read", "observation.read"],
2247
- mutating: true,
2248
- idempotency: "non_idempotent",
2249
- cancellation: "best_effort",
2250
- sensitivity: { input: ["user_file"], output: ["browser_data", "credential"] },
2251
- artifactKinds: []
2252
- }),
2253
- tool({
2254
- name: "browser.tabs.list",
2255
- title: "List browser tabs",
2256
- description: "List ManagedTabSet targets and all eligible user tabs in the connected browser.",
2257
- context: "workspace",
2258
- inputSchema: objectSchema({
2259
- scope: stringSchema({ enum: ["all", "managed_only", "user_tabs"] })
2260
- }),
2261
- outputSchema: resultSchema("workspace", {
2262
- targets: arraySchema(objectSchema({
2263
- targetId: opaqueIdSchema,
2264
- profileContextId: opaqueIdSchema,
2265
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2266
- url: boundedUrl,
2267
- active: booleanSchema(),
2268
- selected: booleanSchema(),
2269
- origin: stringSchema({ enum: ["managed", "managed_popup", "user_tab"] }),
2270
- managedTabSetId: opaqueIdSchema,
2271
- controlState: stringSchema({ enum: ["available", "controlled", "busy"] })
2272
- }, ["targetId", "title", "url", "origin", "controlState"]))
2273
- }, ["targets"]),
2274
- requiredCapabilities: ["browser.control"],
2275
- mutating: false,
2276
- idempotency: "read_only",
2277
- cancellation: "not_applicable",
2278
- sensitivity: { input: [], output: ["browser_data"] },
2279
- artifactKinds: []
2280
- }),
2281
- tool({
2282
- name: "browser.tabs.switch",
2283
- title: "Switch tab",
2284
- description: "Select a managed or user tab as the controlled target for the current Lease.",
2285
- context: "workspace",
2286
- inputSchema: objectSchema({ targetId: opaqueIdSchema }, ["targetId"]),
2287
- outputSchema: resultSchema("target"),
2288
- requiredCapabilities: ["browser.control"],
2289
- mutating: true,
2290
- idempotency: "idempotent",
2291
- cancellation: "before_dispatch",
2292
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2293
- artifactKinds: []
2294
- }),
2295
- tool({
2296
- name: "browser.tabs.close",
2297
- title: "Close tab",
2298
- description: "Close one explicitly addressed managed or user tab.",
2299
- context: "target",
2300
- inputSchema: emptyInput,
2301
- outputSchema: resultSchema("workspace", { closedTargetId: opaqueIdSchema }, ["closedTargetId"]),
2302
- requiredCapabilities: ["browser.control"],
2303
- mutating: true,
2304
- idempotency: "idempotent",
2305
- cancellation: "before_dispatch",
2306
- sensitivity: { input: [], output: ["browser_data"] },
2307
- artifactKinds: []
2308
- }),
2309
- tool({
2310
- name: "browser.tabs.release",
2311
- title: "Release tab control",
2312
- description: "Relinquish this Lease control of one tab without closing it.",
2313
- context: "target",
2314
- inputSchema: emptyInput,
2315
- outputSchema: resultSchema("target", { released: booleanSchema() }, ["released"]),
2316
- requiredCapabilities: ["browser.control"],
2317
- mutating: true,
2318
- idempotency: "idempotent",
2319
- cancellation: "before_dispatch",
2320
- sensitivity: { input: [], output: ["browser_data"] },
2321
- artifactKinds: []
2322
- }),
2323
- tool({
2324
- name: "browser.frames.list",
2325
- title: "List frames",
2326
- description: "List frames and CDP sessions belonging to the controlled target.",
2327
- context: "target",
2328
- inputSchema: emptyInput,
2329
- outputSchema: resultSchema("target", {
2330
- frames: arraySchema(objectSchema({
2331
- frameId: opaqueIdSchema,
2332
- parentFrameId: opaqueIdSchema,
2333
- url: boundedUrl,
2334
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
2335
- }, ["frameId", "url", "name"]))
2336
- }, ["frames"]),
2337
- requiredCapabilities: ["observation.read"],
2338
- mutating: false,
2339
- idempotency: "read_only",
2340
- cancellation: "not_applicable",
2341
- sensitivity: { input: [], output: ["browser_data"] },
2342
- artifactKinds: []
2343
- }),
2344
- tool({
2345
- name: "browser.frames.switch",
2346
- title: "Switch frame",
2347
- description: "Set the active frame for the current Lease after target-access validation.",
2348
- context: "target",
2349
- inputSchema: {
2350
- oneOf: [
2351
- objectSchema({ frameId: opaqueIdSchema }, ["frameId"]),
2352
- objectSchema({ top: booleanSchema({ const: true }) }, ["top"])
2353
- ]
2354
- },
2355
- outputSchema: resultSchema("target", { frameId: opaqueIdSchema }, ["frameId"]),
2356
- requiredCapabilities: ["browser.control"],
2357
- mutating: true,
2358
- idempotency: "idempotent",
2359
- cancellation: "before_dispatch",
2360
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2361
- artifactKinds: []
2362
- }),
2363
- tool({
2364
- name: "browser.dialogs.list",
2365
- title: "List dialogs",
2366
- description: "List pending dialogs for controlled targets without auto-accepting them.",
2367
- context: "workspace",
2368
- inputSchema: emptyInput,
2369
- outputSchema: resultSchema("workspace", {
2370
- dialogs: arraySchema(objectSchema({
2371
- dialogId: opaqueIdSchema,
2372
- targetId: opaqueIdSchema,
2373
- type: stringSchema({ enum: ["alert", "confirm", "prompt", "beforeunload"] }),
2374
- message: sensitive(stringSchema({ maxLength: 65536 }), "browser_data")
2375
- }, ["dialogId", "targetId", "type", "message"]))
2376
- }, ["dialogs"]),
2377
- requiredCapabilities: ["event.read"],
2378
- mutating: false,
2379
- idempotency: "read_only",
2380
- cancellation: "not_applicable",
2381
- sensitivity: { input: [], output: ["browser_data"] },
2382
- artifactKinds: []
2383
- }),
2384
- tool({
2385
- name: "browser.dialogs.respond",
2386
- title: "Respond to dialog",
2387
- description: "Explicitly accept or dismiss a pending dialog on a controlled target.",
2388
- context: "target",
2389
- inputSchema: objectSchema({
2390
- dialogId: opaqueIdSchema,
2391
- action: stringSchema({ enum: ["accept", "dismiss"] }),
2392
- promptText: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential")
2393
- }, ["dialogId", "action"]),
2394
- outputSchema: resultSchema("target", { dialogId: opaqueIdSchema, action: stringSchema({ enum: ["accept", "dismiss"] }) }, ["dialogId", "action"]),
2395
- requiredCapabilities: ["action.input"],
2396
- mutating: true,
2397
- idempotency: "idempotent",
2398
- cancellation: "before_dispatch",
2399
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
2400
- artifactKinds: []
2401
- }),
2402
- tool({
2403
- name: "browser.auth.set",
2404
- title: "Set HTTP auth",
2405
- description: "Set Workspace-scoped HTTP authentication credentials.",
2406
- context: "workspace",
2407
- inputSchema: objectSchema({
2408
- username: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "credential"),
2409
- password: sensitive(stringSchema({ maxLength: 65536 }), "credential")
2410
- }, ["username", "password"]),
2411
- outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: true }) }, ["configured"]),
2412
- requiredCapabilities: ["auth.manage"],
2413
- mutating: true,
2414
- idempotency: "idempotent",
2415
- cancellation: "before_dispatch",
2416
- sensitivity: { input: ["credential"], output: [] },
2417
- artifactKinds: []
2418
- }),
2419
- tool({
2420
- name: "browser.auth.clear",
2421
- title: "Clear HTTP auth",
2422
- description: "Remove Workspace-scoped HTTP authentication credentials.",
2423
- context: "workspace",
2424
- inputSchema: emptyInput,
2425
- outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: false }) }, ["configured"]),
2426
- requiredCapabilities: ["auth.manage"],
2427
- mutating: true,
2428
- idempotency: "idempotent",
2429
- cancellation: "before_dispatch",
2430
- sensitivity: { input: [], output: [] },
2431
- artifactKinds: []
2432
- }),
2433
- tool({
2434
- name: "browser.cookies.list",
2435
- title: "List cookies",
2436
- description: "List cookies available to a controlled target with sensitivity metadata.",
2437
- context: "target",
2438
- inputSchema: objectSchema({
2439
- domain: sensitive(stringSchema({ minLength: 1, maxLength: 2048 }), "browser_data")
2440
- }),
2441
- outputSchema: resultSchema("target", {
2442
- cookies: arraySchema(objectSchema({
2443
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2444
- value: sensitive(stringSchema({ maxLength: 1e6 }), "credential"),
2445
- domain: sensitive(stringSchema({ maxLength: 2048 }), "browser_data"),
2446
- path: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2447
- httpOnly: booleanSchema(),
2448
- secure: booleanSchema(),
2449
- expires: numberSchema()
2450
- }, ["name", "value", "domain", "path", "httpOnly", "secure", "expires"]))
2451
- }, ["cookies"]),
2452
- requiredCapabilities: ["cookies.read"],
2453
- mutating: false,
2454
- idempotency: "read_only",
2455
- cancellation: "best_effort",
2456
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2457
- artifactKinds: []
2458
- }),
2459
- tool({
2460
- name: "browser.network.requests",
2461
- title: "List network requests",
2462
- description: "List bounded Workspace-scoped network request metadata.",
2463
- context: "workspace",
2464
- inputSchema: objectSchema({
2465
- limit: integerSchema({ minimum: 1, maximum: 1e3 }),
2466
- after: integerSchema({ minimum: 0 }),
2467
- url: sensitive(stringSchema({ maxLength: 16384 }), "browser_data"),
2468
- method: stringSchema({ maxLength: 32 }),
2469
- status: stringSchema({ maxLength: 16 }),
2470
- type: arraySchema(stringSchema({ minLength: 1, maxLength: 128 }), { uniqueItems: true })
2471
- }),
2472
- outputSchema: resultSchema("workspace", {
2473
- requests: arraySchema(objectSchema({
2474
- requestId: opaqueIdSchema,
2475
- sequence: integerSchema({ minimum: 1 }),
2476
- method: stringSchema({ maxLength: 32 }),
2477
- url: boundedUrl,
2478
- status: integerSchema({ minimum: 100, maximum: 999 }),
2479
- type: stringSchema({ maxLength: 128 }),
2480
- size: integerSchema({ minimum: 0 }),
2481
- durationMs: numberSchema({ minimum: 0 }),
2482
- error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
2483
- }, ["requestId", "sequence", "method", "url", "type"])),
2484
- nextCursor: integerSchema({ minimum: 0 }),
2485
- truncated: booleanSchema()
2486
- }, ["requests", "nextCursor", "truncated"]),
2487
- requiredCapabilities: ["network.observe"],
2488
- mutating: false,
2489
- idempotency: "read_only",
2490
- cancellation: "best_effort",
2491
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2492
- artifactKinds: []
2493
- }),
2494
- tool({
2495
- name: "browser.network.request",
2496
- title: "Read network request",
2497
- description: "Read bounded details for one Workspace-scoped request.",
2498
- context: "workspace",
2499
- inputSchema: objectSchema({ requestId: opaqueIdSchema, includeBody: booleanSchema() }, ["requestId"]),
2500
- outputSchema: resultSchema("workspace", {
2501
- request: networkRequestDetailSchema,
2502
- body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential"),
2503
- bodyEncoding: stringSchema({ enum: ["utf8", "base64"] }),
2504
- mimeType: stringSchema({ maxLength: 256 }),
2505
- bodyTruncated: booleanSchema()
2506
- }, ["request", "bodyTruncated"]),
2507
- requiredCapabilities: ["network.observe"],
2508
- mutating: false,
2509
- idempotency: "read_only",
2510
- cancellation: "best_effort",
2511
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2512
- artifactKinds: []
2513
- }),
2514
- tool({
2515
- name: "browser.network.clear",
2516
- title: "Clear network journal",
2517
- description: "Clear captured request metadata for the current Workspace.",
2518
- context: "workspace",
2519
- inputSchema: emptyInput,
2520
- outputSchema: resultSchema("workspace", { cleared: booleanSchema({ const: true }) }, ["cleared"]),
2521
- requiredCapabilities: ["network.observe"],
2522
- mutating: true,
2523
- idempotency: "idempotent",
2524
- cancellation: "before_dispatch",
2525
- sensitivity: { input: [], output: [] },
2526
- artifactKinds: []
2527
- }),
2528
- tool({
2529
- name: "browser.network.rules.list",
2530
- title: "List network rules",
2531
- description: "List interception rules owned by the current Workspace.",
2532
- context: "workspace",
2533
- inputSchema: emptyInput,
2534
- outputSchema: resultSchema("workspace", {
2535
- rules: arraySchema(networkRuleSchema)
2536
- }, ["rules"]),
2537
- requiredCapabilities: ["network.observe"],
2538
- mutating: false,
2539
- idempotency: "read_only",
2540
- cancellation: "not_applicable",
2541
- sensitivity: { input: [], output: ["browser_data", "credential"] },
2542
- artifactKinds: []
2543
- }),
2544
- tool({
2545
- name: "browser.network.rules.add",
2546
- title: "Add network rule",
2547
- description: "Add a Workspace-scoped block, mock, or header rule.",
2548
- context: "workspace",
2549
- inputSchema: {
2550
- oneOf: [
2551
- objectSchema({ type: stringSchema({ const: "block" }), pattern: boundedUrl }, ["type", "pattern"]),
2552
- objectSchema({
2553
- type: stringSchema({ const: "mock" }),
2554
- pattern: boundedUrl,
2555
- status: integerSchema({ minimum: 100, maximum: 999 }),
2556
- headers: arraySchema(headerSchema, { maxItems: 256 }),
2557
- body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential")
2558
- }, ["type", "pattern"]),
2559
- objectSchema({
2560
- type: stringSchema({ const: "headers" }),
2561
- pattern: boundedUrl,
2562
- headers: arraySchema(headerSchema, { minItems: 1, maxItems: 256 })
2563
- }, ["type", "pattern", "headers"])
2564
- ]
2565
- },
2566
- outputSchema: resultSchema("workspace", { ruleId: opaqueIdSchema }, ["ruleId"]),
2567
- requiredCapabilities: ["network.modify"],
2568
- mutating: true,
2569
- idempotency: "non_idempotent",
2570
- cancellation: "before_dispatch",
2571
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
2572
- artifactKinds: []
2573
- }),
2574
- tool({
2575
- name: "browser.network.rules.remove",
2576
- title: "Remove network rule",
2577
- description: "Remove one owned interception rule or all rules in the Workspace.",
2578
- context: "workspace",
2579
- inputSchema: {
2580
- oneOf: [
2581
- objectSchema({ ruleId: opaqueIdSchema }, ["ruleId"]),
2582
- objectSchema({ all: booleanSchema({ const: true }) }, ["all"])
2583
- ]
2584
- },
2585
- outputSchema: resultSchema("workspace", { removed: integerSchema({ minimum: 0 }) }, ["removed"]),
2586
- requiredCapabilities: ["network.modify"],
2587
- mutating: true,
2588
- idempotency: "idempotent",
2589
- cancellation: "before_dispatch",
2590
- sensitivity: { input: ["browser_data"], output: [] },
2591
- artifactKinds: []
2592
- }),
2593
- tool({
2594
- name: "browser.eval",
2595
- title: "Evaluate JavaScript",
2596
- description: "Evaluate JavaScript in a controlled target when the Agent host exposes developer capability.",
2597
- context: "target",
2598
- inputSchema: objectSchema({ expression: boundedText, awaitPromise: booleanSchema() }, ["expression"]),
2599
- outputSchema: resultSchema("target", {
2600
- value: sensitive({}, "browser_data", "credential"),
2601
- truncated: booleanSchema()
2602
- }, ["value", "truncated"]),
2603
- requiredCapabilities: ["developer.eval"],
2604
- mutating: true,
2605
- idempotency: "non_idempotent",
2606
- cancellation: "best_effort",
2607
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2608
- artifactKinds: []
2609
- })
2610
- ];
2611
- var toolsByName = new Map(TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
2612
- function schemaSensitivities(schema, result = /* @__PURE__ */ new Set()) {
2613
- for (const value of schema["x-browser-pilot-sensitivity"] ?? []) result.add(value);
2614
- for (const property of Object.values(schema.properties ?? {})) schemaSensitivities(property, result);
2615
- for (const alternative of schema.oneOf ?? []) schemaSensitivities(alternative, result);
2616
- if (schema.items) schemaSensitivities(schema.items, result);
2617
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
2618
- schemaSensitivities(schema.additionalProperties, result);
2619
- }
2620
- return result;
2621
- }
2622
- function assertToolManifest(definitions = TOOL_DEFINITIONS) {
2623
- const names = /* @__PURE__ */ new Set();
2624
- const knownCapabilities = new Set(CAPABILITIES);
2625
- for (const definition of definitions) {
2626
- if (!/^[a-z][a-z0-9]*(?:[._][a-z][a-z0-9]*)+$/.test(definition.name)) {
2627
- throw new Error(`Invalid tool name: ${definition.name}`);
2628
- }
2629
- if (names.has(definition.name)) throw new Error(`Duplicate tool name: ${definition.name}`);
2630
- names.add(definition.name);
2631
- for (const capability of definition.requiredCapabilities) {
2632
- if (!knownCapabilities.has(capability)) {
2633
- throw new Error(`Tool ${definition.name} references unknown capability ${capability}`);
2634
- }
2635
- }
2636
- if (!definition.mutating && definition.idempotency === "non_idempotent") {
2637
- throw new Error(`Read-only tool ${definition.name} cannot be non-idempotent`);
2638
- }
2639
- if (definition.mutating && definition.cancellation === "not_applicable") {
2640
- throw new Error(`Mutating tool ${definition.name} must declare cancellation semantics`);
2641
- }
2642
- for (const direction of ["input", "output"]) {
2643
- const sensitivity = definition.sensitivity[direction];
2644
- if (new Set(sensitivity).size !== sensitivity.length) {
2645
- throw new Error(`Tool ${definition.name} has duplicate ${direction} sensitivity`);
2646
- }
2647
- for (const value of sensitivity) {
2648
- if (!SENSITIVITIES.includes(value)) {
2649
- throw new Error(`Tool ${definition.name} references unknown sensitivity ${value}`);
2650
- }
2651
- }
2652
- for (const value of schemaSensitivities(
2653
- direction === "input" ? definition.inputSchema : definition.outputSchema
2654
- )) {
2655
- if (!SENSITIVITIES.includes(value)) {
2656
- throw new Error(
2657
- `Tool ${definition.name} ${direction} schema contains unknown sensitivity ${value}`
2658
- );
2659
- }
2660
- if (!sensitivity.includes(value)) {
2661
- throw new Error(
2662
- `Tool ${definition.name} ${direction} schema marks ${value} without declaring it`
2663
- );
2664
- }
2665
- }
2666
- }
2667
- assertSchemaDefinition(definition.inputSchema, `${definition.name}.inputSchema`);
2668
- assertSchemaDefinition(definition.outputSchema, `${definition.name}.outputSchema`);
2669
- }
2670
- }
2671
- assertToolManifest();
2672
-
2673
- // src/services/command-runtime.ts
2674
- import { randomUUID as randomUUID2 } from "crypto";
2675
-
2676
- // src/services/event-journal.ts
2677
- import { randomUUID as randomUUID3 } from "crypto";
2678
-
2679
- // src/services/broker-runtime.ts
2680
- var DEFAULT_PROTOCOL_LIMITS = {
2681
- maxMessageBytes: 1024 * 1024,
2682
- maxResultBytes: 4 * 1024 * 1024,
2683
- maxArtifactBytes: 100 * 1024 * 1024,
2684
- eventJournalSize: 1e3
2685
- };
2686
-
2687
- // src/bridge/stdio-bridge.ts
2688
- function isIncomingCall(message) {
2689
- return "method" in message;
2690
- }
2691
- function isOutOfBandControl(message) {
2692
- if (message.method === "commands/get" || message.method === "commands/cancel") return true;
2693
- if (message.method !== "tools/call" || !message.params || typeof message.params !== "object") return false;
2694
- if (Array.isArray(message.params)) return false;
2695
- const name = message.params.name;
2696
- return name === "browser.dialogs.list" || name === "browser.dialogs.respond";
2697
- }
2698
- function errorResponse(id, error) {
2699
- const stable = asBrowserPilotError(error);
2700
- return { jsonrpc: "2.0", id, error: stable.toJsonRpcError() };
2701
- }
2702
- async function writeLine(output, value, maxResultBytes) {
2703
- let serialized = JSON.stringify(value);
2704
- if (Buffer.byteLength(serialized, "utf8") > maxResultBytes) {
2705
- if (!("id" in value)) return;
2706
- serialized = JSON.stringify(errorResponse(value.id, new BrowserPilotError(
2707
- "result_too_large",
2708
- `Protocol result exceeds ${maxResultBytes} bytes`,
2709
- { context: { maxResultBytes } }
2710
- )));
2711
- }
2712
- if (output.destroyed || !output.writable) throw new Error("Protocol output is closed");
2713
- if (!output.write(`${serialized}
2714
- `)) await once(output, "drain");
2715
- }
2716
- function negotiatedTransportLimits(value, ceilings) {
2717
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2718
- const limits = value.limits;
2719
- if (!limits || typeof limits !== "object" || Array.isArray(limits)) return void 0;
2720
- if (!Number.isSafeInteger(limits.maxMessageBytes) || !Number.isSafeInteger(limits.maxResultBytes) || limits.maxMessageBytes <= 0 || limits.maxResultBytes <= 0) return void 0;
2721
- return {
2722
- maxMessageBytes: Math.min(limits.maxMessageBytes, ceilings.maxMessageBytes),
2723
- maxResultBytes: Math.min(limits.maxResultBytes, ceilings.maxResultBytes)
2724
- };
2725
- }
2726
- function decodeLine(bytes) {
2727
- try {
2728
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2729
- } catch (cause) {
2730
- throw new BrowserPilotError("invalid_argument", "Protocol message is not valid UTF-8", {
2731
- rpcCode: -32700,
2732
- cause
2733
- });
2734
- }
2735
- }
2736
- async function runStdioBridge(options) {
2737
- const bridgeSessionId = options.bridgeSessionId ?? `bridge:${randomUUID5()}`;
2738
- const ceilings = {
2739
- maxMessageBytes: options.maxMessageBytes ?? DEFAULT_PROTOCOL_LIMITS.maxMessageBytes,
2740
- maxResultBytes: options.maxResultBytes ?? DEFAULT_PROTOCOL_LIMITS.maxResultBytes
2741
- };
2742
- let maxMessageBytes = ceilings.maxMessageBytes;
2743
- let maxResultBytes = ceilings.maxResultBytes;
2744
- const maxPendingCalls = options.maxPendingCalls ?? 256;
2745
- const maxOutOfBandCalls = options.maxOutOfBandCalls ?? 16;
2746
- if (!Number.isSafeInteger(maxPendingCalls) || maxPendingCalls <= 0 || !Number.isSafeInteger(maxOutOfBandCalls) || maxOutOfBandCalls <= 0) throw new Error("Bridge in-flight call limits must be positive integers");
2747
- let pending = Buffer.alloc(0);
2748
- let reason = "eof";
2749
- let exitCode = 0;
2750
- let stopped = false;
2751
- let framingFailed = false;
2752
- let outputTail = Promise.resolve();
2753
- let normalTail = Promise.resolve();
2754
- let initialization;
2755
- let notificationController;
2756
- let notificationTask;
2757
- const inFlight = /* @__PURE__ */ new Set();
2758
- let pendingCalls = 0;
2759
- let outOfBandCalls = 0;
2760
- const queueWrite = (value) => {
2761
- const write = outputTail.then(() => writeLine(options.output, value, maxResultBytes));
2762
- outputTail = write.catch(() => {
2763
- });
2764
- return write;
2765
- };
2766
- const failFraming = async (error) => {
2767
- exitCode = 1;
2768
- reason = "protocol_error";
2769
- stopped = true;
2770
- framingFailed = true;
2771
- await queueWrite(errorResponse(null, error));
2772
- };
2773
- const track = (task, outOfBand) => {
2774
- inFlight.add(task);
2775
- if (outOfBand) outOfBandCalls += 1;
2776
- else pendingCalls += 1;
2777
- const finished = () => {
2778
- inFlight.delete(task);
2779
- if (outOfBand) outOfBandCalls -= 1;
2780
- else pendingCalls -= 1;
2781
- };
2782
- void task.then(finished, finished);
2783
- };
2784
- const startNotifications = () => {
2785
- if (notificationTask || !options.backend.notifications) return;
2786
- notificationController = new AbortController();
2787
- notificationTask = (async () => {
2788
- for await (const notification of options.backend.notifications(
2789
- bridgeSessionId,
2790
- notificationController.signal
2791
- )) {
2792
- if (notificationController.signal.aborted || stopped) break;
2793
- await queueWrite(notification);
2794
- }
2795
- })().catch(() => {
2796
- if (notificationController?.signal.aborted) return;
2797
- stopped = true;
2798
- reason = "output_closed";
2799
- exitCode = 1;
2800
- options.input.destroy();
2801
- });
2802
- };
2803
- const processLine = async (rawLine) => {
2804
- const line = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
2805
- let message;
2806
- try {
2807
- message = parseJsonRpcMessage(decodeLine(line), maxMessageBytes);
2808
- } catch (error) {
2809
- await failFraming(error);
2810
- return;
1037
+ while (Date.now() < deadline && processIsAlive(pid)) {
1038
+ const client = new DaemonClient();
1039
+ const info = await client.healthInfo();
1040
+ if (info.ok) {
1041
+ assertBrowserSelection(browserFilter, info);
1042
+ return client;
2811
1043
  }
2812
- if (!isIncomingCall(message)) {
2813
- await failFraming(invalidArgument("Bridge accepts JSON-RPC requests and notifications only", void 0, -32600));
2814
- return;
1044
+ await new Promise((resolve) => setTimeout(resolve, 200));
1045
+ }
1046
+ return void 0;
1047
+ }
1048
+ async function connectDaemon(browserFilter) {
1049
+ const existing = new DaemonClient();
1050
+ const existingInfo = await existing.healthInfo();
1051
+ if (existingInfo.ok) {
1052
+ assertBrowserSelection(browserFilter, existingInfo);
1053
+ return existing;
1054
+ }
1055
+ const startupLock = await acquireBrokerStartupLock();
1056
+ try {
1057
+ const winner = new DaemonClient();
1058
+ const winnerInfo = await winner.healthInfo();
1059
+ if (winnerInfo.ok) {
1060
+ assertBrowserSelection(browserFilter, winnerInfo);
1061
+ return winner;
2815
1062
  }
2816
- const requestId = "id" in message ? message.id : void 0;
2817
- const outOfBand = isOutOfBandControl(message);
2818
- const saturated = outOfBand ? outOfBandCalls >= maxOutOfBandCalls : pendingCalls >= maxPendingCalls;
2819
- if (saturated) {
2820
- if (requestId !== void 0) {
2821
- await queueWrite(errorResponse(requestId, new BrowserPilotError(
2822
- "result_too_large",
2823
- `Bridge ${outOfBand ? "control" : "request"} queue is full`,
2824
- {
2825
- retryable: true,
2826
- context: {
2827
- limit: outOfBand ? maxOutOfBandCalls : maxPendingCalls,
2828
- queue: outOfBand ? "control" : "request"
2829
- }
1063
+ const locator = readBrokerLocatorSync();
1064
+ const starting = readBrokerStartingSync();
1065
+ const brokerPid = locator?.pid ?? starting?.pid ?? readBrokerPidSync();
1066
+ if (brokerPid && processIsAlive(brokerPid)) {
1067
+ if (starting?.pid === brokerPid) {
1068
+ const started = await waitForStartingDaemon(brokerPid, browserFilter);
1069
+ if (started) return started;
1070
+ throw new BrowserPilotError("browser_disconnected", "Browser Pilot Broker did not finish starting", {
1071
+ retryable: true,
1072
+ remediation: {
1073
+ code: "inspect_broker_startup",
1074
+ message: "Inspect the Broker startup state; browser authorization is never requested during Broker startup.",
1075
+ actionRequired: false
2830
1076
  }
2831
- )));
1077
+ });
2832
1078
  }
2833
- return;
2834
- }
2835
- const dispatch = async () => {
2836
- try {
2837
- const result = await options.backend.call(bridgeSessionId, message.method, message.params);
2838
- if (requestId !== void 0 && reason !== "protocol_error") {
2839
- await queueWrite({ jsonrpc: "2.0", id: requestId, result });
2840
- }
2841
- if (message.method === "initialize" && reason !== "protocol_error") {
2842
- const negotiated = negotiatedTransportLimits(result, ceilings);
2843
- if (negotiated) {
2844
- maxMessageBytes = negotiated.maxMessageBytes;
2845
- maxResultBytes = negotiated.maxResultBytes;
2846
- }
2847
- startNotifications();
2848
- }
2849
- } catch (error) {
2850
- if (requestId !== void 0 && reason !== "protocol_error") {
2851
- try {
2852
- await queueWrite(errorResponse(requestId, error));
2853
- } catch {
2854
- stopped = true;
2855
- reason = "output_closed";
2856
- exitCode = 1;
2857
- }
1079
+ throw new BrowserPilotError("browser_disconnected", "The Browser Pilot Broker process is alive but its endpoint is unresponsive", {
1080
+ retryable: true,
1081
+ remediation: {
1082
+ code: "restart_unresponsive_broker",
1083
+ message: "Stop the unresponsive Browser Pilot Broker explicitly, then retry. Live processes are never replaced automatically.",
1084
+ actionRequired: true
2858
1085
  }
2859
- }
2860
- };
2861
- const task = outOfBand ? (initialization ?? Promise.resolve()).then(dispatch) : normalTail.then(dispatch);
2862
- if (!outOfBand) normalTail = task.catch(() => {
2863
- });
2864
- if (message.method === "initialize") initialization = task.catch(() => {
2865
- });
2866
- track(task, outOfBand);
2867
- if (message.method === "shutdown") {
2868
- stopped = true;
2869
- reason = "shutdown";
1086
+ });
2870
1087
  }
2871
- };
2872
- try {
2873
- for await (const value of options.input) {
2874
- const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
2875
- let offset = 0;
2876
- while (!stopped && offset < chunk.length) {
2877
- const newline = chunk.indexOf(10, offset);
2878
- if (newline === -1) {
2879
- const remaining = chunk.subarray(offset);
2880
- if (pending.length + remaining.length > maxMessageBytes) {
2881
- await failFraming(new BrowserPilotError(
2882
- "result_too_large",
2883
- `Protocol message exceeds ${maxMessageBytes} bytes`,
2884
- { context: { maxMessageBytes }, rpcCode: -32600 }
2885
- ));
2886
- break;
1088
+ removeStaleBrokerFilesSync();
1089
+ const candidates = await discoverBrowserCandidates();
1090
+ const filter = browserFilter?.toLowerCase();
1091
+ const matches = candidates.filter(({ candidate }) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
1092
+ if (browserFilter && matches.length === 0) {
1093
+ throw new BrowserPilotError(
1094
+ "browser_not_found",
1095
+ "No installed supported browser matches the requested selection.",
1096
+ {
1097
+ remediation: {
1098
+ code: "select_supported_browser",
1099
+ message: "Run browser discovery and select one of the returned browser IDs or product names.",
1100
+ actionRequired: true
2887
1101
  }
2888
- pending = pending.length === 0 ? Buffer.from(remaining) : Buffer.concat([pending, remaining]);
2889
- offset = chunk.length;
2890
- continue;
2891
- }
2892
- const part = chunk.subarray(offset, newline);
2893
- if (pending.length + part.length > maxMessageBytes) {
2894
- await failFraming(new BrowserPilotError(
2895
- "result_too_large",
2896
- `Protocol message exceeds ${maxMessageBytes} bytes`,
2897
- { context: { maxMessageBytes }, rpcCode: -32600 }
2898
- ));
2899
- break;
2900
1102
  }
2901
- const line = pending.length === 0 ? part : Buffer.concat([pending, part]);
2902
- pending = Buffer.alloc(0);
2903
- offset = newline + 1;
2904
- await processLine(line);
2905
- if (initialization) await initialization;
2906
- }
2907
- if (stopped) break;
2908
- }
2909
- if (!stopped && pending.length > 0) await processLine(pending);
2910
- notificationController?.abort();
2911
- await notificationTask;
2912
- if (!framingFailed) await Promise.allSettled([...inFlight]);
2913
- await outputTail;
2914
- } catch {
2915
- if (!stopped) {
2916
- reason = "output_closed";
2917
- exitCode = 1;
1103
+ );
2918
1104
  }
1105
+ const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
1106
+ return await startDaemon(selected);
2919
1107
  } finally {
2920
- notificationController?.abort();
2921
- await notificationTask;
2922
- try {
2923
- await options.backend.disconnect(bridgeSessionId);
2924
- } catch {
2925
- }
1108
+ startupLock.release();
2926
1109
  }
2927
- return { exitCode, reason };
2928
1110
  }
2929
1111
 
1112
+ // src/protocol/model.ts
1113
+ var CAPABILITIES = [
1114
+ "browser.discovery",
1115
+ "browser.control",
1116
+ "workspace.manage",
1117
+ "observation.read",
1118
+ "action.input",
1119
+ "artifact.read",
1120
+ "event.read",
1121
+ "network.observe",
1122
+ "network.modify",
1123
+ "auth.manage",
1124
+ "cookies.read",
1125
+ "developer.eval"
1126
+ ];
1127
+ var OBSERVATION_V1_LIMITS = {
1128
+ defaultElements: 50,
1129
+ maxElements: 1e4,
1130
+ maxTitleCharacters: 4096,
1131
+ maxUrlCharacters: 16384,
1132
+ maxElementNameCharacters: 4096,
1133
+ maxElementValueCharacters: 65536,
1134
+ maxTextCharacters: 1e6,
1135
+ maxTreeDepth: 128,
1136
+ maxSerializedBytes: 2 * 1024 * 1024,
1137
+ ttlMs: 5 * 6e4,
1138
+ maxStoredObservations: 2048
1139
+ };
1140
+
2930
1141
  // src/compatibility-broker-client.ts
2931
- import { createHash as createHash4, randomUUID as randomUUID6 } from "crypto";
2932
- var BRIDGE_SESSION_ID = "bridge:browser-pilot-cli";
2933
1142
  var CLIENT_KEY = "browser-pilot-cli";
2934
1143
  var LEASE_TTL_MS = 5 * 6e4;
2935
- function compatibilityIdentity(clientKey) {
2936
- if (clientKey === CLIENT_KEY) {
2937
- return { bridgeSessionId: BRIDGE_SESSION_ID, instanceId: "local:one-shot" };
2938
- }
2939
- const digest = createHash4("sha256").update(clientKey).digest("base64url").slice(0, 24);
1144
+ function compatibilityIdentity(clientKey, executableVersion) {
1145
+ const principalDigest = createHash4("sha256").update(clientKey).digest("base64url").slice(0, 24);
1146
+ const sessionDigest = createHash4("sha256").update(clientKey).update("\0").update(executableVersion).digest("base64url").slice(0, 24);
2940
1147
  return {
2941
- bridgeSessionId: `bridge:browser-pilot-cli:${digest}`,
2942
- instanceId: `local:one-shot:${digest}`
1148
+ clientSessionId: `client:browser-pilot-cli:${sessionDigest}`,
1149
+ instanceId: `local:browser-pilot-cli:${principalDigest}`
2943
1150
  };
2944
1151
  }
2945
1152
  function isSelected(target) {
@@ -2960,17 +1167,18 @@ function commandResult(value, method) {
2960
1167
  return asRecord(outcome.result, `${method} result`);
2961
1168
  }
2962
1169
  var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
2963
- constructor(transport, bridgeSessionId, initialized, workspace, lease) {
1170
+ constructor(transport, clientSessionId, initialized, workspace, lease, invocation) {
2964
1171
  this.transport = transport;
2965
- this.bridgeSessionId = bridgeSessionId;
1172
+ this.clientSessionId = clientSessionId;
2966
1173
  this.initialized = initialized;
2967
1174
  this.workspace = workspace;
2968
1175
  this.lease = lease;
1176
+ this.invocation = invocation;
2969
1177
  }
2970
1178
  commandSequence = 0;
2971
- static async create(transport, executableVersion, clientKey = CLIENT_KEY) {
2972
- const identity = compatibilityIdentity(clientKey);
2973
- const initialized = asRecord(await transport.brokerCall(identity.bridgeSessionId, "initialize", {
1179
+ static async create(transport, executableVersion, clientKey = CLIENT_KEY, invocation = {}) {
1180
+ const identity = compatibilityIdentity(clientKey, executableVersion);
1181
+ const initialized = asRecord(await transport.brokerCall(identity.clientSessionId, "initialize", {
2974
1182
  client: {
2975
1183
  id: "org.browser-pilot.cli",
2976
1184
  name: "Browser Pilot CLI",
@@ -2978,45 +1186,74 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
2978
1186
  instanceId: identity.instanceId
2979
1187
  },
2980
1188
  protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 3 } },
2981
- requestedCapabilities: [...CAPABILITIES],
2982
- launchMode: "one-shot"
1189
+ requestedCapabilities: [...CAPABILITIES]
2983
1190
  }), "initialize");
2984
- if (initialized.executableVersion !== executableVersion) {
2985
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from another executable version", {
2986
- remediation: {
2987
- code: "use_matching_executable_or_isolate",
2988
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
2989
- actionRequired: true
2990
- }
2991
- });
2992
- }
2993
- const created = asRecord(await transport.brokerCall(identity.bridgeSessionId, "workspaces/create", {
1191
+ const created = asRecord(await transport.brokerCall(identity.clientSessionId, "workspaces/create", {
2994
1192
  clientKey
2995
1193
  }), "workspaces/create");
2996
- const leased = asRecord(await transport.brokerCall(identity.bridgeSessionId, "leases/create", {
1194
+ const leased = asRecord(await transport.brokerCall(identity.clientSessionId, "leases/create", {
2997
1195
  workspaceId: created.workspace.id,
2998
1196
  clientKey,
2999
1197
  ttlMs: LEASE_TTL_MS
3000
1198
  }), "leases/create");
3001
1199
  return new _CompatibilityBrokerClient(
3002
1200
  transport,
3003
- identity.bridgeSessionId,
1201
+ identity.clientSessionId,
3004
1202
  initialized,
3005
1203
  created.workspace,
3006
- leased.lease
1204
+ leased.lease,
1205
+ invocation
3007
1206
  );
3008
1207
  }
3009
1208
  async callTool(name, args = {}, targetId) {
3010
1209
  this.commandSequence += 1;
3011
- return commandResult(await this.transport.brokerCall(this.bridgeSessionId, "tools/call", {
1210
+ if (this.invocation.signal?.aborted) {
1211
+ throw new BrowserPilotError("command_cancelled", "Command was cancelled before dispatch");
1212
+ }
1213
+ const commandId = this.commandId(this.commandSequence);
1214
+ const request = this.transport.brokerCall(this.clientSessionId, "tools/call", {
3012
1215
  name,
3013
1216
  arguments: args,
3014
1217
  workspaceId: this.workspace.id,
3015
1218
  leaseId: this.lease.id,
3016
1219
  ...targetId ? { targetId } : {},
3017
- commandId: `command:cli-${process.pid}-${this.commandSequence}-${randomUUID6()}`,
3018
- deadlineMs: 6e4
3019
- }), name);
1220
+ commandId,
1221
+ ...this.invocation.requestId ? { idempotencyKey: `cli-request:${this.invocation.requestId}:${this.commandSequence}` } : {},
1222
+ deadlineMs: this.invocation.deadlineMs ?? 6e4
1223
+ });
1224
+ const cancel = () => {
1225
+ void this.transport.brokerCall(this.clientSessionId, "commands/cancel", {
1226
+ commandId,
1227
+ workspaceId: this.workspace.id
1228
+ }).catch(() => {
1229
+ });
1230
+ };
1231
+ this.invocation.signal?.addEventListener("abort", cancel, { once: true });
1232
+ try {
1233
+ return commandResult(await request, name);
1234
+ } finally {
1235
+ this.invocation.signal?.removeEventListener("abort", cancel);
1236
+ }
1237
+ }
1238
+ async listCommands(limit = 20, statuses) {
1239
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/list", {
1240
+ workspaceId: this.workspace.id,
1241
+ limit,
1242
+ ...statuses ? { statuses: [...statuses] } : {}
1243
+ }), "commands/list");
1244
+ return result.commands;
1245
+ }
1246
+ async getCommand(commandId) {
1247
+ return asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/get", {
1248
+ workspaceId: this.workspace.id,
1249
+ commandId
1250
+ }), "commands/get");
1251
+ }
1252
+ async cancelCommand(commandId) {
1253
+ return asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/cancel", {
1254
+ workspaceId: this.workspace.id,
1255
+ commandId
1256
+ }), "commands/cancel");
3020
1257
  }
3021
1258
  async connectBrowser(browserId) {
3022
1259
  const selected = browserId ? this.initialized.browsers.find((candidate) => candidate.id === browserId) : this.initialized.browsers.find((candidate) => candidate.state === "ready") ?? this.initialized.browsers[0];
@@ -3115,7 +1352,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3115
1352
  return result.observationId;
3116
1353
  }
3117
1354
  async importArtifact(path, mimeType) {
3118
- const result = asRecord(await this.transport.brokerCall(this.bridgeSessionId, "artifacts/import", {
1355
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "artifacts/import", {
3119
1356
  workspaceId: this.workspace.id,
3120
1357
  leaseId: this.lease.id,
3121
1358
  path,
@@ -3123,8 +1360,19 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3123
1360
  }), "artifacts/import");
3124
1361
  return asRecord(result.artifact, "imported Artifact");
3125
1362
  }
1363
+ async listArtifacts(kinds) {
1364
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "artifacts/list", {
1365
+ workspaceId: this.workspace.id,
1366
+ leaseId: this.lease.id,
1367
+ ...kinds ? { kinds: [...kinds] } : {}
1368
+ }), "artifacts/list");
1369
+ if (!Array.isArray(result.artifacts)) {
1370
+ throw new BrowserPilotError("internal_error", "artifacts/list returned invalid Artifacts");
1371
+ }
1372
+ return result.artifacts;
1373
+ }
3126
1374
  async exportArtifact(artifactId, path) {
3127
- await this.transport.brokerCall(this.bridgeSessionId, "artifacts/export", {
1375
+ await this.transport.brokerCall(this.clientSessionId, "artifacts/export", {
3128
1376
  workspaceId: this.workspace.id,
3129
1377
  leaseId: this.lease.id,
3130
1378
  artifactId,
@@ -3133,70 +1381,62 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3133
1381
  });
3134
1382
  }
3135
1383
  async releaseArtifact(artifactId) {
3136
- await this.transport.brokerCall(this.bridgeSessionId, "artifacts/release", {
1384
+ await this.transport.brokerCall(this.clientSessionId, "artifacts/release", {
3137
1385
  workspaceId: this.workspace.id,
3138
1386
  leaseId: this.lease.id,
3139
1387
  artifactId
3140
1388
  });
3141
1389
  }
3142
1390
  async releaseWorkspace() {
3143
- await this.transport.brokerCall(this.bridgeSessionId, "workspaces/release", {
1391
+ await this.transport.brokerCall(this.clientSessionId, "workspaces/release", {
3144
1392
  workspaceId: this.workspace.id
3145
1393
  });
3146
1394
  }
1395
+ commandId(sequence) {
1396
+ if (!this.invocation.requestId) {
1397
+ return `command:cli-${process.pid}-${sequence}-${randomUUID2()}`;
1398
+ }
1399
+ const digest = createHash4("sha256").update(this.workspace.clientKey ?? CLIENT_KEY).update("\0").update(this.invocation.requestId).update("\0").update(String(sequence)).digest("base64url").slice(0, 32);
1400
+ return `command:cli-${digest}-${sequence}`;
1401
+ }
3147
1402
  };
3148
1403
  async function validateDaemon(client) {
3149
1404
  const health = await client.healthInfo();
3150
1405
  if (!health.ok) throw new BrowserPilotError("browser_disconnected", "Browser Pilot daemon is unavailable");
3151
- if (health.brokerProtocol !== 1) {
3152
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from an older executable", {
3153
- remediation: {
3154
- code: "use_compatible_executable_or_isolate",
3155
- message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3156
- actionRequired: true
3157
- }
3158
- });
3159
- }
3160
- }
3161
- async function validateCompatibilityDaemon(client, executableVersion) {
3162
- await validateDaemon(client);
3163
- const health = await client.healthInfo();
3164
- const requester = createExecutableMetadataSync(
3165
- executableVersion,
3166
- publicExecutablePath(import.meta.url)
3167
- );
3168
- if (health.executableVersion !== void 0 && health.executableVersion !== requester.version || health.executableIdentity !== void 0 && health.executableIdentity !== requester.identity) {
3169
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from another executable installation", {
1406
+ if (health.brokerProtocol !== BROKER_RPC_VERSION) {
1407
+ throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot Broker uses an incompatible private transport", {
3170
1408
  context: {
3171
- brokerExecutableVersion: health.executableVersion,
3172
- requesterExecutableVersion: requester.version
1409
+ brokerRpcVersion: health.brokerProtocol,
1410
+ requiredBrokerRpcVersion: BROKER_RPC_VERSION,
1411
+ serviceVersion: health.serviceVersion,
1412
+ executableVersion: health.executableVersion
3173
1413
  },
3174
1414
  remediation: {
3175
- code: "use_matching_executable_or_isolate",
3176
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
1415
+ code: "stop_incompatible_broker_or_isolate",
1416
+ message: "Stop the running Broker with the Browser Pilot executable that started it, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3177
1417
  actionRequired: true
3178
1418
  }
3179
1419
  });
3180
1420
  }
3181
1421
  }
3182
- async function connectCompatibility(executableVersion, browserFilter, clientKey = CLIENT_KEY) {
1422
+ async function connectCompatibility(executableVersion, browserFilter, clientKey = CLIENT_KEY, invocation = {}) {
3183
1423
  const daemon = await connectDaemon(browserFilter);
3184
- await validateCompatibilityDaemon(daemon, executableVersion);
3185
- return CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
1424
+ await validateDaemon(daemon);
1425
+ return CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
3186
1426
  }
3187
- async function resumeCompatibility(executableVersion, clientKey = CLIENT_KEY) {
1427
+ async function resumeCompatibility(executableVersion, clientKey = CLIENT_KEY, invocation = {}) {
3188
1428
  if (!isDaemonRunning()) return null;
3189
1429
  const daemon = new DaemonClient();
3190
1430
  try {
3191
- await validateCompatibilityDaemon(daemon, executableVersion);
3192
- return await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
1431
+ await validateDaemon(daemon);
1432
+ return await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
3193
1433
  } catch (error) {
3194
1434
  if (error instanceof BrowserPilotError && error.code === "protocol_incompatible") throw error;
3195
1435
  return null;
3196
1436
  }
3197
1437
  }
3198
- async function withCompatibilityTarget(executableVersion, operation, clientKey = CLIENT_KEY) {
3199
- const client = await resumeCompatibility(executableVersion, clientKey);
1438
+ async function withCompatibilityTarget(executableVersion, operation, clientKey = CLIENT_KEY, invocation = {}) {
1439
+ const client = await resumeCompatibility(executableVersion, clientKey, invocation);
3200
1440
  if (!client) throw new Error("Not connected");
3201
1441
  const target = await client.ensureTarget();
3202
1442
  return operation(client, target);
@@ -3205,33 +1445,6 @@ async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY)
3205
1445
  if (!isDaemonRunning()) return;
3206
1446
  const daemon = new DaemonClient();
3207
1447
  await validateDaemon(daemon);
3208
- const health = await daemon.healthInfo();
3209
- if (!health.ok || !health.brokerProcessIdentity || !health.executableVersion || !health.executableIdentity) {
3210
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot Broker does not support protected shutdown", {
3211
- remediation: {
3212
- code: "use_matching_executable_or_isolate",
3213
- message: "Use the Browser Pilot installation that started the Broker, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3214
- actionRequired: true
3215
- }
3216
- });
3217
- }
3218
- const requester = createExecutableMetadataSync(
3219
- executableVersion,
3220
- publicExecutablePath(import.meta.url)
3221
- );
3222
- if (requester.version !== health.executableVersion || requester.identity !== health.executableIdentity) {
3223
- throw new BrowserPilotError("protocol_incompatible", "This Browser Pilot installation does not own the running Broker", {
3224
- context: {
3225
- brokerExecutableVersion: health.executableVersion,
3226
- requesterExecutableVersion: requester.version
3227
- },
3228
- remediation: {
3229
- code: "use_matching_executable_or_isolate",
3230
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3231
- actionRequired: true
3232
- }
3233
- });
3234
- }
3235
1448
  try {
3236
1449
  const client = await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
3237
1450
  await client.releaseWorkspace();
@@ -3239,11 +1452,17 @@ async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY)
3239
1452
  if (!(error instanceof BrowserPilotError) || error.code !== "browser_not_found") throw error;
3240
1453
  }
3241
1454
  const afterRelease = await daemon.healthInfo();
3242
- if ((afterRelease.clients?.embeddedConnections ?? 0) > 0 || (afterRelease.clients?.activeLeases ?? 0) > 0) {
1455
+ if ((afterRelease.clients?.activeLeases ?? 0) > 0) {
3243
1456
  return;
3244
1457
  }
1458
+ if (!afterRelease.ok || !afterRelease.brokerProcessIdentity || !afterRelease.executableVersion || !afterRelease.executableIdentity) return;
1459
+ const requester = createExecutableMetadataSync(
1460
+ executableVersion,
1461
+ publicExecutablePath(import.meta.url)
1462
+ );
1463
+ if (requester.version !== afterRelease.executableVersion || requester.identity !== afterRelease.executableIdentity) return;
3245
1464
  await daemon.shutdown({
3246
- brokerProcessIdentity: health.brokerProcessIdentity,
1465
+ brokerProcessIdentity: afterRelease.brokerProcessIdentity,
3247
1466
  executableVersion: requester.version,
3248
1467
  executableIdentity: requester.identity
3249
1468
  });
@@ -3252,7 +1471,7 @@ async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY)
3252
1471
  // src/cli.ts
3253
1472
  var program = new Command();
3254
1473
  program.exitOverride();
3255
- program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").option("--client-key <key>", "stable namespace for one-shot Agent state").addHelpText("after", `
1474
+ program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").option("--client-key <key>", "stable namespace for Agent browser state").option("--request-id <id>", "stable host request ID for safe retry recovery").option("--timeout <ms>", "browser command deadline in milliseconds", "60000").addHelpText("after", `
3256
1475
  Workflow:
3257
1476
  bp connect # one-time setup (click Allow in Chrome)
3258
1477
  bp profiles # list live Chrome Profile contexts
@@ -3307,6 +1526,16 @@ Eval (escape hatch for operations without a dedicated command):
3307
1526
  bp eval "JSON.stringify(localStorage)" # read storage
3308
1527
  echo 'complex js here' | bp eval # stdin for complex JS
3309
1528
  `);
1529
+ var cliAbortController = new AbortController();
1530
+ var receivedSignal;
1531
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1532
+ process.once(signal, () => {
1533
+ receivedSignal = signal;
1534
+ cliAbortController.abort();
1535
+ const fallback = setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 2e3);
1536
+ fallback.unref();
1537
+ });
1538
+ }
3310
1539
  function useJson() {
3311
1540
  if (process.argv.slice(2).includes("--human") || program.opts().human) return false;
3312
1541
  return !process.stdout.isTTY;
@@ -3333,7 +1562,7 @@ function fail(error, hint, details) {
3333
1562
  }));
3334
1563
  else console.error(`\u2717 ${error}${hint ? `
3335
1564
  hint: ${hint}` : ""}`);
3336
- process.exit(1);
1565
+ process.exit(receivedSignal === "SIGINT" ? 130 : receivedSignal === "SIGTERM" ? 143 : 1);
3337
1566
  }
3338
1567
  function observationElements(result) {
3339
1568
  return Array.isArray(result.elements) ? result.elements : [];
@@ -3437,7 +1666,91 @@ function cliClientKey() {
3437
1666
  }
3438
1667
  return value;
3439
1668
  }
3440
- function requireString2(value, label) {
1669
+ function cliInvocationOptions() {
1670
+ const requestId = program.opts().requestId ?? process.env.BROWSER_PILOT_REQUEST_ID;
1671
+ if (requestId !== void 0 && (typeof requestId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId))) {
1672
+ throw invalidArgument(
1673
+ "Request ID must be 1-128 characters using letters, digits, dot, underscore, colon, or hyphen",
1674
+ "requestId"
1675
+ );
1676
+ }
1677
+ const rawTimeout = String(program.opts().timeout ?? "60000");
1678
+ const deadlineMs = Number(rawTimeout);
1679
+ if (!/^\d+$/.test(rawTimeout) || !Number.isSafeInteger(deadlineMs) || deadlineMs < 1 || deadlineMs > 3e5) {
1680
+ throw invalidArgument("--timeout must be an integer from 1 through 300000", "timeout");
1681
+ }
1682
+ return {
1683
+ ...requestId ? { requestId } : {},
1684
+ deadlineMs,
1685
+ signal: cliAbortController.signal
1686
+ };
1687
+ }
1688
+ function cliCommand(command) {
1689
+ return {
1690
+ id: command.id,
1691
+ method: command.method,
1692
+ mutating: command.mutating,
1693
+ status: command.status,
1694
+ acceptedAt: command.acceptedAt,
1695
+ deadlineAt: command.deadlineAt,
1696
+ ...command.targetId ? { targetId: command.targetId } : {},
1697
+ ...command.dispatchedAt !== void 0 ? { dispatchedAt: command.dispatchedAt } : {},
1698
+ ...command.completedAt !== void 0 ? { completedAt: command.completedAt } : {},
1699
+ ...command.cancellationRequested ? { cancellationRequested: true } : {}
1700
+ };
1701
+ }
1702
+ function cliCommandOutcome(outcome) {
1703
+ return {
1704
+ command: cliCommand(outcome.command),
1705
+ ...outcome.result !== void 0 ? { result: outcome.result } : {},
1706
+ ...outcome.error !== void 0 ? { error: outcome.error } : {}
1707
+ };
1708
+ }
1709
+ function outputPath(filename) {
1710
+ if (isAbsolute3(filename)) return resolvePath(filename);
1711
+ const configured = process.env.BROWSER_PILOT_OUTPUT_DIR;
1712
+ if (configured !== void 0) {
1713
+ if (!isAbsolute3(configured)) {
1714
+ throw invalidArgument("BROWSER_PILOT_OUTPUT_DIR must be an absolute path", "outputDir");
1715
+ }
1716
+ mkdirSync2(configured, { recursive: true, mode: 448 });
1717
+ return resolvePath(configured, filename);
1718
+ }
1719
+ return resolvePath(filename);
1720
+ }
1721
+ function artifactFileResult(artifact, file) {
1722
+ return {
1723
+ file,
1724
+ mimeType: artifact.mimeType,
1725
+ sizeBytes: artifact.byteSize,
1726
+ ...artifact.width !== void 0 ? { width: artifact.width } : {},
1727
+ ...artifact.height !== void 0 ? { height: artifact.height } : {}
1728
+ };
1729
+ }
1730
+ function delay2(ms) {
1731
+ if (cliAbortController.signal.aborted) {
1732
+ return Promise.reject(new BrowserPilotError("command_cancelled", "Wait was cancelled"));
1733
+ }
1734
+ return new Promise((resolve, reject) => {
1735
+ const timer = setTimeout(done, ms);
1736
+ function done() {
1737
+ cliAbortController.signal.removeEventListener("abort", cancelled);
1738
+ resolve();
1739
+ }
1740
+ function cancelled() {
1741
+ clearTimeout(timer);
1742
+ cliAbortController.signal.removeEventListener("abort", cancelled);
1743
+ reject(new BrowserPilotError("command_cancelled", "Wait was cancelled"));
1744
+ }
1745
+ cliAbortController.signal.addEventListener("abort", cancelled, { once: true });
1746
+ });
1747
+ }
1748
+ function matchesUrl(url, pattern) {
1749
+ if (!pattern.includes("*")) return url.includes(pattern);
1750
+ const source = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1751
+ return new RegExp(`^${source}$`).test(url);
1752
+ }
1753
+ function requireString(value, label) {
3441
1754
  if (typeof value !== "string") throw new BrowserPilotError("internal_error", `${label} is missing`);
3442
1755
  return value;
3443
1756
  }
@@ -3458,7 +1771,7 @@ async function cliElementAddress(client, target, raw) {
3458
1771
  return { selector: raw };
3459
1772
  }
3460
1773
  async function requireCompatibility() {
3461
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
1774
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
3462
1775
  if (!client) {
3463
1776
  throw new BrowserPilotError("browser_disconnected", "Browser Pilot is not connected", {
3464
1777
  retryable: true,
@@ -3512,7 +1825,7 @@ function cliProfile(profile, index) {
3512
1825
  };
3513
1826
  }
3514
1827
  function withCliTarget(operation) {
3515
- return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation, cliClientKey());
1828
+ return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation, cliClientKey(), cliInvocationOptions());
3516
1829
  }
3517
1830
  function readStdin() {
3518
1831
  if (process.stdin.isTTY) return Promise.resolve("");
@@ -3524,9 +1837,212 @@ function readStdin() {
3524
1837
  process.stdin.on("end", () => resolve(d.trim()));
3525
1838
  });
3526
1839
  }
1840
+ program.command("status").description("Show current Broker, browser, Agent namespace, and recovery state").action(action(async () => {
1841
+ const brokerRunning = isDaemonRunning();
1842
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
1843
+ if (!client) {
1844
+ const browsers = (await discoverBrowserCandidates()).map((discovered) => discovered.candidate);
1845
+ emit({
1846
+ ok: true,
1847
+ service: { state: brokerRunning ? "unavailable" : "stopped", version: BROWSER_PILOT_VERSION },
1848
+ browser: { state: "disconnected" },
1849
+ browsers,
1850
+ session: { state: "unavailable" },
1851
+ recovery: {
1852
+ required: true,
1853
+ code: "browser_disconnected",
1854
+ action: "bp connect"
1855
+ }
1856
+ }, brokerRunning ? "Browser Pilot Broker is running, but the browser session is unavailable." : "Browser Pilot is not connected. Run bp connect.");
1857
+ return;
1858
+ }
1859
+ const [activeCommands, uncertainCommands] = await Promise.all([
1860
+ client.listCommands(20, ["accepted", "dispatched"]),
1861
+ client.listCommands(20, ["unknown_outcome"])
1862
+ ]);
1863
+ let tabs = [];
1864
+ let profiles = [];
1865
+ let browserStateError;
1866
+ try {
1867
+ [tabs, profiles] = await Promise.all([client.listTabs("all"), client.listProfiles()]);
1868
+ } catch (error) {
1869
+ browserStateError = error instanceof BrowserPilotError ? error : new BrowserPilotError("internal_error", error instanceof Error ? error.message : String(error));
1870
+ }
1871
+ const selectedTarget = tabs.find((tab) => tab.selected ?? tab.active === true);
1872
+ const selectedProfile = profiles.find((profile) => profile.selected);
1873
+ const browser = client.initialized.browsers.find((candidate) => candidate.state === "ready") ?? client.initialized.browsers[0];
1874
+ const recovery = browserStateError ? {
1875
+ required: true,
1876
+ code: browserStateError.code,
1877
+ action: browserStateError.remediation?.message ?? "Inspect the browser connection before continuing."
1878
+ } : uncertainCommands.length > 0 ? {
1879
+ required: true,
1880
+ code: "unknown_outcome",
1881
+ action: "Inspect the current tab and the uncertain command before retrying."
1882
+ } : { required: false };
1883
+ emit({
1884
+ ok: true,
1885
+ service: {
1886
+ state: "running",
1887
+ version: client.initialized.serviceVersion
1888
+ },
1889
+ browser: browser ? { id: browser.id, product: browser.product, state: browser.state } : { state: "disconnected" },
1890
+ session: {
1891
+ state: client.lease.state,
1892
+ expiresAt: client.lease.expiresAt,
1893
+ profile: selectedProfile ? cliProfile(selectedProfile, profiles.indexOf(selectedProfile)) : null,
1894
+ target: selectedTarget ? {
1895
+ index: tabs.indexOf(selectedTarget) + 1,
1896
+ title: selectedTarget.title,
1897
+ url: selectedTarget.url,
1898
+ origin: selectedTarget.origin,
1899
+ profileContextId: selectedTarget.profileContextId
1900
+ } : null
1901
+ },
1902
+ commands: {
1903
+ active: activeCommands.map(cliCommand),
1904
+ uncertain: uncertainCommands.map(cliCommand)
1905
+ },
1906
+ recovery
1907
+ }, `Browser Pilot: ${browser?.state ?? "disconnected"}; ${tabs.length} tab(s); ${activeCommands.length} active command(s)`);
1908
+ }));
1909
+ program.command("commands").description("List recent commands for this Agent namespace").option("-l, --limit <n>", "maximum commands to return", "20").option("--status <statuses>", "comma-separated Command statuses").action(action(async (opts) => {
1910
+ const limit = parseLimit(opts.limit);
1911
+ if (limit > 100) throw invalidArgument("--limit must not exceed 100", "limit");
1912
+ const statuses = opts.status === void 0 ? void 0 : String(opts.status).split(",").map((status) => status.trim()).filter(Boolean);
1913
+ const validStatuses = /* @__PURE__ */ new Set([
1914
+ "accepted",
1915
+ "dispatched",
1916
+ "completed",
1917
+ "unknown_outcome",
1918
+ "cancelled",
1919
+ "expired"
1920
+ ]);
1921
+ if (statuses && (statuses.length === 0 || statuses.some((status) => !validStatuses.has(status)))) {
1922
+ throw invalidArgument("--status contains an invalid Command status", "status");
1923
+ }
1924
+ const commands = await (await requireCompatibility()).listCommands(limit, statuses);
1925
+ if (useJson()) {
1926
+ console.log(JSON.stringify({ ok: true, commands: commands.map(cliCommand) }));
1927
+ } else if (commands.length === 0) {
1928
+ console.log("No recent commands.");
1929
+ } else {
1930
+ for (const command of commands) {
1931
+ console.log(`${command.id} ${command.status.padEnd(15)} ${command.method}`);
1932
+ }
1933
+ }
1934
+ }));
1935
+ program.command("command <commandId>").description("Inspect one recent command and its outcome").action(action(async (commandId) => {
1936
+ const outcome = await (await requireCompatibility()).getCommand(commandId);
1937
+ const result = cliCommandOutcome(outcome);
1938
+ if (useJson()) console.log(JSON.stringify({ ok: true, ...result }));
1939
+ else console.log(`${outcome.command.id}: ${outcome.command.status} (${outcome.command.method})`);
1940
+ }));
1941
+ program.command("cancel <commandId>").description("Request cancellation of a running command").action(action(async (commandId) => {
1942
+ const outcome = await (await requireCompatibility()).cancelCommand(commandId);
1943
+ const result = cliCommandOutcome(outcome);
1944
+ if (useJson()) console.log(JSON.stringify({ ok: true, ...result }));
1945
+ else console.log(`${outcome.command.id}: ${outcome.command.status}`);
1946
+ }));
1947
+ program.command("wait").description("Wait for a browser-visible condition").option("--url <pattern>", "wait for the selected tab URL to contain a value or match a * glob").option("--text <text>", "wait for visible page text").option("--selector <selector>", "wait for a CSS selector").option("--dialog", "wait for a pending JavaScript dialog").option("--download", "wait for a completed unexported download").option("--popup", "wait for a managed popup tab").option("--interval <ms>", "poll interval in milliseconds", "250").action(action(async (opts) => {
1948
+ const conditions = [
1949
+ opts.url !== void 0,
1950
+ opts.text !== void 0,
1951
+ opts.selector !== void 0,
1952
+ opts.dialog === true,
1953
+ opts.download === true,
1954
+ opts.popup === true
1955
+ ].filter(Boolean).length;
1956
+ if (conditions !== 1) {
1957
+ throw invalidArgument(
1958
+ "Choose exactly one of --url, --text, --selector, --dialog, --download, or --popup",
1959
+ "condition"
1960
+ );
1961
+ }
1962
+ const intervalMs = Number(opts.interval);
1963
+ if (!/^\d+$/.test(String(opts.interval)) || !Number.isSafeInteger(intervalMs) || intervalMs < 100 || intervalMs > 5e3) {
1964
+ throw invalidArgument("--interval must be an integer from 100 through 5000", "interval");
1965
+ }
1966
+ const waitTimeoutMs = cliInvocationOptions().deadlineMs ?? 6e4;
1967
+ const startedAt = Date.now();
1968
+ const deadlineAt = startedAt + waitTimeoutMs;
1969
+ const client = await requireCompatibility();
1970
+ const needsTarget = opts.url !== void 0 || opts.text !== void 0 || opts.selector !== void 0;
1971
+ const target = needsTarget ? await client.ensureTarget() : void 0;
1972
+ const condition = opts.url !== void 0 ? "url" : opts.text !== void 0 ? "text" : opts.selector !== void 0 ? "selector" : opts.dialog ? "dialog" : opts.download ? "download" : "popup";
1973
+ while (Date.now() <= deadlineAt) {
1974
+ let matched;
1975
+ if (opts.url !== void 0) {
1976
+ const tabs = await client.listTabs("all");
1977
+ const current = tabs.find((tab) => tab.targetId === target.targetId);
1978
+ if (current && matchesUrl(current.url, String(opts.url))) {
1979
+ matched = {
1980
+ title: current.title,
1981
+ url: current.url,
1982
+ origin: current.origin,
1983
+ profileContextId: current.profileContextId
1984
+ };
1985
+ }
1986
+ } else if (opts.text !== void 0) {
1987
+ const result = await client.callTool("browser.search", {
1988
+ query: String(opts.text),
1989
+ limit: 1
1990
+ }, target.targetId);
1991
+ const matches = Array.isArray(result.matches) ? result.matches : [];
1992
+ if (matches.length > 0) matched = { title: result.title, url: result.url, match: matches[0] };
1993
+ } else if (opts.selector !== void 0) {
1994
+ const result = await client.callTool("browser.elements.find", {
1995
+ selector: String(opts.selector),
1996
+ limit: 1
1997
+ }, target.targetId);
1998
+ const elements = Array.isArray(result.elements) ? result.elements : [];
1999
+ if (elements.length > 0) matched = { url: result.url, element: elements[0] };
2000
+ } else if (opts.dialog) {
2001
+ const result = await client.callTool("browser.dialogs.list");
2002
+ const dialogs = Array.isArray(result.dialogs) ? result.dialogs : [];
2003
+ if (dialogs.length > 0) matched = dialogs[0];
2004
+ } else if (opts.download) {
2005
+ const downloads = await client.listArtifacts(["download"]);
2006
+ if (downloads.length > 0) matched = {
2007
+ index: 1,
2008
+ id: downloads[0].id,
2009
+ fileName: downloads[0].fileName ?? null,
2010
+ mimeType: downloads[0].mimeType,
2011
+ sizeBytes: downloads[0].byteSize,
2012
+ createdAt: downloads[0].createdAt,
2013
+ expiresAt: downloads[0].expiresAt
2014
+ };
2015
+ } else {
2016
+ const tabs = await client.listTabs("all");
2017
+ const popup = tabs.find((tab) => tab.origin === "managed_popup");
2018
+ if (popup) {
2019
+ matched = {
2020
+ title: popup.title,
2021
+ url: popup.url,
2022
+ origin: popup.origin,
2023
+ profileContextId: popup.profileContextId
2024
+ };
2025
+ }
2026
+ }
2027
+ if (matched !== void 0) {
2028
+ emit({
2029
+ ok: true,
2030
+ condition,
2031
+ elapsedMs: Date.now() - startedAt,
2032
+ matched
2033
+ }, `Condition satisfied: ${condition}`);
2034
+ return;
2035
+ }
2036
+ await delay2(Math.min(intervalMs, Math.max(0, deadlineAt - Date.now())));
2037
+ }
2038
+ throw new BrowserPilotError("wait_timeout", `Timed out waiting for ${condition}`, {
2039
+ retryable: true,
2040
+ context: { condition, timeoutMs: waitTimeoutMs }
2041
+ });
2042
+ }));
3527
2043
  program.command("browsers").description("List supported local browsers and their setup state").option("-b, --browser <name>", "filter by browser ID, product, or channel").action(action(async (opts) => {
3528
2044
  const filter = typeof opts.browser === "string" ? opts.browser.toLowerCase() : void 0;
3529
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
2045
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
3530
2046
  const browsers = client ? await client.listBrowsers(filter) : (await discoverBrowserCandidates()).map((discovered) => discovered.candidate).filter((candidate) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
3531
2047
  if (useJson()) {
3532
2048
  console.log(JSON.stringify({ ok: true, browsers }));
@@ -3553,7 +2069,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
3553
2069
  console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
3554
2070
  `);
3555
2071
  }
3556
- const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser, cliClientKey());
2072
+ const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser, cliClientKey(), cliInvocationOptions());
3557
2073
  await client.connectBrowser();
3558
2074
  const profiles = await client.listProfiles();
3559
2075
  if (profiles.length <= 1) await client.ensureManagedTarget();
@@ -3575,25 +2091,6 @@ Multiple Chrome Profiles are open. Run 'bp profiles --identify', then 'bp profil
3575
2091
  Ready! Try: bp open https://example.com`
3576
2092
  );
3577
2093
  }));
3578
- program.command("bridge").description("Run the Agent-neutral JSON-RPC bridge").option("--stdio", "use newline-delimited JSON-RPC over stdin/stdout").option("-b, --browser <name>", "prefer a browser ID, product, or channel when starting the Broker").action((opts) => {
3579
- if (!opts.stdio) {
3580
- process.stderr.write("bridge currently requires --stdio\n");
3581
- process.exitCode = 2;
3582
- return;
3583
- }
3584
- void runStdioBridge({
3585
- input: process.stdin,
3586
- output: process.stdout,
3587
- backend: new DaemonBridgeBackend(opts.browser)
3588
- }).then((result) => {
3589
- process.exitCode = result.exitCode;
3590
- }).catch((error) => {
3591
- const message = error instanceof Error ? error.message : String(error);
3592
- process.stderr.write(`Bridge error: ${message}
3593
- `);
3594
- process.exitCode = 1;
3595
- });
3596
- });
3597
2094
  program.command("disconnect").description("Release CLI browser state and stop an otherwise unused daemon").action(action(async () => {
3598
2095
  await shutdownCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
3599
2096
  emit({ ok: true }, "\u2713 Disconnected");
@@ -3755,10 +2252,10 @@ Examples:
3755
2252
  bp keyboard "Hello Docs!" --click ".kix-appview-editor"
3756
2253
  bp keyboard "slow typing" --delay 50`).action(action(async (text, opts) => {
3757
2254
  const limit = parseLimit(opts.limit);
3758
- let delay2 = 0;
2255
+ let delay3 = 0;
3759
2256
  if (opts.delay) {
3760
- delay2 = Number(opts.delay);
3761
- if (!/^\d+$/.test(opts.delay) || !Number.isSafeInteger(delay2) || delay2 < 0) {
2257
+ delay3 = Number(opts.delay);
2258
+ if (!/^\d+$/.test(opts.delay) || !Number.isSafeInteger(delay3) || delay3 < 0) {
3762
2259
  throw invalidArgument("--delay must be a non-negative integer", "delay");
3763
2260
  }
3764
2261
  }
@@ -3767,7 +2264,7 @@ Examples:
3767
2264
  text,
3768
2265
  ...opts.clear ? { clear: true } : {},
3769
2266
  ...opts.submit ? { submit: true } : {},
3770
- delayMs: delay2,
2267
+ delayMs: delay3,
3771
2268
  ...opts.click ? { focusSelector: opts.click } : {},
3772
2269
  observationLimit: limit
3773
2270
  }, target.targetId);
@@ -4016,6 +2513,43 @@ program.command("upload <filepath>").description('Upload file (auto-finds <input
4016
2513
  }
4017
2514
  });
4018
2515
  }));
2516
+ program.command("downloads").description("List completed downloads available to this Agent namespace").action(action(async () => {
2517
+ const artifacts = await (await requireCompatibility()).listArtifacts(["download"]);
2518
+ const downloads = artifacts.map((artifact, index) => ({
2519
+ index: index + 1,
2520
+ id: artifact.id,
2521
+ fileName: artifact.fileName,
2522
+ mimeType: artifact.mimeType,
2523
+ sizeBytes: artifact.byteSize,
2524
+ createdAt: artifact.createdAt,
2525
+ expiresAt: artifact.expiresAt
2526
+ }));
2527
+ if (useJson()) {
2528
+ console.log(JSON.stringify({ ok: true, downloads }));
2529
+ } else if (downloads.length === 0) {
2530
+ console.log("No completed downloads.");
2531
+ } else {
2532
+ for (const download of downloads) {
2533
+ console.log(`${download.index} ${download.fileName ?? download.id} ${download.sizeBytes} bytes`);
2534
+ }
2535
+ }
2536
+ }));
2537
+ program.command("download <selector> [filename]").description("Export one completed download to a local file").action(action(async (selector, filename) => {
2538
+ const client = await requireCompatibility();
2539
+ const artifacts = await client.listArtifacts(["download"]);
2540
+ const artifact = /^\d+$/.test(selector) ? artifacts[Number(selector) - 1] : artifacts.find((candidate) => candidate.id === selector);
2541
+ if (!artifact) {
2542
+ throw new BrowserPilotError("artifact_not_found", "Download was not found for this Agent namespace", {
2543
+ context: { field: "selector" }
2544
+ });
2545
+ }
2546
+ const fallbackName = artifact.fileName ? basename2(artifact.fileName) : `download-${artifact.createdAt}`;
2547
+ const file = outputPath(filename ?? fallbackName);
2548
+ await client.exportArtifact(artifact.id, file);
2549
+ await client.releaseArtifact(artifact.id).catch(() => {
2550
+ });
2551
+ emit({ ok: true, ...artifactFileResult(artifact, file) }, `\u2713 Download saved to ${file}`);
2552
+ }));
4019
2553
  program.command("screenshot [filename]").description("Capture screenshot").option("-f, --full", "capture full page").option("--selector <sel>", "capture specific element").option("--annotate [refs]", "draw Observation ref boxes; optionally comma-separated refs").addHelpText("after", '\nExamples:\n bp screenshot\n bp screenshot page.png\n bp screenshot --full\n bp screenshot --selector ".chart"\n bp screenshot page.png --annotate\n bp screenshot page.png --annotate 1,3,8').action(action(async (filename, opts) => {
4020
2554
  if (opts.annotate !== void 0 && (opts.full || opts.selector)) {
4021
2555
  throw invalidArgument("--annotate cannot be combined with --full or --selector", "annotate");
@@ -4037,9 +2571,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
4037
2571
  }, target.targetId);
4038
2572
  const artifact = artifactFrom(result);
4039
2573
  const file = filename ?? `screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.png`;
4040
- const outputPath = resolvePath(file);
2574
+ const destination = outputPath(file);
4041
2575
  try {
4042
- await client.exportArtifact(artifact.id, outputPath);
2576
+ await client.exportArtifact(artifact.id, destination);
4043
2577
  } finally {
4044
2578
  await client.releaseArtifact(artifact.id).catch(() => {
4045
2579
  });
@@ -4051,9 +2585,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
4051
2585
  }
4052
2586
  emit({
4053
2587
  ok: true,
4054
- file: outputPath,
2588
+ ...artifactFileResult(artifact, destination),
4055
2589
  ...typeof result.annotationCount === "number" ? { annotationCount: result.annotationCount } : {}
4056
- }, `\u2713 Screenshot saved to ${outputPath}`);
2590
+ }, `\u2713 Screenshot saved to ${destination}`);
4057
2591
  });
4058
2592
  }));
4059
2593
  program.command("pdf [filename]").description("Save page as PDF").option("--landscape", "landscape orientation").addHelpText("after", "\nExamples:\n bp pdf\n bp pdf report.pdf\n bp pdf report.pdf --landscape").action(action(async (filename, opts) => {
@@ -4063,14 +2597,14 @@ program.command("pdf [filename]").description("Save page as PDF").option("--land
4063
2597
  }, target.targetId);
4064
2598
  const artifact = artifactFrom(result);
4065
2599
  const file = filename ?? `page-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.pdf`;
4066
- const outputPath = resolvePath(file);
2600
+ const destination = outputPath(file);
4067
2601
  try {
4068
- await client.exportArtifact(artifact.id, outputPath);
2602
+ await client.exportArtifact(artifact.id, destination);
4069
2603
  } finally {
4070
2604
  await client.releaseArtifact(artifact.id).catch(() => {
4071
2605
  });
4072
2606
  }
4073
- emit({ ok: true, file: outputPath }, `\u2713 PDF saved to ${outputPath}`);
2607
+ emit({ ok: true, ...artifactFileResult(artifact, destination) }, `\u2713 PDF saved to ${destination}`);
4074
2608
  });
4075
2609
  }));
4076
2610
  program.command("cookies [domain]").description("View cookies (CDP-only, includes HttpOnly)").addHelpText("after", "\nExamples:\n bp cookies\n bp cookies github.com").action(action(async (domain) => {
@@ -4114,7 +2648,7 @@ program.command("frame [target]").description("List frames, or switch to a frame
4114
2648
  );
4115
2649
  }
4116
2650
  const frame = frames[idx];
4117
- await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString2(frame.frameId, "frameId") }, controlledTarget.targetId);
2651
+ await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString(frame.frameId, "frameId") }, controlledTarget.targetId);
4118
2652
  emit(
4119
2653
  { ok: true, frame: idx, url: frame.url },
4120
2654
  `\u2713 Switched to frame ${idx}: ${frame.url}`
@@ -4162,7 +2696,7 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
4162
2696
  dialogId,
4163
2697
  action: opts.accept ? "accept" : "dismiss",
4164
2698
  ...opts.prompt !== void 0 ? { promptText: opts.prompt } : {}
4165
- }, requireString2(dialog.targetId, "dialog targetId"));
2699
+ }, requireString(dialog.targetId, "dialog targetId"));
4166
2700
  emit(
4167
2701
  { ok: true, dialogId: result.dialogId, action: result.action },
4168
2702
  `\u2713 ${result.action === "accept" ? "Accepted" : "Dismissed"} dialog ${result.dialogId}`
@@ -4313,7 +2847,7 @@ netCmd.command("show <id>").description("Show full request/response details").op
4313
2847
  const id = Number(idStr);
4314
2848
  const summary = await findNetworkRequest(client, id);
4315
2849
  const result = await client.callTool("browser.network.request", {
4316
- requestId: requireString2(summary.requestId, "requestId"),
2850
+ requestId: requireString(summary.requestId, "requestId"),
4317
2851
  includeBody: true
4318
2852
  });
4319
2853
  const request = result.request && typeof result.request === "object" && !Array.isArray(result.request) ? result.request : {};
@@ -4326,10 +2860,15 @@ netCmd.command("show <id>").description("Show full request/response details").op
4326
2860
  { context: { sequence: id } }
4327
2861
  );
4328
2862
  }
4329
- const outputPath = resolvePath(opts.save);
2863
+ const destination = outputPath(opts.save);
4330
2864
  const bytes = result.bodyEncoding === "base64" ? Buffer.from(responseBody, "base64") : Buffer.from(responseBody, "utf8");
4331
- writeFileSync2(outputPath, bytes);
4332
- emit({ ok: true, file: outputPath }, `Saved to ${outputPath}`);
2865
+ writeFileSync2(destination, bytes, { mode: 384 });
2866
+ emit({
2867
+ ok: true,
2868
+ file: destination,
2869
+ mimeType: typeof request.mimeType === "string" ? request.mimeType : "application/octet-stream",
2870
+ sizeBytes: bytes.byteLength
2871
+ }, `Saved to ${destination}`);
4333
2872
  return;
4334
2873
  }
4335
2874
  const detail = { id: request.sequence, ...request, responseBody };