browser-pilot-cli 0.3.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/README.md +270 -354
- package/dist/cli.js +1285 -2864
- package/dist/daemon.js +1142 -887
- package/docs/architecture/browser-pilot-platform-spec.md +440 -1013
- package/docs/plans/browser-capability-evolution.md +6 -6
- package/docs/plans/profile-context-routing.md +35 -10
- package/docs/plans/universal-agent-integration.md +121 -335
- package/docs/plans/v0.3.0-stabilization.md +10 -4
- package/docs/releases/v0.2.0.md +53 -0
- package/docs/releases/v0.2.1.md +13 -0
- package/docs/releases/v0.2.2.md +18 -0
- package/docs/releases/v0.4.0.md +62 -0
- package/docs/releases/v0.5.0.md +93 -0
- package/package.json +8 -15
- package/docs/integration/stdio-bridge.md +0 -670
- package/docs/integration/stdio-conformance.md +0 -51
- package/scripts/run-stdio-conformance.mjs +0 -698
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
6
|
-
import { resolve as resolvePath } from "path";
|
|
4
|
+
import { Command, CommanderError } from "commander";
|
|
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.
|
|
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/
|
|
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 {
|
|
142
|
-
import {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 (
|
|
235
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
254
|
-
|
|
270
|
+
function pathExistsWithoutFollowingSymlink(path) {
|
|
271
|
+
try {
|
|
272
|
+
lstatSync(path);
|
|
273
|
+
return true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
255
277
|
}
|
|
256
|
-
function
|
|
257
|
-
return
|
|
278
|
+
function installed(definition) {
|
|
279
|
+
return pathExistsWithoutFollowingSymlink(definition.dataDir) || definition.installPaths.some(pathExistsWithoutFollowingSymlink);
|
|
258
280
|
}
|
|
259
|
-
function
|
|
260
|
-
|
|
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 =
|
|
561
|
+
const digest = createHash3("sha256").update(`${resolvedPath}\0${version}\0`);
|
|
287
562
|
for (const artifactPath of [
|
|
288
563
|
resolvedPath,
|
|
289
|
-
|
|
290
|
-
|
|
564
|
+
join3(dirname(resolvedPath), "daemon.js"),
|
|
565
|
+
join3(dirname(resolvedPath), "managed-target-janitor.js")
|
|
291
566
|
]) {
|
|
292
|
-
if (!
|
|
293
|
-
digest.update(basename(artifactPath)).update("\0").update(
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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() -
|
|
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 (!
|
|
782
|
+
if (!existsSync3(BROWSER_PILOT_PATHS.pidFile)) return false;
|
|
462
783
|
try {
|
|
463
|
-
const raw =
|
|
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:
|
|
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(
|
|
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(
|
|
883
|
+
async brokerCall(clientSessionId, method, params) {
|
|
557
884
|
return this.request("/broker/rpc", {
|
|
558
|
-
|
|
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");
|
|
@@ -598,648 +915,42 @@ var DaemonClient = class {
|
|
|
598
915
|
}
|
|
599
916
|
async netRequests(opts) {
|
|
600
917
|
const p = new URLSearchParams();
|
|
601
|
-
if (opts?.limit) p.set("limit", String(opts.limit));
|
|
602
|
-
if (opts?.url) p.set("url", opts.url);
|
|
603
|
-
if (opts?.method) p.set("method", opts.method);
|
|
604
|
-
if (opts?.status) p.set("status", opts.status);
|
|
605
|
-
if (opts?.type) p.set("type", opts.type);
|
|
606
|
-
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
|
-
profile: 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 } : {} });
|
|
922
|
-
}
|
|
923
|
-
return results;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// src/page-scripts.ts
|
|
927
|
-
var CLICK_TARGET_STATE_FUNCTION = `function() {
|
|
928
|
-
const target = this;
|
|
929
|
-
if (!target || target.nodeType !== 1 || !target.isConnected) {
|
|
930
|
-
return {connected:false, kind:'other', focused:false};
|
|
931
|
-
}
|
|
932
|
-
const composedParent = node => {
|
|
933
|
-
if (node && node.parentElement) return node.parentElement;
|
|
934
|
-
const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
|
|
935
|
-
return root && root.host ? root.host : null;
|
|
936
|
-
};
|
|
937
|
-
const composedContains = (container, node) => {
|
|
938
|
-
for (let current = node; current; current = composedParent(current)) {
|
|
939
|
-
if (current === container) return true;
|
|
940
|
-
}
|
|
941
|
-
return false;
|
|
942
|
-
};
|
|
943
|
-
const isShadowHostOf = (node, possibleHost) => {
|
|
944
|
-
let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
|
|
945
|
-
while (root && root.host) {
|
|
946
|
-
if (root.host === possibleHost) return true;
|
|
947
|
-
root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
|
|
948
|
-
}
|
|
949
|
-
return false;
|
|
950
|
-
};
|
|
951
|
-
const token = (value, mixed) => {
|
|
952
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
953
|
-
if (normalized === 'true') return true;
|
|
954
|
-
if (normalized === 'false') return false;
|
|
955
|
-
return mixed && normalized === 'mixed' ? 'mixed' : undefined;
|
|
956
|
-
};
|
|
957
|
-
const tag = String(target.tagName || '').toLowerCase();
|
|
958
|
-
const role = String(target.getAttribute && target.getAttribute('role') || '').trim().toLowerCase();
|
|
959
|
-
const inputType = tag === 'input' ? String(target.type || '').toLowerCase() : '';
|
|
960
|
-
let kind = 'other';
|
|
961
|
-
if (inputType === 'checkbox' || role === 'checkbox' || role === 'menuitemcheckbox') kind = 'checkbox';
|
|
962
|
-
else if (inputType === 'radio' || role === 'radio' || role === 'menuitemradio') kind = 'radio';
|
|
963
|
-
else if (role === 'switch') kind = 'switch';
|
|
964
|
-
else if (tag === 'option' || role === 'option') kind = 'option';
|
|
965
|
-
else if (tag === 'select' || role === 'listbox' || role === 'combobox') kind = 'select';
|
|
966
|
-
else if (
|
|
967
|
-
tag === 'button' || tag === 'a' || tag === 'input' || tag === 'textarea' ||
|
|
968
|
-
role === 'button' || role === 'link' || role === 'tab' || role === 'menuitem'
|
|
969
|
-
) kind = 'control';
|
|
970
|
-
|
|
971
|
-
const state = {connected:true, kind, focused:false};
|
|
972
|
-
if ((kind === 'checkbox' || kind === 'radio') && tag === 'input') {
|
|
973
|
-
state.checked = !!target.checked;
|
|
974
|
-
} else if (kind === 'checkbox' || kind === 'radio' || kind === 'switch') {
|
|
975
|
-
const checked = token(target.getAttribute('aria-checked'), true);
|
|
976
|
-
if (checked !== undefined) state.checked = checked;
|
|
977
|
-
}
|
|
978
|
-
if (kind === 'option' && tag === 'option') state.selected = !!target.selected;
|
|
979
|
-
else {
|
|
980
|
-
const selected = token(target.getAttribute('aria-selected'), false);
|
|
981
|
-
if (selected !== undefined) state.selected = selected === true;
|
|
982
|
-
}
|
|
983
|
-
const pressed = token(target.getAttribute('aria-pressed'), true);
|
|
984
|
-
if (pressed !== undefined) state.pressed = pressed;
|
|
985
|
-
const expanded = token(target.getAttribute('aria-expanded'), false);
|
|
986
|
-
if (expanded !== undefined) state.expanded = expanded === true;
|
|
987
|
-
|
|
988
|
-
let active = document.activeElement;
|
|
989
|
-
for (let depth = 0; depth < 32 && active && active.shadowRoot && active.shadowRoot.activeElement; depth += 1) {
|
|
990
|
-
active = active.shadowRoot.activeElement;
|
|
991
|
-
}
|
|
992
|
-
state.focused = active === target || composedContains(target, active) || isShadowHostOf(target, active);
|
|
993
|
-
return state;
|
|
994
|
-
}`;
|
|
995
|
-
var GET_POINTER_TARGET_STATE = `function() {
|
|
996
|
-
const blocked = (reason, obstruction) => ({
|
|
997
|
-
status: 'blocked',
|
|
998
|
-
reason,
|
|
999
|
-
...(obstruction ? {obstruction} : {}),
|
|
1000
|
-
});
|
|
1001
|
-
const composedParent = node => {
|
|
1002
|
-
if (node && node.parentElement) return node.parentElement;
|
|
1003
|
-
const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
|
|
1004
|
-
return root && root.host ? root.host : null;
|
|
1005
|
-
};
|
|
1006
|
-
const composedContains = (container, node) => {
|
|
1007
|
-
for (let current = node; current; current = composedParent(current)) {
|
|
1008
|
-
if (current === container) return true;
|
|
1009
|
-
}
|
|
1010
|
-
return false;
|
|
1011
|
-
};
|
|
1012
|
-
const isShadowHostOf = (node, possibleHost) => {
|
|
1013
|
-
let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
|
|
1014
|
-
while (root && root.host) {
|
|
1015
|
-
if (root.host === possibleHost) return true;
|
|
1016
|
-
root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
|
|
1017
|
-
}
|
|
1018
|
-
return false;
|
|
1019
|
-
};
|
|
1020
|
-
const describes = node => {
|
|
1021
|
-
if (!node || typeof node.tagName !== 'string') return undefined;
|
|
1022
|
-
const tagName = node.tagName.toLowerCase().slice(0, 40);
|
|
1023
|
-
const rawRole = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null;
|
|
1024
|
-
const role = typeof rawRole === 'string' ? rawRole.trim().slice(0, 40) : '';
|
|
1025
|
-
return role ? {tagName, role} : {tagName};
|
|
1026
|
-
};
|
|
1027
|
-
|
|
1028
|
-
const target = this;
|
|
1029
|
-
if (!target || target.nodeType !== 1 || !target.isConnected) return blocked('detached');
|
|
1030
|
-
target.scrollIntoView({block:'center', inline:'center', behavior:'instant'});
|
|
1031
|
-
if (!target.isConnected) return blocked('detached');
|
|
1032
|
-
|
|
1033
|
-
const style = getComputedStyle(target);
|
|
1034
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
|
|
1035
|
-
return blocked('no_layout');
|
|
918
|
+
if (opts?.limit) p.set("limit", String(opts.limit));
|
|
919
|
+
if (opts?.url) p.set("url", opts.url);
|
|
920
|
+
if (opts?.method) p.set("method", opts.method);
|
|
921
|
+
if (opts?.status) p.set("status", opts.status);
|
|
922
|
+
if (opts?.type) p.set("type", opts.type);
|
|
923
|
+
if (opts?.after) p.set("after", String(opts.after));
|
|
924
|
+
const qs = p.toString();
|
|
925
|
+
return this.request(`/net/requests${qs ? "?" + qs : ""}`);
|
|
1036
926
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const ariaDisabled = typeof current.getAttribute === 'function'
|
|
1040
|
-
? current.getAttribute('aria-disabled') : null;
|
|
1041
|
-
const nativeDisabled = typeof current.matches === 'function' && current.matches(':disabled');
|
|
1042
|
-
if (nativeDisabled || current.inert === true || String(ariaDisabled).trim().toLowerCase() === 'true') {
|
|
1043
|
-
return blocked('disabled');
|
|
1044
|
-
}
|
|
927
|
+
async netRequestDetail(id) {
|
|
928
|
+
return this.request(`/net/request/${id}`);
|
|
1045
929
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const viewportHeight = Math.max(0, window.innerHeight || document.documentElement.clientHeight || 0);
|
|
1049
|
-
const rectList = target.getClientRects();
|
|
1050
|
-
const rects = [];
|
|
1051
|
-
for (let index = 0; index < Math.min(rectList.length, 64); index += 1) {
|
|
1052
|
-
const rect = rectList[index];
|
|
1053
|
-
if (
|
|
1054
|
-
Number.isFinite(rect.left) && Number.isFinite(rect.top) &&
|
|
1055
|
-
Number.isFinite(rect.right) && Number.isFinite(rect.bottom) &&
|
|
1056
|
-
rect.width > 0 && rect.height > 0
|
|
1057
|
-
) rects.push(rect);
|
|
1058
|
-
}
|
|
1059
|
-
if (rects.length === 0) return blocked('no_layout');
|
|
1060
|
-
|
|
1061
|
-
const visibleRects = rects.map(rect => ({
|
|
1062
|
-
left: Math.max(0, rect.left),
|
|
1063
|
-
top: Math.max(0, rect.top),
|
|
1064
|
-
right: Math.min(viewportWidth, rect.right),
|
|
1065
|
-
bottom: Math.min(viewportHeight, rect.bottom),
|
|
1066
|
-
})).filter(rect => rect.right > rect.left && rect.bottom > rect.top)
|
|
1067
|
-
.sort((a, b) => ((b.right - b.left) * (b.bottom - b.top)) - ((a.right - a.left) * (a.bottom - a.top)))
|
|
1068
|
-
.slice(0, 4);
|
|
1069
|
-
if (visibleRects.length === 0) return blocked('outside_viewport');
|
|
1070
|
-
|
|
1071
|
-
const labels = [];
|
|
1072
|
-
if (target.labels && typeof target.labels.length === 'number') {
|
|
1073
|
-
for (let index = 0; index < Math.min(target.labels.length, 32); index += 1) {
|
|
1074
|
-
labels.push(target.labels[index]);
|
|
1075
|
-
}
|
|
930
|
+
async netBody(id) {
|
|
931
|
+
return this.request(`/net/body/${id}`);
|
|
1076
932
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
933
|
+
async netClear() {
|
|
934
|
+
await this.request("/net/clear", {});
|
|
1079
935
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (hit === target || composedContains(target, hit) || isShadowHostOf(target, hit)) return true;
|
|
1083
|
-
return labels.some(label => (
|
|
1084
|
-
hit === label || composedContains(label, hit) || isShadowHostOf(label, hit)
|
|
1085
|
-
));
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
|
-
let firstObstruction;
|
|
1089
|
-
for (const rect of visibleRects) {
|
|
1090
|
-
const width = rect.right - rect.left;
|
|
1091
|
-
const height = rect.bottom - rect.top;
|
|
1092
|
-
const points = [
|
|
1093
|
-
[rect.left + width * 0.5, rect.top + height * 0.5],
|
|
1094
|
-
[rect.left + width * 0.25, rect.top + height * 0.25],
|
|
1095
|
-
[rect.left + width * 0.75, rect.top + height * 0.25],
|
|
1096
|
-
[rect.left + width * 0.25, rect.top + height * 0.75],
|
|
1097
|
-
[rect.left + width * 0.75, rect.top + height * 0.75],
|
|
1098
|
-
];
|
|
1099
|
-
for (const [x, y] of points) {
|
|
1100
|
-
const hit = document.elementsFromPoint(x, y)[0];
|
|
1101
|
-
if (accepts(hit)) {
|
|
1102
|
-
const readClickTargetState = ${CLICK_TARGET_STATE_FUNCTION};
|
|
1103
|
-
return {status:'ready', x, y, targetState:readClickTargetState.call(target)};
|
|
1104
|
-
}
|
|
1105
|
-
if (!firstObstruction && hit) firstObstruction = describes(hit);
|
|
1106
|
-
}
|
|
936
|
+
async netAddRule(rule) {
|
|
937
|
+
return this.request("/net/rules", rule);
|
|
1107
938
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
var EDITABLE_STATE_FUNCTION = `function(target) {
|
|
1111
|
-
const unsupported = (reason, inputType) => ({
|
|
1112
|
-
kind:'unsupported', value:'', sensitive:false, editable:false,
|
|
1113
|
-
...(inputType ? {inputType} : {}), reason,
|
|
1114
|
-
});
|
|
1115
|
-
if (!target || target.nodeType !== 1 || !target.isConnected) return unsupported('detached');
|
|
1116
|
-
|
|
1117
|
-
const composedParent = node => {
|
|
1118
|
-
if (node && node.parentElement) return node.parentElement;
|
|
1119
|
-
const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
|
|
1120
|
-
return root && root.host ? root.host : null;
|
|
1121
|
-
};
|
|
1122
|
-
const hasComposedState = (node, property, attribute) => {
|
|
1123
|
-
for (let current = node; current; current = composedParent(current)) {
|
|
1124
|
-
if (current[property] === true || current.hasAttribute?.(attribute)) return true;
|
|
1125
|
-
}
|
|
1126
|
-
return false;
|
|
1127
|
-
};
|
|
1128
|
-
const ariaTrue = (node, name) => String(node.getAttribute?.(name) || '').trim().toLowerCase() === 'true';
|
|
1129
|
-
const blockedReason = node => {
|
|
1130
|
-
if (hasComposedState(node, 'inert', 'inert')) return 'inert';
|
|
1131
|
-
for (let current = node; current; current = composedParent(current)) {
|
|
1132
|
-
if (current.matches?.(':disabled') || current.disabled === true || ariaTrue(current, 'aria-disabled')) {
|
|
1133
|
-
return 'disabled';
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
for (let current = node; current; current = composedParent(current)) {
|
|
1137
|
-
if (current.readOnly === true || ariaTrue(current, 'aria-readonly')) return 'readonly';
|
|
1138
|
-
}
|
|
1139
|
-
return null;
|
|
1140
|
-
};
|
|
1141
|
-
const contenteditableRoot = node => {
|
|
1142
|
-
let root = node;
|
|
1143
|
-
while (root.parentElement && root.parentElement.isContentEditable) root = root.parentElement;
|
|
1144
|
-
return root;
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
if (target instanceof HTMLInputElement) {
|
|
1148
|
-
const inputType = String(target.type || 'text').toLowerCase();
|
|
1149
|
-
const rangeTextTypes = new Set(['text', 'search', 'tel', 'url', 'password']);
|
|
1150
|
-
const selectTextTypes = new Set(['email', 'number']);
|
|
1151
|
-
const valueTypes = new Set(['date', 'time', 'datetime-local', 'month', 'week', 'color', 'range']);
|
|
1152
|
-
const editMode = rangeTextTypes.has(inputType) || selectTextTypes.has(inputType)
|
|
1153
|
-
? 'text' : valueTypes.has(inputType) ? 'value' : null;
|
|
1154
|
-
if (!editMode) return unsupported('unsupported_input_type', inputType);
|
|
1155
|
-
const reason = blockedReason(target);
|
|
1156
|
-
return {
|
|
1157
|
-
kind:'input', value:String(target.value ?? ''), sensitive:inputType === 'password',
|
|
1158
|
-
editable:!reason, inputType, editMode,
|
|
1159
|
-
...(editMode === 'text' ? {selectionMode:selectTextTypes.has(inputType) ? 'select' : 'range'} : {}),
|
|
1160
|
-
...(reason ? {reason} : {}),
|
|
1161
|
-
};
|
|
939
|
+
async netRules() {
|
|
940
|
+
return this.request("/net/rules");
|
|
1162
941
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
return {
|
|
1166
|
-
kind:'input', value:String(target.value ?? ''), sensitive:false,
|
|
1167
|
-
editable:!reason, inputType:'textarea', editMode:'text', selectionMode:'range',
|
|
1168
|
-
...(reason ? {reason} : {}),
|
|
1169
|
-
};
|
|
942
|
+
async netRemoveRule(id) {
|
|
943
|
+
await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
|
|
1170
944
|
}
|
|
1171
|
-
|
|
1172
|
-
const root = contenteditableRoot(target);
|
|
1173
|
-
const reason = blockedReason(root);
|
|
1174
|
-
return {
|
|
1175
|
-
kind:'contenteditable', value:String(root.innerText ?? root.textContent ?? ''), sensitive:false,
|
|
1176
|
-
editable:!reason, inputType:'contenteditable', editMode:'text', selectionMode:'range',
|
|
1177
|
-
...(reason ? {reason} : {}),
|
|
1178
|
-
};
|
|
945
|
+
close() {
|
|
1179
946
|
}
|
|
1180
|
-
|
|
1181
|
-
}`;
|
|
1182
|
-
var PREPARE_EDITABLE_TARGET = `function(clear) {
|
|
1183
|
-
const readState = ${EDITABLE_STATE_FUNCTION};
|
|
1184
|
-
const state = readState(this);
|
|
1185
|
-
if (!state.editable) return state;
|
|
947
|
+
};
|
|
1186
948
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const focusedState = readState(this);
|
|
1190
|
-
if (!focusedState.editable) return focusedState;
|
|
1191
|
-
if (focusedState.editMode === 'text') {
|
|
1192
|
-
if (focusedState.selectionMode === 'range') {
|
|
1193
|
-
const end = String(this.value ?? '').length;
|
|
1194
|
-
this.setSelectionRange(clear ? 0 : end, end, 'none');
|
|
1195
|
-
} else if (clear) {
|
|
1196
|
-
this.select();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
return focusedState;
|
|
1200
|
-
}
|
|
949
|
+
// src/compatibility-broker-client.ts
|
|
950
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
|
|
1201
951
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
root.focus();
|
|
1205
|
-
const focusedState = readState(root);
|
|
1206
|
-
if (!focusedState.editable) return focusedState;
|
|
1207
|
-
const range = document.createRange();
|
|
1208
|
-
range.selectNodeContents(root);
|
|
1209
|
-
if (!clear) range.collapse(false);
|
|
1210
|
-
const selection = window.getSelection();
|
|
1211
|
-
if (!selection) return {...focusedState, editable:false, reason:'selection_unavailable'};
|
|
1212
|
-
selection.removeAllRanges();
|
|
1213
|
-
selection.addRange(range);
|
|
1214
|
-
return focusedState;
|
|
1215
|
-
}`;
|
|
1216
|
-
var SET_VALUE_CONTROL = `function(value) {
|
|
1217
|
-
const readState = ${EDITABLE_STATE_FUNCTION};
|
|
1218
|
-
const state = readState(this);
|
|
1219
|
-
if (!state.editable || state.kind !== 'input' || state.editMode !== 'value') return;
|
|
1220
|
-
this.focus();
|
|
1221
|
-
const beforeInput = new InputEvent('beforeinput', {
|
|
1222
|
-
bubbles:true, composed:true, cancelable:true, inputType:'insertReplacementText', data:value,
|
|
1223
|
-
});
|
|
1224
|
-
if (!this.dispatchEvent(beforeInput)) return;
|
|
1225
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
1226
|
-
if (setter) setter.call(this, value); else this.value = value;
|
|
1227
|
-
this.dispatchEvent(new InputEvent('input', {
|
|
1228
|
-
bubbles:true, composed:true, inputType:'insertReplacementText', data:value,
|
|
1229
|
-
}));
|
|
1230
|
-
}`;
|
|
1231
|
-
var READ_EDITABLE_STATE = `function() {
|
|
1232
|
-
const readState = ${EDITABLE_STATE_FUNCTION};
|
|
1233
|
-
return readState(this);
|
|
1234
|
-
}`;
|
|
1235
|
-
var READ_ACTIVE_EDITABLE_STATE = `(() => {
|
|
1236
|
-
const readState = ${EDITABLE_STATE_FUNCTION};
|
|
1237
|
-
let active = document.activeElement;
|
|
1238
|
-
while (active && active.shadowRoot && active.shadowRoot.activeElement) {
|
|
1239
|
-
active = active.shadowRoot.activeElement;
|
|
1240
|
-
}
|
|
1241
|
-
return readState(active);
|
|
1242
|
-
})()`;
|
|
952
|
+
// src/session.ts
|
|
953
|
+
import { spawn } from "child_process";
|
|
1243
954
|
|
|
1244
955
|
// src/runtime-layout.ts
|
|
1245
956
|
import { fileURLToPath } from "url";
|
|
@@ -1370,1847 +1081,77 @@ async function connectDaemon(browserFilter) {
|
|
|
1370
1081
|
remediation: {
|
|
1371
1082
|
code: "restart_unresponsive_broker",
|
|
1372
1083
|
message: "Stop the unresponsive Browser Pilot Broker explicitly, then retry. Live processes are never replaced automatically.",
|
|
1373
|
-
actionRequired: true
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
removeStaleBrokerFilesSync();
|
|
1378
|
-
const candidates = await discoverBrowserCandidates();
|
|
1379
|
-
const filter = browserFilter?.toLowerCase();
|
|
1380
|
-
const matches = candidates.filter(({ candidate }) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
|
|
1381
|
-
if (browserFilter && matches.length === 0) {
|
|
1382
|
-
throw new BrowserPilotError(
|
|
1383
|
-
"browser_not_found",
|
|
1384
|
-
"No installed supported browser matches the requested selection.",
|
|
1385
|
-
{
|
|
1386
|
-
remediation: {
|
|
1387
|
-
code: "select_supported_browser",
|
|
1388
|
-
message: "Run browser discovery and select one of the returned browser IDs or product names.",
|
|
1389
|
-
actionRequired: true
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
);
|
|
1393
|
-
}
|
|
1394
|
-
const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
|
|
1395
|
-
return await startDaemon(selected);
|
|
1396
|
-
} finally {
|
|
1397
|
-
startupLock.release();
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// src/bridge/daemon-bridge-backend.ts
|
|
1402
|
-
var DaemonBridgeBackend = class {
|
|
1403
|
-
constructor(browserFilter) {
|
|
1404
|
-
this.browserFilter = browserFilter;
|
|
1405
|
-
}
|
|
1406
|
-
client;
|
|
1407
|
-
async call(bridgeSessionId, method, params) {
|
|
1408
|
-
const client = await this.getClient();
|
|
1409
|
-
return client.brokerCall(bridgeSessionId, method, params);
|
|
1410
|
-
}
|
|
1411
|
-
async disconnect(bridgeSessionId) {
|
|
1412
|
-
if (!this.client) return;
|
|
1413
|
-
await this.client.brokerDisconnect(bridgeSessionId);
|
|
1414
|
-
}
|
|
1415
|
-
async *notifications(bridgeSessionId, signal) {
|
|
1416
|
-
const client = await this.getClient();
|
|
1417
|
-
while (!signal.aborted) {
|
|
1418
|
-
try {
|
|
1419
|
-
const notification = await client.brokerNextNotification(bridgeSessionId, 25e3, signal);
|
|
1420
|
-
if (notification) yield notification;
|
|
1421
|
-
} catch (error) {
|
|
1422
|
-
if (signal.aborted) return;
|
|
1423
|
-
throw error;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
async getClient() {
|
|
1428
|
-
if (!this.client) this.client = await connectDaemon(this.browserFilter);
|
|
1429
|
-
const health = await this.client.healthInfo();
|
|
1430
|
-
if (health.brokerProtocol !== 1) {
|
|
1431
|
-
throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from an older executable", {
|
|
1432
|
-
remediation: {
|
|
1433
|
-
code: "use_compatible_executable_or_isolate",
|
|
1434
|
-
message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
|
|
1435
|
-
actionRequired: true
|
|
1436
|
-
}
|
|
1437
|
-
});
|
|
1438
|
-
}
|
|
1439
|
-
return this.client;
|
|
1440
|
-
}
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
// src/bridge/stdio-bridge.ts
|
|
1444
|
-
import { once } from "events";
|
|
1445
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
1446
|
-
|
|
1447
|
-
// src/protocol/model.ts
|
|
1448
|
-
var CAPABILITIES = [
|
|
1449
|
-
"browser.discovery",
|
|
1450
|
-
"browser.control",
|
|
1451
|
-
"workspace.manage",
|
|
1452
|
-
"observation.read",
|
|
1453
|
-
"action.input",
|
|
1454
|
-
"artifact.read",
|
|
1455
|
-
"event.read",
|
|
1456
|
-
"network.observe",
|
|
1457
|
-
"network.modify",
|
|
1458
|
-
"auth.manage",
|
|
1459
|
-
"cookies.read",
|
|
1460
|
-
"developer.eval"
|
|
1461
|
-
];
|
|
1462
|
-
var OBSERVATION_TRUNCATION_REASONS = [
|
|
1463
|
-
"element_limit",
|
|
1464
|
-
"text_limit",
|
|
1465
|
-
"depth_limit",
|
|
1466
|
-
"byte_limit"
|
|
1467
|
-
];
|
|
1468
|
-
var OBSERVATION_V1_LIMITS = {
|
|
1469
|
-
defaultElements: 50,
|
|
1470
|
-
maxElements: 1e4,
|
|
1471
|
-
maxTitleCharacters: 4096,
|
|
1472
|
-
maxUrlCharacters: 16384,
|
|
1473
|
-
maxElementNameCharacters: 4096,
|
|
1474
|
-
maxElementValueCharacters: 65536,
|
|
1475
|
-
maxTextCharacters: 1e6,
|
|
1476
|
-
maxTreeDepth: 128,
|
|
1477
|
-
maxSerializedBytes: 2 * 1024 * 1024,
|
|
1478
|
-
ttlMs: 5 * 6e4,
|
|
1479
|
-
maxStoredObservations: 2048
|
|
1480
|
-
};
|
|
1481
|
-
var SENSITIVITIES = [
|
|
1482
|
-
"public",
|
|
1483
|
-
"browser_data",
|
|
1484
|
-
"credential",
|
|
1485
|
-
"user_file"
|
|
1486
|
-
];
|
|
1487
|
-
|
|
1488
|
-
// src/protocol/validation.ts
|
|
1489
|
-
var MAX_ID_LENGTH = 256;
|
|
1490
|
-
var MIN_NEGOTIATED_TRANSPORT_BYTES = 64 * 1024;
|
|
1491
|
-
var MAX_DECLARED_TRANSPORT_BYTES = 1024 * 1024 * 1024;
|
|
1492
|
-
function isRecord(value) {
|
|
1493
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1494
|
-
}
|
|
1495
|
-
function assertOnlyKeys(record, allowed, field = "params") {
|
|
1496
|
-
const allowedSet = new Set(allowed);
|
|
1497
|
-
const unknown = Object.keys(record).filter((key) => !allowedSet.has(key));
|
|
1498
|
-
if (unknown.length > 0) {
|
|
1499
|
-
throw invalidArgument(`Unknown ${field} field: ${unknown[0]}`, `${field}.${unknown[0]}`, field === "message" ? -32600 : -32602);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
function isJsonValue(value) {
|
|
1503
|
-
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
|
|
1504
|
-
return typeof value !== "number" || Number.isFinite(value);
|
|
1505
|
-
}
|
|
1506
|
-
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
1507
|
-
if (isRecord(value)) return Object.values(value).every(isJsonValue);
|
|
1508
|
-
return false;
|
|
1509
|
-
}
|
|
1510
|
-
function requireString(record, key, options = {}) {
|
|
1511
|
-
const value = record[key];
|
|
1512
|
-
const maxLength = options.maxLength ?? MAX_ID_LENGTH;
|
|
1513
|
-
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
|
|
1514
|
-
throw invalidArgument(`${key} must be a non-empty string no longer than ${maxLength} characters`, key);
|
|
1515
|
-
}
|
|
1516
|
-
if (options.pattern && !options.pattern.test(value)) {
|
|
1517
|
-
throw invalidArgument(`${key} has an invalid format`, key);
|
|
1518
|
-
}
|
|
1519
|
-
return value;
|
|
1520
|
-
}
|
|
1521
|
-
function parseId(value, allowNull) {
|
|
1522
|
-
if (allowNull && value === null) return null;
|
|
1523
|
-
if (typeof value === "string" && value.length > 0 && value.length <= MAX_ID_LENGTH) return value;
|
|
1524
|
-
if (typeof value === "number" && Number.isSafeInteger(value)) return value;
|
|
1525
|
-
throw invalidArgument("JSON-RPC id must be a non-empty string or safe integer", "id", -32600);
|
|
1526
|
-
}
|
|
1527
|
-
function parseParams(value) {
|
|
1528
|
-
if (value === void 0) return void 0;
|
|
1529
|
-
if (!Array.isArray(value) && !isRecord(value) || !isJsonValue(value)) {
|
|
1530
|
-
throw invalidArgument("JSON-RPC params must be a JSON object or array", "params", -32600);
|
|
1531
|
-
}
|
|
1532
|
-
return value;
|
|
1533
|
-
}
|
|
1534
|
-
function parseErrorObject(value) {
|
|
1535
|
-
if (!isRecord(value)) throw invalidArgument("JSON-RPC error must be an object", "error", -32600);
|
|
1536
|
-
if (!Number.isSafeInteger(value.code)) throw invalidArgument("JSON-RPC error.code must be an integer", "error.code", -32600);
|
|
1537
|
-
const message = requireString(value, "message", { maxLength: 4096 });
|
|
1538
|
-
if (value.data !== void 0 && !isJsonValue(value.data)) {
|
|
1539
|
-
throw invalidArgument("JSON-RPC error.data must be valid JSON", "error.data", -32600);
|
|
1540
|
-
}
|
|
1541
|
-
return { code: value.code, message, ...value.data !== void 0 ? { data: value.data } : {} };
|
|
1542
|
-
}
|
|
1543
|
-
function parseJsonRpcMessage(line, maxMessageBytes = 1024 * 1024) {
|
|
1544
|
-
if (Buffer.byteLength(line, "utf8") > maxMessageBytes) {
|
|
1545
|
-
throw new BrowserPilotError("result_too_large", `Protocol message exceeds ${maxMessageBytes} bytes`, {
|
|
1546
|
-
context: { maxMessageBytes },
|
|
1547
|
-
rpcCode: -32600
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
if (line.trim().length === 0) throw invalidArgument("Protocol message is empty", void 0, -32700);
|
|
1551
|
-
let parsed;
|
|
1552
|
-
try {
|
|
1553
|
-
parsed = JSON.parse(line);
|
|
1554
|
-
} catch (cause) {
|
|
1555
|
-
throw new BrowserPilotError("invalid_argument", "Invalid JSON", { rpcCode: -32700, cause });
|
|
1556
|
-
}
|
|
1557
|
-
if (!isRecord(parsed) || parsed.jsonrpc !== "2.0") {
|
|
1558
|
-
throw invalidArgument("Message must be a JSON-RPC 2.0 object", "jsonrpc", -32600);
|
|
1559
|
-
}
|
|
1560
|
-
if (typeof parsed.method === "string") {
|
|
1561
|
-
assertOnlyKeys(parsed, ["jsonrpc", "id", "method", "params"], "message");
|
|
1562
|
-
const method = requireString(parsed, "method", { maxLength: 256 });
|
|
1563
|
-
const params = parseParams(parsed.params);
|
|
1564
|
-
if (parsed.id === void 0) {
|
|
1565
|
-
return { jsonrpc: "2.0", method, ...params !== void 0 ? { params } : {} };
|
|
1566
|
-
}
|
|
1567
|
-
return {
|
|
1568
|
-
jsonrpc: "2.0",
|
|
1569
|
-
id: parseId(parsed.id, false),
|
|
1570
|
-
method,
|
|
1571
|
-
...params !== void 0 ? { params } : {}
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
if (parsed.id === void 0) {
|
|
1575
|
-
throw invalidArgument("JSON-RPC response must include id", "id", -32600);
|
|
1576
|
-
}
|
|
1577
|
-
const id = parseId(parsed.id, true);
|
|
1578
|
-
const hasResult = Object.hasOwn(parsed, "result");
|
|
1579
|
-
const hasError = Object.hasOwn(parsed, "error");
|
|
1580
|
-
if (hasResult === hasError) {
|
|
1581
|
-
throw invalidArgument("JSON-RPC response must contain exactly one of result or error", void 0, -32600);
|
|
1582
|
-
}
|
|
1583
|
-
if (hasResult) {
|
|
1584
|
-
assertOnlyKeys(parsed, ["jsonrpc", "id", "result"], "message");
|
|
1585
|
-
if (!isJsonValue(parsed.result)) throw invalidArgument("JSON-RPC result must be valid JSON", "result", -32600);
|
|
1586
|
-
return { jsonrpc: "2.0", id, result: parsed.result };
|
|
1587
|
-
}
|
|
1588
|
-
assertOnlyKeys(parsed, ["jsonrpc", "id", "error"], "message");
|
|
1589
|
-
return { jsonrpc: "2.0", id, error: parseErrorObject(parsed.error) };
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// src/services/broker-runtime.ts
|
|
1593
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
1594
|
-
|
|
1595
|
-
// src/protocol/schema.ts
|
|
1596
|
-
function isRecord2(value) {
|
|
1597
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1598
|
-
}
|
|
1599
|
-
function assertSchemaDefinition(schema, path = "$schema") {
|
|
1600
|
-
const sensitivity = schema["x-browser-pilot-sensitivity"];
|
|
1601
|
-
if (sensitivity) {
|
|
1602
|
-
if (sensitivity.length === 0 || new Set(sensitivity).size !== sensitivity.length) {
|
|
1603
|
-
throw new Error(`${path}.x-browser-pilot-sensitivity must be non-empty and unique`);
|
|
1604
|
-
}
|
|
1605
|
-
for (const value of sensitivity) {
|
|
1606
|
-
if (!SENSITIVITIES.includes(value)) {
|
|
1607
|
-
throw new Error(`${path}.x-browser-pilot-sensitivity contains unknown value ${value}`);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
if (schema.type === "object") {
|
|
1612
|
-
const properties = schema.properties ?? {};
|
|
1613
|
-
for (const required of schema.required ?? []) {
|
|
1614
|
-
if (!Object.hasOwn(properties, required)) {
|
|
1615
|
-
throw new Error(`${path}.required references unknown property ${required}`);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
for (const [name, property] of Object.entries(properties)) {
|
|
1619
|
-
assertSchemaDefinition(property, `${path}.properties.${name}`);
|
|
1620
|
-
}
|
|
1621
|
-
if (isRecord2(schema.additionalProperties)) {
|
|
1622
|
-
assertSchemaDefinition(schema.additionalProperties, `${path}.additionalProperties`);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
if (schema.type === "array" && schema.items) {
|
|
1626
|
-
assertSchemaDefinition(schema.items, `${path}.items`);
|
|
1627
|
-
}
|
|
1628
|
-
for (const [index, alternative] of (schema.oneOf ?? []).entries()) {
|
|
1629
|
-
assertSchemaDefinition(alternative, `${path}.oneOf[${index}]`);
|
|
1630
|
-
}
|
|
1631
|
-
if (schema.pattern) new RegExp(schema.pattern, "u");
|
|
1632
|
-
}
|
|
1633
|
-
var stringSchema = (options = {}) => ({ type: "string", ...options });
|
|
1634
|
-
var booleanSchema = (options = {}) => ({ type: "boolean", ...options });
|
|
1635
|
-
var integerSchema = (options = {}) => ({ type: "integer", ...options });
|
|
1636
|
-
var numberSchema = (options = {}) => ({ type: "number", ...options });
|
|
1637
|
-
var arraySchema = (items, options = {}) => ({
|
|
1638
|
-
type: "array",
|
|
1639
|
-
items,
|
|
1640
|
-
...options
|
|
1641
|
-
});
|
|
1642
|
-
var objectSchema = (properties, required = [], options = {}) => ({
|
|
1643
|
-
type: "object",
|
|
1644
|
-
properties,
|
|
1645
|
-
required,
|
|
1646
|
-
additionalProperties: false,
|
|
1647
|
-
...options
|
|
1648
|
-
});
|
|
1649
|
-
|
|
1650
|
-
// src/protocol/tools.ts
|
|
1651
|
-
var emptyInput = objectSchema({});
|
|
1652
|
-
function sensitive(schema, ...sensitivity) {
|
|
1653
|
-
return { ...schema, "x-browser-pilot-sensitivity": sensitivity };
|
|
1654
|
-
}
|
|
1655
|
-
var boundedText = sensitive(
|
|
1656
|
-
stringSchema({ minLength: 1, maxLength: 1e6 }),
|
|
1657
|
-
"browser_data",
|
|
1658
|
-
"credential"
|
|
1659
|
-
);
|
|
1660
|
-
var boundedSelector = sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data");
|
|
1661
|
-
var boundedUrl = sensitive(stringSchema({ minLength: 1, maxLength: 16384 }), "browser_data");
|
|
1662
|
-
var opaqueIdSchema = stringSchema({ minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$" });
|
|
1663
|
-
var headerSchema = objectSchema({
|
|
1664
|
-
name: sensitive(stringSchema({ minLength: 1, maxLength: 256 }), "browser_data"),
|
|
1665
|
-
value: sensitive(stringSchema({ maxLength: 8192 }), "browser_data", "credential")
|
|
1666
|
-
}, ["name", "value"]);
|
|
1667
|
-
var networkRuleSchema = objectSchema({
|
|
1668
|
-
ruleId: opaqueIdSchema,
|
|
1669
|
-
type: stringSchema({ enum: ["block", "mock", "headers"] }),
|
|
1670
|
-
pattern: boundedUrl,
|
|
1671
|
-
status: integerSchema({ minimum: 100, maximum: 999 }),
|
|
1672
|
-
headers: arraySchema(headerSchema, { maxItems: 256 }),
|
|
1673
|
-
bodySize: integerSchema({ minimum: 0 })
|
|
1674
|
-
}, ["ruleId", "type", "pattern"]);
|
|
1675
|
-
var networkRequestDetailSchema = objectSchema({
|
|
1676
|
-
requestId: opaqueIdSchema,
|
|
1677
|
-
sequence: integerSchema({ minimum: 1 }),
|
|
1678
|
-
method: stringSchema({ maxLength: 32 }),
|
|
1679
|
-
url: boundedUrl,
|
|
1680
|
-
type: stringSchema({ maxLength: 128 }),
|
|
1681
|
-
requestHeaders: arraySchema(headerSchema, { maxItems: 256 }),
|
|
1682
|
-
postData: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential"),
|
|
1683
|
-
postDataTruncated: booleanSchema(),
|
|
1684
|
-
status: integerSchema({ minimum: 100, maximum: 999 }),
|
|
1685
|
-
statusText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
1686
|
-
responseHeaders: arraySchema(headerSchema, { maxItems: 256 }),
|
|
1687
|
-
mimeType: stringSchema({ maxLength: 256 }),
|
|
1688
|
-
size: integerSchema({ minimum: 0 }),
|
|
1689
|
-
durationMs: numberSchema({ minimum: 0 }),
|
|
1690
|
-
error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
1691
|
-
bodyAvailable: booleanSchema()
|
|
1692
|
-
}, [
|
|
1693
|
-
"requestId",
|
|
1694
|
-
"sequence",
|
|
1695
|
-
"method",
|
|
1696
|
-
"url",
|
|
1697
|
-
"type",
|
|
1698
|
-
"requestHeaders",
|
|
1699
|
-
"postDataTruncated",
|
|
1700
|
-
"bodyAvailable"
|
|
1701
|
-
]);
|
|
1702
|
-
var elementSchema = objectSchema({
|
|
1703
|
-
ref: integerSchema({ minimum: 1 }),
|
|
1704
|
-
role: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
1705
|
-
name: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementNameCharacters }), "browser_data"),
|
|
1706
|
-
value: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementValueCharacters }), "browser_data", "credential"),
|
|
1707
|
-
checked: booleanSchema()
|
|
1708
|
-
}, ["ref", "role", "name"]);
|
|
1709
|
-
var inputEvidenceSchema = objectSchema({
|
|
1710
|
-
action: stringSchema({ enum: ["type", "keyboard"] }),
|
|
1711
|
-
status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
|
|
1712
|
-
kind: stringSchema({ enum: ["input", "contenteditable", "unsupported"] }),
|
|
1713
|
-
sensitive: booleanSchema(),
|
|
1714
|
-
beforeLength: integerSchema({ minimum: 0 }),
|
|
1715
|
-
expectedLength: integerSchema({ minimum: 0 }),
|
|
1716
|
-
afterLength: integerSchema({ minimum: 0 }),
|
|
1717
|
-
reason: stringSchema({ enum: ["active_element_not_readable", "value_mismatch"] })
|
|
1718
|
-
}, ["action", "status", "kind", "sensitive"]);
|
|
1719
|
-
var clickEvidenceSchema = objectSchema({
|
|
1720
|
-
action: stringSchema({ const: "click" }),
|
|
1721
|
-
status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
|
|
1722
|
-
kind: stringSchema({ enum: [
|
|
1723
|
-
"checkbox",
|
|
1724
|
-
"radio",
|
|
1725
|
-
"switch",
|
|
1726
|
-
"option",
|
|
1727
|
-
"select",
|
|
1728
|
-
"control",
|
|
1729
|
-
"other",
|
|
1730
|
-
"coordinates"
|
|
1731
|
-
] }),
|
|
1732
|
-
effects: arraySchema(stringSchema({ enum: [
|
|
1733
|
-
"checked_changed",
|
|
1734
|
-
"selected_changed",
|
|
1735
|
-
"pressed_changed",
|
|
1736
|
-
"expanded_changed",
|
|
1737
|
-
"focus_changed",
|
|
1738
|
-
"navigation",
|
|
1739
|
-
"document_changed",
|
|
1740
|
-
"dialog_opened",
|
|
1741
|
-
"popup_opened"
|
|
1742
|
-
] }), { maxItems: 9, uniqueItems: true }),
|
|
1743
|
-
checked: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
|
|
1744
|
-
selected: booleanSchema(),
|
|
1745
|
-
pressed: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
|
|
1746
|
-
expanded: booleanSchema(),
|
|
1747
|
-
focused: booleanSchema(),
|
|
1748
|
-
reason: stringSchema({ enum: [
|
|
1749
|
-
"coordinate_target",
|
|
1750
|
-
"target_unavailable",
|
|
1751
|
-
"expected_state_unchanged",
|
|
1752
|
-
"no_observable_effect"
|
|
1753
|
-
] })
|
|
1754
|
-
}, ["action", "status", "kind", "effects"]);
|
|
1755
|
-
var pressEvidenceSchema = objectSchema({
|
|
1756
|
-
action: stringSchema({ const: "press" }),
|
|
1757
|
-
status: stringSchema({ enum: ["verified", "unavailable"] }),
|
|
1758
|
-
kind: stringSchema({ enum: [
|
|
1759
|
-
"input",
|
|
1760
|
-
"contenteditable",
|
|
1761
|
-
"checkbox",
|
|
1762
|
-
"radio",
|
|
1763
|
-
"select",
|
|
1764
|
-
"control",
|
|
1765
|
-
"other"
|
|
1766
|
-
] }),
|
|
1767
|
-
effects: arraySchema(stringSchema({ enum: [
|
|
1768
|
-
"value_changed",
|
|
1769
|
-
"checked_changed",
|
|
1770
|
-
"selected_changed",
|
|
1771
|
-
"pressed_changed",
|
|
1772
|
-
"expanded_changed",
|
|
1773
|
-
"focus_changed",
|
|
1774
|
-
"navigation",
|
|
1775
|
-
"document_changed",
|
|
1776
|
-
"dialog_opened",
|
|
1777
|
-
"popup_opened"
|
|
1778
|
-
] }), { maxItems: 10, uniqueItems: true }),
|
|
1779
|
-
sensitive: booleanSchema(),
|
|
1780
|
-
reason: stringSchema({ enum: ["target_unavailable", "no_observable_effect"] })
|
|
1781
|
-
}, ["action", "status", "kind", "effects", "sensitive"]);
|
|
1782
|
-
var uploadEvidenceSchema = objectSchema({
|
|
1783
|
-
action: stringSchema({ const: "upload" }),
|
|
1784
|
-
status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
|
|
1785
|
-
expectedFileCount: integerSchema({ const: 1 }),
|
|
1786
|
-
fileCount: integerSchema({ minimum: 0 }),
|
|
1787
|
-
nameMatched: booleanSchema(),
|
|
1788
|
-
reason: stringSchema({ enum: [
|
|
1789
|
-
"target_unavailable",
|
|
1790
|
-
"file_count_mismatch",
|
|
1791
|
-
"file_name_mismatch"
|
|
1792
|
-
] })
|
|
1793
|
-
}, ["action", "status", "expectedFileCount"]);
|
|
1794
|
-
var scrollEvidenceSchema = objectSchema({
|
|
1795
|
-
action: stringSchema({ const: "scroll" }),
|
|
1796
|
-
status: stringSchema({ enum: ["verified", "mismatch"] }),
|
|
1797
|
-
mode: stringSchema({ enum: ["relative", "position", "text"] }),
|
|
1798
|
-
target: stringSchema({ enum: ["page", "element", "text"] }),
|
|
1799
|
-
moved: booleanSchema(),
|
|
1800
|
-
deltaX: numberSchema(),
|
|
1801
|
-
deltaY: numberSchema(),
|
|
1802
|
-
beforeX: numberSchema({ minimum: 0 }),
|
|
1803
|
-
beforeY: numberSchema({ minimum: 0 }),
|
|
1804
|
-
afterX: numberSchema({ minimum: 0 }),
|
|
1805
|
-
afterY: numberSchema({ minimum: 0 }),
|
|
1806
|
-
matchedText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
1807
|
-
reason: stringSchema({ enum: ["at_boundary", "text_not_found", "text_not_revealed"] })
|
|
1808
|
-
}, [
|
|
1809
|
-
"action",
|
|
1810
|
-
"status",
|
|
1811
|
-
"mode",
|
|
1812
|
-
"target",
|
|
1813
|
-
"moved",
|
|
1814
|
-
"deltaX",
|
|
1815
|
-
"deltaY",
|
|
1816
|
-
"beforeX",
|
|
1817
|
-
"beforeY",
|
|
1818
|
-
"afterX",
|
|
1819
|
-
"afterY"
|
|
1820
|
-
]);
|
|
1821
|
-
var dropdownOptionSchema = objectSchema({
|
|
1822
|
-
index: integerSchema({ minimum: 1, maximum: 500 }),
|
|
1823
|
-
label: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
1824
|
-
value: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
1825
|
-
selected: booleanSchema(),
|
|
1826
|
-
disabled: booleanSchema()
|
|
1827
|
-
}, ["index", "label", "value", "selected", "disabled"]);
|
|
1828
|
-
var selectEvidenceSchema = objectSchema({
|
|
1829
|
-
action: stringSchema({ const: "select" }),
|
|
1830
|
-
status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
|
|
1831
|
-
kind: stringSchema({ enum: ["native", "aria"] }),
|
|
1832
|
-
selected: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
|
|
1833
|
-
reason: stringSchema({ enum: ["option_not_found", "selection_mismatch", "selection_not_exposed"] })
|
|
1834
|
-
}, ["action", "status", "kind", "selected"]);
|
|
1835
|
-
var artifactSchema = objectSchema({
|
|
1836
|
-
id: opaqueIdSchema,
|
|
1837
|
-
workspaceId: opaqueIdSchema,
|
|
1838
|
-
kind: stringSchema({ enum: ["screenshot", "screenshot_preview", "pdf", "download", "upload_input"] }),
|
|
1839
|
-
mimeType: stringSchema({ minLength: 1, maxLength: 256 }),
|
|
1840
|
-
byteSize: integerSchema({ minimum: 0 }),
|
|
1841
|
-
fileName: stringSchema({ minLength: 1, maxLength: 4096 }),
|
|
1842
|
-
width: integerSchema({ minimum: 1 }),
|
|
1843
|
-
height: integerSchema({ minimum: 1 }),
|
|
1844
|
-
sensitivity: stringSchema({ enum: ["public", "browser_data", "credential", "user_file"] }),
|
|
1845
|
-
createdAt: integerSchema({ minimum: 0 }),
|
|
1846
|
-
expiresAt: integerSchema({ minimum: 0 }),
|
|
1847
|
-
retained: booleanSchema(),
|
|
1848
|
-
previewOf: opaqueIdSchema
|
|
1849
|
-
}, ["id", "workspaceId", "kind", "mimeType", "byteSize", "sensitivity", "createdAt", "expiresAt", "retained"]);
|
|
1850
|
-
var hintRefsSchema = arraySchema(integerSchema({ minimum: 1 }), {
|
|
1851
|
-
maxItems: 32,
|
|
1852
|
-
uniqueItems: true
|
|
1853
|
-
});
|
|
1854
|
-
var agentHintSchema = {
|
|
1855
|
-
oneOf: [
|
|
1856
|
-
objectSchema({
|
|
1857
|
-
code: stringSchema({ const: "autocomplete" }),
|
|
1858
|
-
source: stringSchema({ const: "observation" }),
|
|
1859
|
-
confidence: stringSchema({ enum: ["strong", "possible"] }),
|
|
1860
|
-
recommendedAction: stringSchema({ const: "observe_then_select" }),
|
|
1861
|
-
refs: hintRefsSchema
|
|
1862
|
-
}, ["code", "source", "confidence", "recommendedAction", "refs"]),
|
|
1863
|
-
objectSchema({
|
|
1864
|
-
code: stringSchema({ const: "modal_overlay" }),
|
|
1865
|
-
source: stringSchema({ const: "observation" }),
|
|
1866
|
-
confidence: stringSchema({ enum: ["strong", "possible"] }),
|
|
1867
|
-
recommendedAction: stringSchema({ const: "resolve_overlay_first" }),
|
|
1868
|
-
blocking: booleanSchema(),
|
|
1869
|
-
refs: hintRefsSchema
|
|
1870
|
-
}, ["code", "source", "confidence", "recommendedAction", "blocking", "refs"]),
|
|
1871
|
-
objectSchema({
|
|
1872
|
-
code: stringSchema({ const: "filter_controls" }),
|
|
1873
|
-
source: stringSchema({ const: "observation" }),
|
|
1874
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1875
|
-
recommendedAction: stringSchema({ const: "review_refinement_controls" }),
|
|
1876
|
-
refs: hintRefsSchema
|
|
1877
|
-
}, ["code", "source", "confidence", "recommendedAction", "refs"]),
|
|
1878
|
-
objectSchema({
|
|
1879
|
-
code: stringSchema({ const: "access_blocked" }),
|
|
1880
|
-
source: stringSchema({ const: "network" }),
|
|
1881
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1882
|
-
recommendedAction: stringSchema({ const: "avoid_same_navigation_retry" }),
|
|
1883
|
-
status: integerSchema({ enum: [403, 429] })
|
|
1884
|
-
}, ["code", "source", "confidence", "recommendedAction", "status"]),
|
|
1885
|
-
objectSchema({
|
|
1886
|
-
code: stringSchema({ const: "authentication_surface" }),
|
|
1887
|
-
source: stringSchema({ const: "observation" }),
|
|
1888
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1889
|
-
recommendedAction: stringSchema({ const: "inspect_authentication_state" }),
|
|
1890
|
-
state: stringSchema({ enum: ["present", "entered", "left"] })
|
|
1891
|
-
}, ["code", "source", "confidence", "recommendedAction", "state"]),
|
|
1892
|
-
objectSchema({
|
|
1893
|
-
code: stringSchema({ const: "download" }),
|
|
1894
|
-
source: stringSchema({ const: "download" }),
|
|
1895
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1896
|
-
recommendedAction: stringSchema({ const: "wait_for_download" }),
|
|
1897
|
-
state: stringSchema({ const: "started" })
|
|
1898
|
-
}, ["code", "source", "confidence", "recommendedAction", "state"]),
|
|
1899
|
-
objectSchema({
|
|
1900
|
-
code: stringSchema({ const: "download" }),
|
|
1901
|
-
source: stringSchema({ const: "download" }),
|
|
1902
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1903
|
-
recommendedAction: stringSchema({ const: "inspect_download_artifact" }),
|
|
1904
|
-
state: stringSchema({ const: "completed" }),
|
|
1905
|
-
artifactId: opaqueIdSchema
|
|
1906
|
-
}, ["code", "source", "confidence", "recommendedAction", "state", "artifactId"]),
|
|
1907
|
-
objectSchema({
|
|
1908
|
-
code: stringSchema({ const: "download" }),
|
|
1909
|
-
source: stringSchema({ const: "download" }),
|
|
1910
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1911
|
-
recommendedAction: stringSchema({ const: "inspect_download_failure" }),
|
|
1912
|
-
state: stringSchema({ enum: ["failed", "cancelled"] }),
|
|
1913
|
-
reason: stringSchema({ minLength: 1, maxLength: 128 })
|
|
1914
|
-
}, ["code", "source", "confidence", "recommendedAction", "state", "reason"]),
|
|
1915
|
-
objectSchema({
|
|
1916
|
-
code: stringSchema({ const: "repeated_action" }),
|
|
1917
|
-
source: stringSchema({ const: "watchdog" }),
|
|
1918
|
-
confidence: stringSchema({ const: "strong" }),
|
|
1919
|
-
recommendedAction: stringSchema({ const: "change_strategy" }),
|
|
1920
|
-
streak: integerSchema({ minimum: 1, maximum: 1e6 }),
|
|
1921
|
-
reason: stringSchema({ minLength: 1, maxLength: 128 })
|
|
1922
|
-
}, ["code", "source", "confidence", "recommendedAction", "streak", "reason"])
|
|
1923
|
-
]
|
|
1924
|
-
};
|
|
1925
|
-
function resultSchema(context, properties = {}, required = []) {
|
|
1926
|
-
const contextProperties = {};
|
|
1927
|
-
const contextRequired = [];
|
|
1928
|
-
if (context === "workspace" || context === "target") {
|
|
1929
|
-
contextProperties.workspaceId = opaqueIdSchema;
|
|
1930
|
-
contextProperties.leaseId = opaqueIdSchema;
|
|
1931
|
-
contextRequired.push("workspaceId", "leaseId");
|
|
1932
|
-
}
|
|
1933
|
-
if (context === "target") {
|
|
1934
|
-
contextProperties.targetId = opaqueIdSchema;
|
|
1935
|
-
contextProperties.profileContextId = opaqueIdSchema;
|
|
1936
|
-
contextProperties.url = boundedUrl;
|
|
1937
|
-
contextRequired.push("targetId", "url");
|
|
1938
|
-
}
|
|
1939
|
-
return objectSchema({ ...contextProperties, ...properties }, [...contextRequired, ...required]);
|
|
1940
|
-
}
|
|
1941
|
-
var observationOutput = resultSchema("target", {
|
|
1942
|
-
observationId: opaqueIdSchema,
|
|
1943
|
-
title: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxTitleCharacters }), "browser_data"),
|
|
1944
|
-
page: objectSchema({
|
|
1945
|
-
viewportWidth: integerSchema({ minimum: 0 }),
|
|
1946
|
-
viewportHeight: integerSchema({ minimum: 0 }),
|
|
1947
|
-
documentWidth: integerSchema({ minimum: 0 }),
|
|
1948
|
-
documentHeight: integerSchema({ minimum: 0 }),
|
|
1949
|
-
scrollX: integerSchema({ minimum: 0 }),
|
|
1950
|
-
scrollY: integerSchema({ minimum: 0 }),
|
|
1951
|
-
pixelsAbove: integerSchema({ minimum: 0 }),
|
|
1952
|
-
pixelsBelow: integerSchema({ minimum: 0 }),
|
|
1953
|
-
pixelsLeft: integerSchema({ minimum: 0 }),
|
|
1954
|
-
pixelsRight: integerSchema({ minimum: 0 }),
|
|
1955
|
-
scrollPercentX: numberSchema({ minimum: 0, maximum: 100 }),
|
|
1956
|
-
scrollPercentY: numberSchema({ minimum: 0, maximum: 100 })
|
|
1957
|
-
}, [
|
|
1958
|
-
"viewportWidth",
|
|
1959
|
-
"viewportHeight",
|
|
1960
|
-
"documentWidth",
|
|
1961
|
-
"documentHeight",
|
|
1962
|
-
"scrollX",
|
|
1963
|
-
"scrollY",
|
|
1964
|
-
"pixelsAbove",
|
|
1965
|
-
"pixelsBelow",
|
|
1966
|
-
"pixelsLeft",
|
|
1967
|
-
"pixelsRight",
|
|
1968
|
-
"scrollPercentX",
|
|
1969
|
-
"scrollPercentY"
|
|
1970
|
-
]),
|
|
1971
|
-
elements: arraySchema(elementSchema, { maxItems: OBSERVATION_V1_LIMITS.maxElements }),
|
|
1972
|
-
truncated: booleanSchema(),
|
|
1973
|
-
truncationReasons: arraySchema(stringSchema({
|
|
1974
|
-
enum: [...OBSERVATION_TRUNCATION_REASONS]
|
|
1975
|
-
}), { uniqueItems: true }),
|
|
1976
|
-
hints: arraySchema(agentHintSchema, { maxItems: 16 }),
|
|
1977
|
-
evidence: { oneOf: [
|
|
1978
|
-
inputEvidenceSchema,
|
|
1979
|
-
clickEvidenceSchema,
|
|
1980
|
-
pressEvidenceSchema,
|
|
1981
|
-
uploadEvidenceSchema,
|
|
1982
|
-
scrollEvidenceSchema,
|
|
1983
|
-
selectEvidenceSchema
|
|
1984
|
-
] }
|
|
1985
|
-
}, ["observationId", "title", "elements", "truncated", "truncationReasons"]);
|
|
1986
|
-
var artifactOutput = resultSchema("target", {
|
|
1987
|
-
artifact: artifactSchema,
|
|
1988
|
-
preview: artifactSchema,
|
|
1989
|
-
annotationCount: integerSchema({ minimum: 0, maximum: 200 })
|
|
1990
|
-
}, ["artifact"]);
|
|
1991
|
-
var elementAddressSchema = {
|
|
1992
|
-
oneOf: [
|
|
1993
|
-
objectSchema({
|
|
1994
|
-
observationId: opaqueIdSchema,
|
|
1995
|
-
ref: integerSchema({ minimum: 1 })
|
|
1996
|
-
}, ["observationId", "ref"]),
|
|
1997
|
-
objectSchema({ selector: boundedSelector }, ["selector"])
|
|
1998
|
-
]
|
|
1999
|
-
};
|
|
2000
|
-
var dropdownChoiceSchema = {
|
|
2001
|
-
oneOf: [
|
|
2002
|
-
objectSchema({ by: stringSchema({ const: "index" }), index: integerSchema({ minimum: 1, maximum: 500 }) }, ["by", "index"]),
|
|
2003
|
-
objectSchema({
|
|
2004
|
-
by: stringSchema({ const: "label" }),
|
|
2005
|
-
label: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
|
|
2006
|
-
exact: booleanSchema()
|
|
2007
|
-
}, ["by", "label"]),
|
|
2008
|
-
objectSchema({
|
|
2009
|
-
by: stringSchema({ const: "value" }),
|
|
2010
|
-
value: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
|
|
2011
|
-
exact: booleanSchema()
|
|
2012
|
-
}, ["by", "value"])
|
|
2013
|
-
]
|
|
2014
|
-
};
|
|
2015
|
-
var profileRepresentativeTabSchema = objectSchema({
|
|
2016
|
-
targetId: opaqueIdSchema,
|
|
2017
|
-
title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2018
|
-
url: boundedUrl
|
|
2019
|
-
}, ["targetId", "title", "url"]);
|
|
2020
|
-
var profileContextSchema = objectSchema({
|
|
2021
|
-
profileContextId: opaqueIdSchema,
|
|
2022
|
-
label: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
2023
|
-
displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
|
|
2024
|
-
tabCount: integerSchema({ minimum: 0 }),
|
|
2025
|
-
eligibleTabCount: integerSchema({ minimum: 0 }),
|
|
2026
|
-
selected: booleanSchema(),
|
|
2027
|
-
representativeTabs: arraySchema(profileRepresentativeTabSchema, { maxItems: 3 })
|
|
2028
|
-
}, [
|
|
2029
|
-
"profileContextId",
|
|
2030
|
-
"label",
|
|
2031
|
-
"tabCount",
|
|
2032
|
-
"eligibleTabCount",
|
|
2033
|
-
"selected",
|
|
2034
|
-
"representativeTabs"
|
|
2035
|
-
]);
|
|
2036
|
-
function tool(definition) {
|
|
2037
|
-
return definition;
|
|
2038
|
-
}
|
|
2039
|
-
var TOOL_DEFINITIONS = [
|
|
2040
|
-
tool({
|
|
2041
|
-
name: "browser.discover",
|
|
2042
|
-
title: "Discover browsers",
|
|
2043
|
-
description: "List supported local browsers and structured setup or authorization state.",
|
|
2044
|
-
context: "connection",
|
|
2045
|
-
inputSchema: objectSchema({ browser: stringSchema({ minLength: 1, maxLength: 128 }) }),
|
|
2046
|
-
outputSchema: resultSchema("connection", {
|
|
2047
|
-
browsers: arraySchema(objectSchema({
|
|
2048
|
-
id: opaqueIdSchema,
|
|
2049
|
-
product: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
2050
|
-
channel: stringSchema({ maxLength: 128 }),
|
|
2051
|
-
profile: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2052
|
-
processState: stringSchema({ enum: ["running", "not_running", "unknown"] }),
|
|
2053
|
-
remoteDebuggingState: stringSchema({ enum: ["enabled", "disabled", "stale"] }),
|
|
2054
|
-
authorizationState: stringSchema({ enum: ["authorized", "required", "not_applicable", "unknown"] }),
|
|
2055
|
-
state: stringSchema({ enum: ["ready", "not_running", "remote_debugging_disabled", "authorization_required", "disconnected"] }),
|
|
2056
|
-
remediation: objectSchema({
|
|
2057
|
-
code: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
2058
|
-
message: stringSchema({ minLength: 1, maxLength: 4096 }),
|
|
2059
|
-
actionRequired: booleanSchema()
|
|
2060
|
-
}, ["code", "message", "actionRequired"])
|
|
2061
|
-
}, [
|
|
2062
|
-
"id",
|
|
2063
|
-
"product",
|
|
2064
|
-
"processState",
|
|
2065
|
-
"remoteDebuggingState",
|
|
2066
|
-
"authorizationState",
|
|
2067
|
-
"state"
|
|
2068
|
-
]))
|
|
2069
|
-
}, ["browsers"]),
|
|
2070
|
-
requiredCapabilities: ["browser.discovery"],
|
|
2071
|
-
mutating: false,
|
|
2072
|
-
idempotency: "read_only",
|
|
2073
|
-
cancellation: "not_applicable",
|
|
2074
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2075
|
-
artifactKinds: []
|
|
2076
|
-
}),
|
|
2077
|
-
tool({
|
|
2078
|
-
name: "browser.connect",
|
|
2079
|
-
title: "Connect browser",
|
|
2080
|
-
description: "Connect a Workspace to a selected authorized browser instance.",
|
|
2081
|
-
context: "workspace",
|
|
2082
|
-
inputSchema: objectSchema({ browserId: opaqueIdSchema }, ["browserId"]),
|
|
2083
|
-
outputSchema: resultSchema("workspace", {
|
|
2084
|
-
browserInstanceId: opaqueIdSchema,
|
|
2085
|
-
connectionGeneration: integerSchema({ minimum: 1 }),
|
|
2086
|
-
state: stringSchema({ const: "connected" })
|
|
2087
|
-
}, ["browserInstanceId", "connectionGeneration", "state"]),
|
|
2088
|
-
requiredCapabilities: ["browser.control"],
|
|
2089
|
-
mutating: true,
|
|
2090
|
-
idempotency: "idempotent",
|
|
2091
|
-
cancellation: "before_dispatch",
|
|
2092
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2093
|
-
artifactKinds: []
|
|
2094
|
-
}),
|
|
2095
|
-
tool({
|
|
2096
|
-
name: "browser.profiles.list",
|
|
2097
|
-
title: "List browser Profiles",
|
|
2098
|
-
description: "Passively list live Profile contexts and bounded representative tabs.",
|
|
2099
|
-
context: "workspace",
|
|
2100
|
-
inputSchema: emptyInput,
|
|
2101
|
-
outputSchema: resultSchema("workspace", {
|
|
2102
|
-
profiles: arraySchema(profileContextSchema, { maxItems: 128 })
|
|
2103
|
-
}, ["profiles"]),
|
|
2104
|
-
requiredCapabilities: ["browser.control"],
|
|
2105
|
-
mutating: false,
|
|
2106
|
-
idempotency: "read_only",
|
|
2107
|
-
cancellation: "not_applicable",
|
|
2108
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2109
|
-
artifactKinds: []
|
|
2110
|
-
}),
|
|
2111
|
-
tool({
|
|
2112
|
-
name: "browser.profiles.select",
|
|
2113
|
-
title: "Select browser Profile",
|
|
2114
|
-
description: "Select one current Profile context for new managed targets in this Workspace.",
|
|
2115
|
-
context: "workspace",
|
|
2116
|
-
inputSchema: objectSchema({ profileContextId: opaqueIdSchema }, ["profileContextId"]),
|
|
2117
|
-
outputSchema: resultSchema("workspace", {
|
|
2118
|
-
profileContextId: opaqueIdSchema,
|
|
2119
|
-
label: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
2120
|
-
displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data")
|
|
2121
|
-
}, ["profileContextId", "label"]),
|
|
2122
|
-
requiredCapabilities: ["browser.control"],
|
|
2123
|
-
mutating: true,
|
|
2124
|
-
idempotency: "idempotent",
|
|
2125
|
-
cancellation: "before_dispatch",
|
|
2126
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2127
|
-
artifactKinds: []
|
|
2128
|
-
}),
|
|
2129
|
-
tool({
|
|
2130
|
-
name: "browser.open",
|
|
2131
|
-
title: "Open URL",
|
|
2132
|
-
description: "Navigate an authorized controlled target or create a new target in the ManagedTabSet.",
|
|
2133
|
-
context: "workspace",
|
|
2134
|
-
inputSchema: objectSchema({
|
|
2135
|
-
url: boundedUrl,
|
|
2136
|
-
targetId: opaqueIdSchema,
|
|
2137
|
-
newTarget: booleanSchema(),
|
|
2138
|
-
profileContextId: opaqueIdSchema,
|
|
2139
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2140
|
-
}, ["url"]),
|
|
2141
|
-
outputSchema: observationOutput,
|
|
2142
|
-
requiredCapabilities: ["browser.control", "observation.read"],
|
|
2143
|
-
mutating: true,
|
|
2144
|
-
idempotency: "non_idempotent",
|
|
2145
|
-
cancellation: "best_effort",
|
|
2146
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2147
|
-
artifactKinds: []
|
|
2148
|
-
}),
|
|
2149
|
-
tool({
|
|
2150
|
-
name: "browser.observe",
|
|
2151
|
-
title: "Observe page",
|
|
2152
|
-
description: "Create an immutable bounded Observation with numbered refs.",
|
|
2153
|
-
context: "target",
|
|
2154
|
-
inputSchema: objectSchema({
|
|
2155
|
-
limit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2156
|
-
}),
|
|
2157
|
-
outputSchema: observationOutput,
|
|
2158
|
-
requiredCapabilities: ["observation.read"],
|
|
2159
|
-
mutating: false,
|
|
2160
|
-
idempotency: "read_only",
|
|
2161
|
-
cancellation: "best_effort",
|
|
2162
|
-
sensitivity: { input: [], output: ["browser_data", "credential"] },
|
|
2163
|
-
artifactKinds: []
|
|
2164
|
-
}),
|
|
2165
|
-
tool({
|
|
2166
|
-
name: "browser.observation.latest",
|
|
2167
|
-
title: "Get latest Observation",
|
|
2168
|
-
description: "Return the latest live Observation descriptor for a controlled target.",
|
|
2169
|
-
context: "target",
|
|
2170
|
-
inputSchema: emptyInput,
|
|
2171
|
-
outputSchema: resultSchema("target", {
|
|
2172
|
-
observationId: opaqueIdSchema,
|
|
2173
|
-
createdAt: integerSchema({ minimum: 0 }),
|
|
2174
|
-
expiresAt: integerSchema({ minimum: 0 }),
|
|
2175
|
-
elementCount: integerSchema({ minimum: 0, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2176
|
-
}, ["observationId", "createdAt", "expiresAt", "elementCount"]),
|
|
2177
|
-
requiredCapabilities: ["observation.read"],
|
|
2178
|
-
mutating: false,
|
|
2179
|
-
idempotency: "read_only",
|
|
2180
|
-
cancellation: "not_applicable",
|
|
2181
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2182
|
-
artifactKinds: []
|
|
2183
|
-
}),
|
|
2184
|
-
tool({
|
|
2185
|
-
name: "browser.locate",
|
|
2186
|
-
title: "Locate element",
|
|
2187
|
-
description: "Return viewport coordinates for a CSS selector in the selected frame.",
|
|
2188
|
-
context: "target",
|
|
2189
|
-
inputSchema: objectSchema({ selector: boundedSelector }, ["selector"]),
|
|
2190
|
-
outputSchema: resultSchema("target", {
|
|
2191
|
-
x: numberSchema(),
|
|
2192
|
-
y: numberSchema(),
|
|
2193
|
-
top: numberSchema(),
|
|
2194
|
-
left: numberSchema(),
|
|
2195
|
-
width: numberSchema({ minimum: 0 }),
|
|
2196
|
-
height: numberSchema({ minimum: 0 })
|
|
2197
|
-
}, ["x", "y", "top", "left", "width", "height"]),
|
|
2198
|
-
requiredCapabilities: ["observation.read"],
|
|
2199
|
-
mutating: false,
|
|
2200
|
-
idempotency: "read_only",
|
|
2201
|
-
cancellation: "best_effort",
|
|
2202
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2203
|
-
artifactKinds: []
|
|
2204
|
-
}),
|
|
2205
|
-
tool({
|
|
2206
|
-
name: "browser.read",
|
|
2207
|
-
title: "Read page text",
|
|
2208
|
-
description: "Read bounded text from the page or a selector.",
|
|
2209
|
-
context: "target",
|
|
2210
|
-
inputSchema: objectSchema({
|
|
2211
|
-
selector: boundedSelector,
|
|
2212
|
-
limit: integerSchema({ minimum: 1, maximum: 1e6 })
|
|
2213
|
-
}),
|
|
2214
|
-
outputSchema: resultSchema("target", {
|
|
2215
|
-
title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2216
|
-
text: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data"),
|
|
2217
|
-
length: integerSchema({ minimum: 0 }),
|
|
2218
|
-
truncated: booleanSchema()
|
|
2219
|
-
}, ["title", "text", "length", "truncated"]),
|
|
2220
|
-
requiredCapabilities: ["observation.read"],
|
|
2221
|
-
mutating: false,
|
|
2222
|
-
idempotency: "read_only",
|
|
2223
|
-
cancellation: "best_effort",
|
|
2224
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2225
|
-
artifactKinds: []
|
|
2226
|
-
}),
|
|
2227
|
-
tool({
|
|
2228
|
-
name: "browser.search",
|
|
2229
|
-
title: "Search page text",
|
|
2230
|
-
description: "Find bounded visible text matches without returning the entire page.",
|
|
2231
|
-
context: "target",
|
|
2232
|
-
inputSchema: objectSchema({
|
|
2233
|
-
query: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
|
|
2234
|
-
selector: boundedSelector,
|
|
2235
|
-
caseSensitive: booleanSchema(),
|
|
2236
|
-
wholeWord: booleanSchema(),
|
|
2237
|
-
limit: integerSchema({ minimum: 1, maximum: 200 })
|
|
2238
|
-
}, ["query"]),
|
|
2239
|
-
outputSchema: resultSchema("target", {
|
|
2240
|
-
title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2241
|
-
totalMatches: integerSchema({ minimum: 0 }),
|
|
2242
|
-
matches: arraySchema(objectSchema({
|
|
2243
|
-
index: integerSchema({ minimum: 1 }),
|
|
2244
|
-
text: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2245
|
-
context: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
|
|
2246
|
-
tagName: stringSchema({ maxLength: 64 }),
|
|
2247
|
-
visible: booleanSchema(),
|
|
2248
|
-
x: numberSchema(),
|
|
2249
|
-
y: numberSchema(),
|
|
2250
|
-
width: numberSchema({ minimum: 0 }),
|
|
2251
|
-
height: numberSchema({ minimum: 0 })
|
|
2252
|
-
}, ["index", "text", "context", "tagName", "visible", "x", "y", "width", "height"]), { maxItems: 200 }),
|
|
2253
|
-
truncated: booleanSchema()
|
|
2254
|
-
}, ["title", "totalMatches", "matches", "truncated"]),
|
|
2255
|
-
requiredCapabilities: ["observation.read"],
|
|
2256
|
-
mutating: false,
|
|
2257
|
-
idempotency: "read_only",
|
|
2258
|
-
cancellation: "best_effort",
|
|
2259
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2260
|
-
artifactKinds: []
|
|
2261
|
-
}),
|
|
2262
|
-
tool({
|
|
2263
|
-
name: "browser.elements.find",
|
|
2264
|
-
title: "Find DOM elements",
|
|
2265
|
-
description: "Run a bounded CSS query and return safe element metadata without exposing DOM handles.",
|
|
2266
|
-
context: "target",
|
|
2267
|
-
inputSchema: objectSchema({
|
|
2268
|
-
selector: boundedSelector,
|
|
2269
|
-
limit: integerSchema({ minimum: 1, maximum: 200 }),
|
|
2270
|
-
attributeNames: arraySchema(
|
|
2271
|
-
sensitive(stringSchema({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z_:][A-Za-z0-9_.:-]*$" }), "browser_data"),
|
|
2272
|
-
{ maxItems: 20, uniqueItems: true }
|
|
2273
|
-
),
|
|
2274
|
-
pierceShadow: booleanSchema()
|
|
2275
|
-
}, ["selector"]),
|
|
2276
|
-
outputSchema: resultSchema("target", {
|
|
2277
|
-
title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2278
|
-
totalMatches: integerSchema({ minimum: 0 }),
|
|
2279
|
-
elements: arraySchema(objectSchema({
|
|
2280
|
-
index: integerSchema({ minimum: 1 }),
|
|
2281
|
-
tagName: stringSchema({ maxLength: 64 }),
|
|
2282
|
-
role: stringSchema({ maxLength: 128 }),
|
|
2283
|
-
name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2284
|
-
text: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
|
|
2285
|
-
visible: booleanSchema(),
|
|
2286
|
-
enabled: booleanSchema(),
|
|
2287
|
-
x: numberSchema(),
|
|
2288
|
-
y: numberSchema(),
|
|
2289
|
-
width: numberSchema({ minimum: 0 }),
|
|
2290
|
-
height: numberSchema({ minimum: 0 }),
|
|
2291
|
-
attributes: arraySchema(objectSchema({
|
|
2292
|
-
name: sensitive(stringSchema({ maxLength: 128 }), "browser_data"),
|
|
2293
|
-
value: sensitive(stringSchema({ maxLength: 2048 }), "browser_data", "credential")
|
|
2294
|
-
}, ["name", "value"]), { maxItems: 20 })
|
|
2295
|
-
}, [
|
|
2296
|
-
"index",
|
|
2297
|
-
"tagName",
|
|
2298
|
-
"role",
|
|
2299
|
-
"name",
|
|
2300
|
-
"text",
|
|
2301
|
-
"visible",
|
|
2302
|
-
"enabled",
|
|
2303
|
-
"x",
|
|
2304
|
-
"y",
|
|
2305
|
-
"width",
|
|
2306
|
-
"height",
|
|
2307
|
-
"attributes"
|
|
2308
|
-
]), { maxItems: 200 }),
|
|
2309
|
-
truncated: booleanSchema()
|
|
2310
|
-
}, ["title", "totalMatches", "elements", "truncated"]),
|
|
2311
|
-
requiredCapabilities: ["observation.read"],
|
|
2312
|
-
mutating: false,
|
|
2313
|
-
idempotency: "read_only",
|
|
2314
|
-
cancellation: "best_effort",
|
|
2315
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2316
|
-
artifactKinds: []
|
|
2317
|
-
}),
|
|
2318
|
-
tool({
|
|
2319
|
-
name: "browser.scroll",
|
|
2320
|
-
title: "Scroll page or element",
|
|
2321
|
-
description: "Scroll the page, a ref, a CSS-selected container, or visible text and return a fresh Observation.",
|
|
2322
|
-
context: "target",
|
|
2323
|
-
inputSchema: objectSchema({
|
|
2324
|
-
target: elementAddressSchema,
|
|
2325
|
-
direction: stringSchema({ enum: ["up", "down", "left", "right"] }),
|
|
2326
|
-
amount: numberSchema({ minimum: 0.01, maximum: 1e5 }),
|
|
2327
|
-
unit: stringSchema({ enum: ["pixels", "viewport"] }),
|
|
2328
|
-
position: stringSchema({ enum: ["start", "end"] }),
|
|
2329
|
-
text: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
|
|
2330
|
-
exact: booleanSchema(),
|
|
2331
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2332
|
-
}),
|
|
2333
|
-
outputSchema: observationOutput,
|
|
2334
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2335
|
-
mutating: true,
|
|
2336
|
-
idempotency: "non_idempotent",
|
|
2337
|
-
cancellation: "best_effort",
|
|
2338
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2339
|
-
artifactKinds: []
|
|
2340
|
-
}),
|
|
2341
|
-
tool({
|
|
2342
|
-
name: "browser.dropdown.options",
|
|
2343
|
-
title: "List dropdown options",
|
|
2344
|
-
description: "Enumerate bounded native or currently exposed ARIA dropdown options.",
|
|
2345
|
-
context: "target",
|
|
2346
|
-
inputSchema: objectSchema({ target: elementAddressSchema }, ["target"]),
|
|
2347
|
-
outputSchema: resultSchema("target", {
|
|
2348
|
-
kind: stringSchema({ enum: ["native", "aria"] }),
|
|
2349
|
-
expanded: booleanSchema(),
|
|
2350
|
-
multiple: booleanSchema(),
|
|
2351
|
-
requiresOpen: booleanSchema(),
|
|
2352
|
-
options: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
|
|
2353
|
-
truncated: booleanSchema()
|
|
2354
|
-
}, ["kind", "expanded", "multiple", "requiresOpen", "options", "truncated"]),
|
|
2355
|
-
requiredCapabilities: ["observation.read"],
|
|
2356
|
-
mutating: false,
|
|
2357
|
-
idempotency: "read_only",
|
|
2358
|
-
cancellation: "best_effort",
|
|
2359
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2360
|
-
artifactKinds: []
|
|
2361
|
-
}),
|
|
2362
|
-
tool({
|
|
2363
|
-
name: "browser.dropdown.select",
|
|
2364
|
-
title: "Select dropdown option",
|
|
2365
|
-
description: "Select and verify a native or ARIA dropdown option, opening custom controls when needed.",
|
|
2366
|
-
context: "target",
|
|
2367
|
-
inputSchema: objectSchema({
|
|
2368
|
-
target: elementAddressSchema,
|
|
2369
|
-
choice: dropdownChoiceSchema,
|
|
2370
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2371
|
-
}, ["target", "choice"]),
|
|
2372
|
-
outputSchema: observationOutput,
|
|
2373
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2374
|
-
mutating: true,
|
|
2375
|
-
idempotency: "non_idempotent",
|
|
2376
|
-
cancellation: "best_effort",
|
|
2377
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2378
|
-
artifactKinds: []
|
|
2379
|
-
}),
|
|
2380
|
-
tool({
|
|
2381
|
-
name: "browser.click",
|
|
2382
|
-
title: "Click element",
|
|
2383
|
-
description: "Click an Observation ref or viewport coordinates after interactability checks.",
|
|
2384
|
-
context: "target",
|
|
2385
|
-
inputSchema: objectSchema({
|
|
2386
|
-
target: {
|
|
2387
|
-
oneOf: [
|
|
2388
|
-
objectSchema({ observationId: opaqueIdSchema, ref: integerSchema({ minimum: 1 }) }, ["observationId", "ref"]),
|
|
2389
|
-
objectSchema({ x: numberSchema(), y: numberSchema() }, ["x", "y"])
|
|
2390
|
-
]
|
|
2391
|
-
},
|
|
2392
|
-
button: stringSchema({ enum: ["left", "right"] }),
|
|
2393
|
-
clickCount: integerSchema({ minimum: 1, maximum: 2 }),
|
|
2394
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2395
|
-
}, ["target"]),
|
|
2396
|
-
outputSchema: observationOutput,
|
|
2397
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2398
|
-
mutating: true,
|
|
2399
|
-
idempotency: "non_idempotent",
|
|
2400
|
-
cancellation: "best_effort",
|
|
2401
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2402
|
-
artifactKinds: []
|
|
2403
|
-
}),
|
|
2404
|
-
tool({
|
|
2405
|
-
name: "browser.type",
|
|
2406
|
-
title: "Type into element",
|
|
2407
|
-
description: "Enter text into an Observation ref and read back the effective value.",
|
|
2408
|
-
context: "target",
|
|
2409
|
-
inputSchema: objectSchema({
|
|
2410
|
-
observationId: opaqueIdSchema,
|
|
2411
|
-
ref: integerSchema({ minimum: 1 }),
|
|
2412
|
-
text: boundedText,
|
|
2413
|
-
clear: booleanSchema(),
|
|
2414
|
-
submit: booleanSchema(),
|
|
2415
|
-
verification: stringSchema({ enum: ["report", "require_exact"] }),
|
|
2416
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2417
|
-
}, ["observationId", "ref", "text"]),
|
|
2418
|
-
outputSchema: observationOutput,
|
|
2419
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2420
|
-
mutating: true,
|
|
2421
|
-
idempotency: "non_idempotent",
|
|
2422
|
-
cancellation: "best_effort",
|
|
2423
|
-
sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
|
|
2424
|
-
artifactKinds: []
|
|
2425
|
-
}),
|
|
2426
|
-
tool({
|
|
2427
|
-
name: "browser.keyboard",
|
|
2428
|
-
title: "Type with keyboard",
|
|
2429
|
-
description: "Dispatch keyboard input to the currently focused page control.",
|
|
2430
|
-
context: "target",
|
|
2431
|
-
inputSchema: objectSchema({
|
|
2432
|
-
text: boundedText,
|
|
2433
|
-
clear: booleanSchema(),
|
|
2434
|
-
submit: booleanSchema(),
|
|
2435
|
-
delayMs: integerSchema({ minimum: 0, maximum: 6e4 }),
|
|
2436
|
-
focusSelector: boundedSelector,
|
|
2437
|
-
verification: stringSchema({ enum: ["report", "require_exact"] }),
|
|
2438
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2439
|
-
}, ["text"]),
|
|
2440
|
-
outputSchema: observationOutput,
|
|
2441
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2442
|
-
mutating: true,
|
|
2443
|
-
idempotency: "non_idempotent",
|
|
2444
|
-
cancellation: "best_effort",
|
|
2445
|
-
sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
|
|
2446
|
-
artifactKinds: []
|
|
2447
|
-
}),
|
|
2448
|
-
tool({
|
|
2449
|
-
name: "browser.press",
|
|
2450
|
-
title: "Press key",
|
|
2451
|
-
description: "Dispatch a key or key combination and report bounded observable effects.",
|
|
2452
|
-
context: "target",
|
|
2453
|
-
inputSchema: objectSchema({
|
|
2454
|
-
key: stringSchema({ minLength: 1, maxLength: 128 }),
|
|
2455
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2456
|
-
}, ["key"]),
|
|
2457
|
-
outputSchema: observationOutput,
|
|
2458
|
-
requiredCapabilities: ["action.input", "observation.read"],
|
|
2459
|
-
mutating: true,
|
|
2460
|
-
idempotency: "non_idempotent",
|
|
2461
|
-
cancellation: "best_effort",
|
|
2462
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2463
|
-
artifactKinds: []
|
|
2464
|
-
}),
|
|
2465
|
-
tool({
|
|
2466
|
-
name: "browser.capture",
|
|
2467
|
-
title: "Capture screenshot",
|
|
2468
|
-
description: "Capture the viewport, full page, or a selected element as protected Artifacts.",
|
|
2469
|
-
context: "target",
|
|
2470
|
-
inputSchema: objectSchema({
|
|
2471
|
-
fullPage: booleanSchema(),
|
|
2472
|
-
selector: boundedSelector,
|
|
2473
|
-
includeOriginal: booleanSchema(),
|
|
2474
|
-
annotations: objectSchema({
|
|
2475
|
-
observationId: opaqueIdSchema,
|
|
2476
|
-
refs: arraySchema(integerSchema({ minimum: 1 }), { maxItems: 200, uniqueItems: true })
|
|
2477
|
-
}, ["observationId"])
|
|
2478
|
-
}),
|
|
2479
|
-
outputSchema: artifactOutput,
|
|
2480
|
-
requiredCapabilities: ["artifact.read"],
|
|
2481
|
-
mutating: false,
|
|
2482
|
-
idempotency: "idempotent",
|
|
2483
|
-
cancellation: "best_effort",
|
|
2484
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2485
|
-
artifactKinds: ["screenshot", "screenshot_preview"]
|
|
2486
|
-
}),
|
|
2487
|
-
tool({
|
|
2488
|
-
name: "browser.pdf",
|
|
2489
|
-
title: "Capture PDF",
|
|
2490
|
-
description: "Print the controlled target to a protected PDF Artifact.",
|
|
2491
|
-
context: "target",
|
|
2492
|
-
inputSchema: objectSchema({ landscape: booleanSchema() }),
|
|
2493
|
-
outputSchema: artifactOutput,
|
|
2494
|
-
requiredCapabilities: ["artifact.read"],
|
|
2495
|
-
mutating: false,
|
|
2496
|
-
idempotency: "idempotent",
|
|
2497
|
-
cancellation: "best_effort",
|
|
2498
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2499
|
-
artifactKinds: ["pdf"]
|
|
2500
|
-
}),
|
|
2501
|
-
tool({
|
|
2502
|
-
name: "browser.upload",
|
|
2503
|
-
title: "Upload file",
|
|
2504
|
-
description: "Assign a client-authorized local file and verify the browser selection.",
|
|
2505
|
-
context: "target",
|
|
2506
|
-
inputSchema: {
|
|
2507
|
-
oneOf: [
|
|
2508
|
-
objectSchema({
|
|
2509
|
-
artifactId: sensitive(opaqueIdSchema, "user_file"),
|
|
2510
|
-
observationId: opaqueIdSchema,
|
|
2511
|
-
ref: integerSchema({ minimum: 1 }),
|
|
2512
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2513
|
-
}, ["artifactId", "observationId", "ref"]),
|
|
2514
|
-
objectSchema({
|
|
2515
|
-
artifactId: sensitive(opaqueIdSchema, "user_file"),
|
|
2516
|
-
inputIndex: integerSchema({ minimum: 1 }),
|
|
2517
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2518
|
-
}, ["artifactId", "inputIndex"]),
|
|
2519
|
-
objectSchema({
|
|
2520
|
-
artifactId: sensitive(opaqueIdSchema, "user_file"),
|
|
2521
|
-
observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
|
|
2522
|
-
}, ["artifactId"])
|
|
2523
|
-
]
|
|
2524
|
-
},
|
|
2525
|
-
outputSchema: observationOutput,
|
|
2526
|
-
requiredCapabilities: ["action.input", "artifact.read", "observation.read"],
|
|
2527
|
-
mutating: true,
|
|
2528
|
-
idempotency: "non_idempotent",
|
|
2529
|
-
cancellation: "best_effort",
|
|
2530
|
-
sensitivity: { input: ["user_file"], output: ["browser_data", "credential"] },
|
|
2531
|
-
artifactKinds: []
|
|
2532
|
-
}),
|
|
2533
|
-
tool({
|
|
2534
|
-
name: "browser.tabs.list",
|
|
2535
|
-
title: "List browser tabs",
|
|
2536
|
-
description: "List ManagedTabSet targets and all eligible user tabs in the connected browser.",
|
|
2537
|
-
context: "workspace",
|
|
2538
|
-
inputSchema: objectSchema({
|
|
2539
|
-
scope: stringSchema({ enum: ["all", "managed_only", "user_tabs"] })
|
|
2540
|
-
}),
|
|
2541
|
-
outputSchema: resultSchema("workspace", {
|
|
2542
|
-
targets: arraySchema(objectSchema({
|
|
2543
|
-
targetId: opaqueIdSchema,
|
|
2544
|
-
profileContextId: opaqueIdSchema,
|
|
2545
|
-
title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2546
|
-
url: boundedUrl,
|
|
2547
|
-
active: booleanSchema(),
|
|
2548
|
-
origin: stringSchema({ enum: ["managed", "managed_popup", "user_tab"] }),
|
|
2549
|
-
managedTabSetId: opaqueIdSchema,
|
|
2550
|
-
controlState: stringSchema({ enum: ["available", "controlled", "busy"] })
|
|
2551
|
-
}, ["targetId", "title", "url", "active", "origin", "controlState"]))
|
|
2552
|
-
}, ["targets"]),
|
|
2553
|
-
requiredCapabilities: ["browser.control"],
|
|
2554
|
-
mutating: false,
|
|
2555
|
-
idempotency: "read_only",
|
|
2556
|
-
cancellation: "not_applicable",
|
|
2557
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2558
|
-
artifactKinds: []
|
|
2559
|
-
}),
|
|
2560
|
-
tool({
|
|
2561
|
-
name: "browser.tabs.switch",
|
|
2562
|
-
title: "Switch tab",
|
|
2563
|
-
description: "Set a managed or user tab as the active controlled target for the current Lease.",
|
|
2564
|
-
context: "workspace",
|
|
2565
|
-
inputSchema: objectSchema({ targetId: opaqueIdSchema }, ["targetId"]),
|
|
2566
|
-
outputSchema: resultSchema("target"),
|
|
2567
|
-
requiredCapabilities: ["browser.control"],
|
|
2568
|
-
mutating: true,
|
|
2569
|
-
idempotency: "idempotent",
|
|
2570
|
-
cancellation: "before_dispatch",
|
|
2571
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2572
|
-
artifactKinds: []
|
|
2573
|
-
}),
|
|
2574
|
-
tool({
|
|
2575
|
-
name: "browser.tabs.close",
|
|
2576
|
-
title: "Close tab",
|
|
2577
|
-
description: "Close one explicitly addressed managed or user tab.",
|
|
2578
|
-
context: "target",
|
|
2579
|
-
inputSchema: emptyInput,
|
|
2580
|
-
outputSchema: resultSchema("workspace", { closedTargetId: opaqueIdSchema }, ["closedTargetId"]),
|
|
2581
|
-
requiredCapabilities: ["browser.control"],
|
|
2582
|
-
mutating: true,
|
|
2583
|
-
idempotency: "idempotent",
|
|
2584
|
-
cancellation: "before_dispatch",
|
|
2585
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2586
|
-
artifactKinds: []
|
|
2587
|
-
}),
|
|
2588
|
-
tool({
|
|
2589
|
-
name: "browser.tabs.release",
|
|
2590
|
-
title: "Release tab control",
|
|
2591
|
-
description: "Relinquish this Lease control of one tab without closing it.",
|
|
2592
|
-
context: "target",
|
|
2593
|
-
inputSchema: emptyInput,
|
|
2594
|
-
outputSchema: resultSchema("target", { released: booleanSchema() }, ["released"]),
|
|
2595
|
-
requiredCapabilities: ["browser.control"],
|
|
2596
|
-
mutating: true,
|
|
2597
|
-
idempotency: "idempotent",
|
|
2598
|
-
cancellation: "before_dispatch",
|
|
2599
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2600
|
-
artifactKinds: []
|
|
2601
|
-
}),
|
|
2602
|
-
tool({
|
|
2603
|
-
name: "browser.frames.list",
|
|
2604
|
-
title: "List frames",
|
|
2605
|
-
description: "List frames and CDP sessions belonging to the controlled target.",
|
|
2606
|
-
context: "target",
|
|
2607
|
-
inputSchema: emptyInput,
|
|
2608
|
-
outputSchema: resultSchema("target", {
|
|
2609
|
-
frames: arraySchema(objectSchema({
|
|
2610
|
-
frameId: opaqueIdSchema,
|
|
2611
|
-
parentFrameId: opaqueIdSchema,
|
|
2612
|
-
url: boundedUrl,
|
|
2613
|
-
name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
|
|
2614
|
-
}, ["frameId", "url", "name"]))
|
|
2615
|
-
}, ["frames"]),
|
|
2616
|
-
requiredCapabilities: ["observation.read"],
|
|
2617
|
-
mutating: false,
|
|
2618
|
-
idempotency: "read_only",
|
|
2619
|
-
cancellation: "not_applicable",
|
|
2620
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2621
|
-
artifactKinds: []
|
|
2622
|
-
}),
|
|
2623
|
-
tool({
|
|
2624
|
-
name: "browser.frames.switch",
|
|
2625
|
-
title: "Switch frame",
|
|
2626
|
-
description: "Set the active frame for the current Lease after target-access validation.",
|
|
2627
|
-
context: "target",
|
|
2628
|
-
inputSchema: {
|
|
2629
|
-
oneOf: [
|
|
2630
|
-
objectSchema({ frameId: opaqueIdSchema }, ["frameId"]),
|
|
2631
|
-
objectSchema({ top: booleanSchema({ const: true }) }, ["top"])
|
|
2632
|
-
]
|
|
2633
|
-
},
|
|
2634
|
-
outputSchema: resultSchema("target", { frameId: opaqueIdSchema }, ["frameId"]),
|
|
2635
|
-
requiredCapabilities: ["browser.control"],
|
|
2636
|
-
mutating: true,
|
|
2637
|
-
idempotency: "idempotent",
|
|
2638
|
-
cancellation: "before_dispatch",
|
|
2639
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2640
|
-
artifactKinds: []
|
|
2641
|
-
}),
|
|
2642
|
-
tool({
|
|
2643
|
-
name: "browser.dialogs.list",
|
|
2644
|
-
title: "List dialogs",
|
|
2645
|
-
description: "List pending dialogs for controlled targets without auto-accepting them.",
|
|
2646
|
-
context: "workspace",
|
|
2647
|
-
inputSchema: emptyInput,
|
|
2648
|
-
outputSchema: resultSchema("workspace", {
|
|
2649
|
-
dialogs: arraySchema(objectSchema({
|
|
2650
|
-
dialogId: opaqueIdSchema,
|
|
2651
|
-
targetId: opaqueIdSchema,
|
|
2652
|
-
type: stringSchema({ enum: ["alert", "confirm", "prompt", "beforeunload"] }),
|
|
2653
|
-
message: sensitive(stringSchema({ maxLength: 65536 }), "browser_data")
|
|
2654
|
-
}, ["dialogId", "targetId", "type", "message"]))
|
|
2655
|
-
}, ["dialogs"]),
|
|
2656
|
-
requiredCapabilities: ["event.read"],
|
|
2657
|
-
mutating: false,
|
|
2658
|
-
idempotency: "read_only",
|
|
2659
|
-
cancellation: "not_applicable",
|
|
2660
|
-
sensitivity: { input: [], output: ["browser_data"] },
|
|
2661
|
-
artifactKinds: []
|
|
2662
|
-
}),
|
|
2663
|
-
tool({
|
|
2664
|
-
name: "browser.dialogs.respond",
|
|
2665
|
-
title: "Respond to dialog",
|
|
2666
|
-
description: "Explicitly accept or dismiss a pending dialog on a controlled target.",
|
|
2667
|
-
context: "target",
|
|
2668
|
-
inputSchema: objectSchema({
|
|
2669
|
-
dialogId: opaqueIdSchema,
|
|
2670
|
-
action: stringSchema({ enum: ["accept", "dismiss"] }),
|
|
2671
|
-
promptText: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential")
|
|
2672
|
-
}, ["dialogId", "action"]),
|
|
2673
|
-
outputSchema: resultSchema("target", { dialogId: opaqueIdSchema, action: stringSchema({ enum: ["accept", "dismiss"] }) }, ["dialogId", "action"]),
|
|
2674
|
-
requiredCapabilities: ["action.input"],
|
|
2675
|
-
mutating: true,
|
|
2676
|
-
idempotency: "idempotent",
|
|
2677
|
-
cancellation: "before_dispatch",
|
|
2678
|
-
sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
|
|
2679
|
-
artifactKinds: []
|
|
2680
|
-
}),
|
|
2681
|
-
tool({
|
|
2682
|
-
name: "browser.auth.set",
|
|
2683
|
-
title: "Set HTTP auth",
|
|
2684
|
-
description: "Set Workspace-scoped HTTP authentication credentials.",
|
|
2685
|
-
context: "workspace",
|
|
2686
|
-
inputSchema: objectSchema({
|
|
2687
|
-
username: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "credential"),
|
|
2688
|
-
password: sensitive(stringSchema({ maxLength: 65536 }), "credential")
|
|
2689
|
-
}, ["username", "password"]),
|
|
2690
|
-
outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: true }) }, ["configured"]),
|
|
2691
|
-
requiredCapabilities: ["auth.manage"],
|
|
2692
|
-
mutating: true,
|
|
2693
|
-
idempotency: "idempotent",
|
|
2694
|
-
cancellation: "before_dispatch",
|
|
2695
|
-
sensitivity: { input: ["credential"], output: [] },
|
|
2696
|
-
artifactKinds: []
|
|
2697
|
-
}),
|
|
2698
|
-
tool({
|
|
2699
|
-
name: "browser.auth.clear",
|
|
2700
|
-
title: "Clear HTTP auth",
|
|
2701
|
-
description: "Remove Workspace-scoped HTTP authentication credentials.",
|
|
2702
|
-
context: "workspace",
|
|
2703
|
-
inputSchema: emptyInput,
|
|
2704
|
-
outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: false }) }, ["configured"]),
|
|
2705
|
-
requiredCapabilities: ["auth.manage"],
|
|
2706
|
-
mutating: true,
|
|
2707
|
-
idempotency: "idempotent",
|
|
2708
|
-
cancellation: "before_dispatch",
|
|
2709
|
-
sensitivity: { input: [], output: [] },
|
|
2710
|
-
artifactKinds: []
|
|
2711
|
-
}),
|
|
2712
|
-
tool({
|
|
2713
|
-
name: "browser.cookies.list",
|
|
2714
|
-
title: "List cookies",
|
|
2715
|
-
description: "List cookies available to a controlled target with sensitivity metadata.",
|
|
2716
|
-
context: "target",
|
|
2717
|
-
inputSchema: objectSchema({
|
|
2718
|
-
domain: sensitive(stringSchema({ minLength: 1, maxLength: 2048 }), "browser_data")
|
|
2719
|
-
}),
|
|
2720
|
-
outputSchema: resultSchema("target", {
|
|
2721
|
-
cookies: arraySchema(objectSchema({
|
|
2722
|
-
name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2723
|
-
value: sensitive(stringSchema({ maxLength: 1e6 }), "credential"),
|
|
2724
|
-
domain: sensitive(stringSchema({ maxLength: 2048 }), "browser_data"),
|
|
2725
|
-
path: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
|
|
2726
|
-
httpOnly: booleanSchema(),
|
|
2727
|
-
secure: booleanSchema(),
|
|
2728
|
-
expires: numberSchema()
|
|
2729
|
-
}, ["name", "value", "domain", "path", "httpOnly", "secure", "expires"]))
|
|
2730
|
-
}, ["cookies"]),
|
|
2731
|
-
requiredCapabilities: ["cookies.read"],
|
|
2732
|
-
mutating: false,
|
|
2733
|
-
idempotency: "read_only",
|
|
2734
|
-
cancellation: "best_effort",
|
|
2735
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2736
|
-
artifactKinds: []
|
|
2737
|
-
}),
|
|
2738
|
-
tool({
|
|
2739
|
-
name: "browser.network.requests",
|
|
2740
|
-
title: "List network requests",
|
|
2741
|
-
description: "List bounded Workspace-scoped network request metadata.",
|
|
2742
|
-
context: "workspace",
|
|
2743
|
-
inputSchema: objectSchema({
|
|
2744
|
-
limit: integerSchema({ minimum: 1, maximum: 1e3 }),
|
|
2745
|
-
after: integerSchema({ minimum: 0 }),
|
|
2746
|
-
url: sensitive(stringSchema({ maxLength: 16384 }), "browser_data"),
|
|
2747
|
-
method: stringSchema({ maxLength: 32 }),
|
|
2748
|
-
status: stringSchema({ maxLength: 16 }),
|
|
2749
|
-
type: arraySchema(stringSchema({ minLength: 1, maxLength: 128 }), { uniqueItems: true })
|
|
2750
|
-
}),
|
|
2751
|
-
outputSchema: resultSchema("workspace", {
|
|
2752
|
-
requests: arraySchema(objectSchema({
|
|
2753
|
-
requestId: opaqueIdSchema,
|
|
2754
|
-
sequence: integerSchema({ minimum: 1 }),
|
|
2755
|
-
method: stringSchema({ maxLength: 32 }),
|
|
2756
|
-
url: boundedUrl,
|
|
2757
|
-
status: integerSchema({ minimum: 100, maximum: 999 }),
|
|
2758
|
-
type: stringSchema({ maxLength: 128 }),
|
|
2759
|
-
size: integerSchema({ minimum: 0 }),
|
|
2760
|
-
durationMs: numberSchema({ minimum: 0 }),
|
|
2761
|
-
error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
|
|
2762
|
-
}, ["requestId", "sequence", "method", "url", "type"])),
|
|
2763
|
-
nextCursor: integerSchema({ minimum: 0 }),
|
|
2764
|
-
truncated: booleanSchema()
|
|
2765
|
-
}, ["requests", "nextCursor", "truncated"]),
|
|
2766
|
-
requiredCapabilities: ["network.observe"],
|
|
2767
|
-
mutating: false,
|
|
2768
|
-
idempotency: "read_only",
|
|
2769
|
-
cancellation: "best_effort",
|
|
2770
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data"] },
|
|
2771
|
-
artifactKinds: []
|
|
2772
|
-
}),
|
|
2773
|
-
tool({
|
|
2774
|
-
name: "browser.network.request",
|
|
2775
|
-
title: "Read network request",
|
|
2776
|
-
description: "Read bounded details for one Workspace-scoped request.",
|
|
2777
|
-
context: "workspace",
|
|
2778
|
-
inputSchema: objectSchema({ requestId: opaqueIdSchema, includeBody: booleanSchema() }, ["requestId"]),
|
|
2779
|
-
outputSchema: resultSchema("workspace", {
|
|
2780
|
-
request: networkRequestDetailSchema,
|
|
2781
|
-
body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential"),
|
|
2782
|
-
bodyEncoding: stringSchema({ enum: ["utf8", "base64"] }),
|
|
2783
|
-
mimeType: stringSchema({ maxLength: 256 }),
|
|
2784
|
-
bodyTruncated: booleanSchema()
|
|
2785
|
-
}, ["request", "bodyTruncated"]),
|
|
2786
|
-
requiredCapabilities: ["network.observe"],
|
|
2787
|
-
mutating: false,
|
|
2788
|
-
idempotency: "read_only",
|
|
2789
|
-
cancellation: "best_effort",
|
|
2790
|
-
sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
|
|
2791
|
-
artifactKinds: []
|
|
2792
|
-
}),
|
|
2793
|
-
tool({
|
|
2794
|
-
name: "browser.network.clear",
|
|
2795
|
-
title: "Clear network journal",
|
|
2796
|
-
description: "Clear captured request metadata for the current Workspace.",
|
|
2797
|
-
context: "workspace",
|
|
2798
|
-
inputSchema: emptyInput,
|
|
2799
|
-
outputSchema: resultSchema("workspace", { cleared: booleanSchema({ const: true }) }, ["cleared"]),
|
|
2800
|
-
requiredCapabilities: ["network.observe"],
|
|
2801
|
-
mutating: true,
|
|
2802
|
-
idempotency: "idempotent",
|
|
2803
|
-
cancellation: "before_dispatch",
|
|
2804
|
-
sensitivity: { input: [], output: [] },
|
|
2805
|
-
artifactKinds: []
|
|
2806
|
-
}),
|
|
2807
|
-
tool({
|
|
2808
|
-
name: "browser.network.rules.list",
|
|
2809
|
-
title: "List network rules",
|
|
2810
|
-
description: "List interception rules owned by the current Workspace.",
|
|
2811
|
-
context: "workspace",
|
|
2812
|
-
inputSchema: emptyInput,
|
|
2813
|
-
outputSchema: resultSchema("workspace", {
|
|
2814
|
-
rules: arraySchema(networkRuleSchema)
|
|
2815
|
-
}, ["rules"]),
|
|
2816
|
-
requiredCapabilities: ["network.observe"],
|
|
2817
|
-
mutating: false,
|
|
2818
|
-
idempotency: "read_only",
|
|
2819
|
-
cancellation: "not_applicable",
|
|
2820
|
-
sensitivity: { input: [], output: ["browser_data", "credential"] },
|
|
2821
|
-
artifactKinds: []
|
|
2822
|
-
}),
|
|
2823
|
-
tool({
|
|
2824
|
-
name: "browser.network.rules.add",
|
|
2825
|
-
title: "Add network rule",
|
|
2826
|
-
description: "Add a Workspace-scoped block, mock, or header rule.",
|
|
2827
|
-
context: "workspace",
|
|
2828
|
-
inputSchema: {
|
|
2829
|
-
oneOf: [
|
|
2830
|
-
objectSchema({ type: stringSchema({ const: "block" }), pattern: boundedUrl }, ["type", "pattern"]),
|
|
2831
|
-
objectSchema({
|
|
2832
|
-
type: stringSchema({ const: "mock" }),
|
|
2833
|
-
pattern: boundedUrl,
|
|
2834
|
-
status: integerSchema({ minimum: 100, maximum: 999 }),
|
|
2835
|
-
headers: arraySchema(headerSchema, { maxItems: 256 }),
|
|
2836
|
-
body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential")
|
|
2837
|
-
}, ["type", "pattern"]),
|
|
2838
|
-
objectSchema({
|
|
2839
|
-
type: stringSchema({ const: "headers" }),
|
|
2840
|
-
pattern: boundedUrl,
|
|
2841
|
-
headers: arraySchema(headerSchema, { minItems: 1, maxItems: 256 })
|
|
2842
|
-
}, ["type", "pattern", "headers"])
|
|
2843
|
-
]
|
|
2844
|
-
},
|
|
2845
|
-
outputSchema: resultSchema("workspace", { ruleId: opaqueIdSchema }, ["ruleId"]),
|
|
2846
|
-
requiredCapabilities: ["network.modify"],
|
|
2847
|
-
mutating: true,
|
|
2848
|
-
idempotency: "non_idempotent",
|
|
2849
|
-
cancellation: "before_dispatch",
|
|
2850
|
-
sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
|
|
2851
|
-
artifactKinds: []
|
|
2852
|
-
}),
|
|
2853
|
-
tool({
|
|
2854
|
-
name: "browser.network.rules.remove",
|
|
2855
|
-
title: "Remove network rule",
|
|
2856
|
-
description: "Remove one owned interception rule or all rules in the Workspace.",
|
|
2857
|
-
context: "workspace",
|
|
2858
|
-
inputSchema: {
|
|
2859
|
-
oneOf: [
|
|
2860
|
-
objectSchema({ ruleId: opaqueIdSchema }, ["ruleId"]),
|
|
2861
|
-
objectSchema({ all: booleanSchema({ const: true }) }, ["all"])
|
|
2862
|
-
]
|
|
2863
|
-
},
|
|
2864
|
-
outputSchema: resultSchema("workspace", { removed: integerSchema({ minimum: 0 }) }, ["removed"]),
|
|
2865
|
-
requiredCapabilities: ["network.modify"],
|
|
2866
|
-
mutating: true,
|
|
2867
|
-
idempotency: "idempotent",
|
|
2868
|
-
cancellation: "before_dispatch",
|
|
2869
|
-
sensitivity: { input: ["browser_data"], output: [] },
|
|
2870
|
-
artifactKinds: []
|
|
2871
|
-
}),
|
|
2872
|
-
tool({
|
|
2873
|
-
name: "browser.eval",
|
|
2874
|
-
title: "Evaluate JavaScript",
|
|
2875
|
-
description: "Evaluate JavaScript in a controlled target when the Agent host exposes developer capability.",
|
|
2876
|
-
context: "target",
|
|
2877
|
-
inputSchema: objectSchema({ expression: boundedText, awaitPromise: booleanSchema() }, ["expression"]),
|
|
2878
|
-
outputSchema: resultSchema("target", {
|
|
2879
|
-
value: sensitive({}, "browser_data", "credential"),
|
|
2880
|
-
truncated: booleanSchema()
|
|
2881
|
-
}, ["value", "truncated"]),
|
|
2882
|
-
requiredCapabilities: ["developer.eval"],
|
|
2883
|
-
mutating: true,
|
|
2884
|
-
idempotency: "non_idempotent",
|
|
2885
|
-
cancellation: "best_effort",
|
|
2886
|
-
sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
|
|
2887
|
-
artifactKinds: []
|
|
2888
|
-
})
|
|
2889
|
-
];
|
|
2890
|
-
var toolsByName = new Map(TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
|
|
2891
|
-
function schemaSensitivities(schema, result = /* @__PURE__ */ new Set()) {
|
|
2892
|
-
for (const value of schema["x-browser-pilot-sensitivity"] ?? []) result.add(value);
|
|
2893
|
-
for (const property of Object.values(schema.properties ?? {})) schemaSensitivities(property, result);
|
|
2894
|
-
for (const alternative of schema.oneOf ?? []) schemaSensitivities(alternative, result);
|
|
2895
|
-
if (schema.items) schemaSensitivities(schema.items, result);
|
|
2896
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
2897
|
-
schemaSensitivities(schema.additionalProperties, result);
|
|
2898
|
-
}
|
|
2899
|
-
return result;
|
|
2900
|
-
}
|
|
2901
|
-
function assertToolManifest(definitions = TOOL_DEFINITIONS) {
|
|
2902
|
-
const names = /* @__PURE__ */ new Set();
|
|
2903
|
-
const knownCapabilities = new Set(CAPABILITIES);
|
|
2904
|
-
for (const definition of definitions) {
|
|
2905
|
-
if (!/^[a-z][a-z0-9]*(?:[._][a-z][a-z0-9]*)+$/.test(definition.name)) {
|
|
2906
|
-
throw new Error(`Invalid tool name: ${definition.name}`);
|
|
2907
|
-
}
|
|
2908
|
-
if (names.has(definition.name)) throw new Error(`Duplicate tool name: ${definition.name}`);
|
|
2909
|
-
names.add(definition.name);
|
|
2910
|
-
for (const capability of definition.requiredCapabilities) {
|
|
2911
|
-
if (!knownCapabilities.has(capability)) {
|
|
2912
|
-
throw new Error(`Tool ${definition.name} references unknown capability ${capability}`);
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
if (!definition.mutating && definition.idempotency === "non_idempotent") {
|
|
2916
|
-
throw new Error(`Read-only tool ${definition.name} cannot be non-idempotent`);
|
|
2917
|
-
}
|
|
2918
|
-
if (definition.mutating && definition.cancellation === "not_applicable") {
|
|
2919
|
-
throw new Error(`Mutating tool ${definition.name} must declare cancellation semantics`);
|
|
2920
|
-
}
|
|
2921
|
-
for (const direction of ["input", "output"]) {
|
|
2922
|
-
const sensitivity = definition.sensitivity[direction];
|
|
2923
|
-
if (new Set(sensitivity).size !== sensitivity.length) {
|
|
2924
|
-
throw new Error(`Tool ${definition.name} has duplicate ${direction} sensitivity`);
|
|
2925
|
-
}
|
|
2926
|
-
for (const value of sensitivity) {
|
|
2927
|
-
if (!SENSITIVITIES.includes(value)) {
|
|
2928
|
-
throw new Error(`Tool ${definition.name} references unknown sensitivity ${value}`);
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
for (const value of schemaSensitivities(
|
|
2932
|
-
direction === "input" ? definition.inputSchema : definition.outputSchema
|
|
2933
|
-
)) {
|
|
2934
|
-
if (!SENSITIVITIES.includes(value)) {
|
|
2935
|
-
throw new Error(
|
|
2936
|
-
`Tool ${definition.name} ${direction} schema contains unknown sensitivity ${value}`
|
|
2937
|
-
);
|
|
2938
|
-
}
|
|
2939
|
-
if (!sensitivity.includes(value)) {
|
|
2940
|
-
throw new Error(
|
|
2941
|
-
`Tool ${definition.name} ${direction} schema marks ${value} without declaring it`
|
|
2942
|
-
);
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
assertSchemaDefinition(definition.inputSchema, `${definition.name}.inputSchema`);
|
|
2947
|
-
assertSchemaDefinition(definition.outputSchema, `${definition.name}.outputSchema`);
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
assertToolManifest();
|
|
2951
|
-
|
|
2952
|
-
// src/services/command-runtime.ts
|
|
2953
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2954
|
-
|
|
2955
|
-
// src/services/event-journal.ts
|
|
2956
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
2957
|
-
|
|
2958
|
-
// src/services/broker-runtime.ts
|
|
2959
|
-
var DEFAULT_PROTOCOL_LIMITS = {
|
|
2960
|
-
maxMessageBytes: 1024 * 1024,
|
|
2961
|
-
maxResultBytes: 4 * 1024 * 1024,
|
|
2962
|
-
maxArtifactBytes: 100 * 1024 * 1024,
|
|
2963
|
-
eventJournalSize: 1e3
|
|
2964
|
-
};
|
|
2965
|
-
|
|
2966
|
-
// src/bridge/stdio-bridge.ts
|
|
2967
|
-
function isIncomingCall(message) {
|
|
2968
|
-
return "method" in message;
|
|
2969
|
-
}
|
|
2970
|
-
function isOutOfBandControl(message) {
|
|
2971
|
-
if (message.method === "commands/get" || message.method === "commands/cancel") return true;
|
|
2972
|
-
if (message.method !== "tools/call" || !message.params || typeof message.params !== "object") return false;
|
|
2973
|
-
if (Array.isArray(message.params)) return false;
|
|
2974
|
-
const name = message.params.name;
|
|
2975
|
-
return name === "browser.dialogs.list" || name === "browser.dialogs.respond";
|
|
2976
|
-
}
|
|
2977
|
-
function errorResponse(id, error) {
|
|
2978
|
-
const stable = asBrowserPilotError(error);
|
|
2979
|
-
return { jsonrpc: "2.0", id, error: stable.toJsonRpcError() };
|
|
2980
|
-
}
|
|
2981
|
-
async function writeLine(output, value, maxResultBytes) {
|
|
2982
|
-
let serialized = JSON.stringify(value);
|
|
2983
|
-
if (Buffer.byteLength(serialized, "utf8") > maxResultBytes) {
|
|
2984
|
-
if (!("id" in value)) return;
|
|
2985
|
-
serialized = JSON.stringify(errorResponse(value.id, new BrowserPilotError(
|
|
2986
|
-
"result_too_large",
|
|
2987
|
-
`Protocol result exceeds ${maxResultBytes} bytes`,
|
|
2988
|
-
{ context: { maxResultBytes } }
|
|
2989
|
-
)));
|
|
2990
|
-
}
|
|
2991
|
-
if (output.destroyed || !output.writable) throw new Error("Protocol output is closed");
|
|
2992
|
-
if (!output.write(`${serialized}
|
|
2993
|
-
`)) await once(output, "drain");
|
|
2994
|
-
}
|
|
2995
|
-
function negotiatedTransportLimits(value, ceilings) {
|
|
2996
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2997
|
-
const limits = value.limits;
|
|
2998
|
-
if (!limits || typeof limits !== "object" || Array.isArray(limits)) return void 0;
|
|
2999
|
-
if (!Number.isSafeInteger(limits.maxMessageBytes) || !Number.isSafeInteger(limits.maxResultBytes) || limits.maxMessageBytes <= 0 || limits.maxResultBytes <= 0) return void 0;
|
|
3000
|
-
return {
|
|
3001
|
-
maxMessageBytes: Math.min(limits.maxMessageBytes, ceilings.maxMessageBytes),
|
|
3002
|
-
maxResultBytes: Math.min(limits.maxResultBytes, ceilings.maxResultBytes)
|
|
3003
|
-
};
|
|
3004
|
-
}
|
|
3005
|
-
function decodeLine(bytes) {
|
|
3006
|
-
try {
|
|
3007
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3008
|
-
} catch (cause) {
|
|
3009
|
-
throw new BrowserPilotError("invalid_argument", "Protocol message is not valid UTF-8", {
|
|
3010
|
-
rpcCode: -32700,
|
|
3011
|
-
cause
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
async function runStdioBridge(options) {
|
|
3016
|
-
const bridgeSessionId = options.bridgeSessionId ?? `bridge:${randomUUID5()}`;
|
|
3017
|
-
const ceilings = {
|
|
3018
|
-
maxMessageBytes: options.maxMessageBytes ?? DEFAULT_PROTOCOL_LIMITS.maxMessageBytes,
|
|
3019
|
-
maxResultBytes: options.maxResultBytes ?? DEFAULT_PROTOCOL_LIMITS.maxResultBytes
|
|
3020
|
-
};
|
|
3021
|
-
let maxMessageBytes = ceilings.maxMessageBytes;
|
|
3022
|
-
let maxResultBytes = ceilings.maxResultBytes;
|
|
3023
|
-
const maxPendingCalls = options.maxPendingCalls ?? 256;
|
|
3024
|
-
const maxOutOfBandCalls = options.maxOutOfBandCalls ?? 16;
|
|
3025
|
-
if (!Number.isSafeInteger(maxPendingCalls) || maxPendingCalls <= 0 || !Number.isSafeInteger(maxOutOfBandCalls) || maxOutOfBandCalls <= 0) throw new Error("Bridge in-flight call limits must be positive integers");
|
|
3026
|
-
let pending = Buffer.alloc(0);
|
|
3027
|
-
let reason = "eof";
|
|
3028
|
-
let exitCode = 0;
|
|
3029
|
-
let stopped = false;
|
|
3030
|
-
let framingFailed = false;
|
|
3031
|
-
let outputTail = Promise.resolve();
|
|
3032
|
-
let normalTail = Promise.resolve();
|
|
3033
|
-
let initialization;
|
|
3034
|
-
let notificationController;
|
|
3035
|
-
let notificationTask;
|
|
3036
|
-
const inFlight = /* @__PURE__ */ new Set();
|
|
3037
|
-
let pendingCalls = 0;
|
|
3038
|
-
let outOfBandCalls = 0;
|
|
3039
|
-
const queueWrite = (value) => {
|
|
3040
|
-
const write = outputTail.then(() => writeLine(options.output, value, maxResultBytes));
|
|
3041
|
-
outputTail = write.catch(() => {
|
|
3042
|
-
});
|
|
3043
|
-
return write;
|
|
3044
|
-
};
|
|
3045
|
-
const failFraming = async (error) => {
|
|
3046
|
-
exitCode = 1;
|
|
3047
|
-
reason = "protocol_error";
|
|
3048
|
-
stopped = true;
|
|
3049
|
-
framingFailed = true;
|
|
3050
|
-
await queueWrite(errorResponse(null, error));
|
|
3051
|
-
};
|
|
3052
|
-
const track = (task, outOfBand) => {
|
|
3053
|
-
inFlight.add(task);
|
|
3054
|
-
if (outOfBand) outOfBandCalls += 1;
|
|
3055
|
-
else pendingCalls += 1;
|
|
3056
|
-
const finished = () => {
|
|
3057
|
-
inFlight.delete(task);
|
|
3058
|
-
if (outOfBand) outOfBandCalls -= 1;
|
|
3059
|
-
else pendingCalls -= 1;
|
|
3060
|
-
};
|
|
3061
|
-
void task.then(finished, finished);
|
|
3062
|
-
};
|
|
3063
|
-
const startNotifications = () => {
|
|
3064
|
-
if (notificationTask || !options.backend.notifications) return;
|
|
3065
|
-
notificationController = new AbortController();
|
|
3066
|
-
notificationTask = (async () => {
|
|
3067
|
-
for await (const notification of options.backend.notifications(
|
|
3068
|
-
bridgeSessionId,
|
|
3069
|
-
notificationController.signal
|
|
3070
|
-
)) {
|
|
3071
|
-
if (notificationController.signal.aborted || stopped) break;
|
|
3072
|
-
await queueWrite(notification);
|
|
3073
|
-
}
|
|
3074
|
-
})().catch(() => {
|
|
3075
|
-
if (notificationController?.signal.aborted) return;
|
|
3076
|
-
stopped = true;
|
|
3077
|
-
reason = "output_closed";
|
|
3078
|
-
exitCode = 1;
|
|
3079
|
-
options.input.destroy();
|
|
3080
|
-
});
|
|
3081
|
-
};
|
|
3082
|
-
const processLine = async (rawLine) => {
|
|
3083
|
-
const line = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
|
|
3084
|
-
let message;
|
|
3085
|
-
try {
|
|
3086
|
-
message = parseJsonRpcMessage(decodeLine(line), maxMessageBytes);
|
|
3087
|
-
} catch (error) {
|
|
3088
|
-
await failFraming(error);
|
|
3089
|
-
return;
|
|
3090
|
-
}
|
|
3091
|
-
if (!isIncomingCall(message)) {
|
|
3092
|
-
await failFraming(invalidArgument("Bridge accepts JSON-RPC requests and notifications only", void 0, -32600));
|
|
3093
|
-
return;
|
|
3094
|
-
}
|
|
3095
|
-
const requestId = "id" in message ? message.id : void 0;
|
|
3096
|
-
const outOfBand = isOutOfBandControl(message);
|
|
3097
|
-
const saturated = outOfBand ? outOfBandCalls >= maxOutOfBandCalls : pendingCalls >= maxPendingCalls;
|
|
3098
|
-
if (saturated) {
|
|
3099
|
-
if (requestId !== void 0) {
|
|
3100
|
-
await queueWrite(errorResponse(requestId, new BrowserPilotError(
|
|
3101
|
-
"result_too_large",
|
|
3102
|
-
`Bridge ${outOfBand ? "control" : "request"} queue is full`,
|
|
3103
|
-
{
|
|
3104
|
-
retryable: true,
|
|
3105
|
-
context: {
|
|
3106
|
-
limit: outOfBand ? maxOutOfBandCalls : maxPendingCalls,
|
|
3107
|
-
queue: outOfBand ? "control" : "request"
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
)));
|
|
3111
|
-
}
|
|
3112
|
-
return;
|
|
3113
|
-
}
|
|
3114
|
-
const dispatch = async () => {
|
|
3115
|
-
try {
|
|
3116
|
-
const result = await options.backend.call(bridgeSessionId, message.method, message.params);
|
|
3117
|
-
if (requestId !== void 0 && reason !== "protocol_error") {
|
|
3118
|
-
await queueWrite({ jsonrpc: "2.0", id: requestId, result });
|
|
3119
|
-
}
|
|
3120
|
-
if (message.method === "initialize" && reason !== "protocol_error") {
|
|
3121
|
-
const negotiated = negotiatedTransportLimits(result, ceilings);
|
|
3122
|
-
if (negotiated) {
|
|
3123
|
-
maxMessageBytes = negotiated.maxMessageBytes;
|
|
3124
|
-
maxResultBytes = negotiated.maxResultBytes;
|
|
3125
|
-
}
|
|
3126
|
-
startNotifications();
|
|
3127
|
-
}
|
|
3128
|
-
} catch (error) {
|
|
3129
|
-
if (requestId !== void 0 && reason !== "protocol_error") {
|
|
3130
|
-
try {
|
|
3131
|
-
await queueWrite(errorResponse(requestId, error));
|
|
3132
|
-
} catch {
|
|
3133
|
-
stopped = true;
|
|
3134
|
-
reason = "output_closed";
|
|
3135
|
-
exitCode = 1;
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
};
|
|
3140
|
-
const task = outOfBand ? (initialization ?? Promise.resolve()).then(dispatch) : normalTail.then(dispatch);
|
|
3141
|
-
if (!outOfBand) normalTail = task.catch(() => {
|
|
3142
|
-
});
|
|
3143
|
-
if (message.method === "initialize") initialization = task.catch(() => {
|
|
3144
|
-
});
|
|
3145
|
-
track(task, outOfBand);
|
|
3146
|
-
if (message.method === "shutdown") {
|
|
3147
|
-
stopped = true;
|
|
3148
|
-
reason = "shutdown";
|
|
1084
|
+
actionRequired: true
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
3149
1087
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
{ context: { maxMessageBytes }, rpcCode: -32600 }
|
|
3164
|
-
));
|
|
3165
|
-
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
|
|
3166
1101
|
}
|
|
3167
|
-
pending = pending.length === 0 ? Buffer.from(remaining) : Buffer.concat([pending, remaining]);
|
|
3168
|
-
offset = chunk.length;
|
|
3169
|
-
continue;
|
|
3170
1102
|
}
|
|
3171
|
-
|
|
3172
|
-
if (pending.length + part.length > maxMessageBytes) {
|
|
3173
|
-
await failFraming(new BrowserPilotError(
|
|
3174
|
-
"result_too_large",
|
|
3175
|
-
`Protocol message exceeds ${maxMessageBytes} bytes`,
|
|
3176
|
-
{ context: { maxMessageBytes }, rpcCode: -32600 }
|
|
3177
|
-
));
|
|
3178
|
-
break;
|
|
3179
|
-
}
|
|
3180
|
-
const line = pending.length === 0 ? part : Buffer.concat([pending, part]);
|
|
3181
|
-
pending = Buffer.alloc(0);
|
|
3182
|
-
offset = newline + 1;
|
|
3183
|
-
await processLine(line);
|
|
3184
|
-
if (initialization) await initialization;
|
|
3185
|
-
}
|
|
3186
|
-
if (stopped) break;
|
|
3187
|
-
}
|
|
3188
|
-
if (!stopped && pending.length > 0) await processLine(pending);
|
|
3189
|
-
notificationController?.abort();
|
|
3190
|
-
await notificationTask;
|
|
3191
|
-
if (!framingFailed) await Promise.allSettled([...inFlight]);
|
|
3192
|
-
await outputTail;
|
|
3193
|
-
} catch {
|
|
3194
|
-
if (!stopped) {
|
|
3195
|
-
reason = "output_closed";
|
|
3196
|
-
exitCode = 1;
|
|
1103
|
+
);
|
|
3197
1104
|
}
|
|
1105
|
+
const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
|
|
1106
|
+
return await startDaemon(selected);
|
|
3198
1107
|
} finally {
|
|
3199
|
-
|
|
3200
|
-
await notificationTask;
|
|
3201
|
-
try {
|
|
3202
|
-
await options.backend.disconnect(bridgeSessionId);
|
|
3203
|
-
} catch {
|
|
3204
|
-
}
|
|
1108
|
+
startupLock.release();
|
|
3205
1109
|
}
|
|
3206
|
-
return { exitCode, reason };
|
|
3207
1110
|
}
|
|
3208
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
|
+
|
|
3209
1141
|
// src/compatibility-broker-client.ts
|
|
3210
|
-
import { randomUUID as randomUUID6 } from "crypto";
|
|
3211
|
-
var BRIDGE_SESSION_ID = "bridge:browser-pilot-cli";
|
|
3212
1142
|
var CLIENT_KEY = "browser-pilot-cli";
|
|
3213
1143
|
var LEASE_TTL_MS = 5 * 6e4;
|
|
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);
|
|
1147
|
+
return {
|
|
1148
|
+
clientSessionId: `client:browser-pilot-cli:${sessionDigest}`,
|
|
1149
|
+
instanceId: `local:browser-pilot-cli:${principalDigest}`
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function isSelected(target) {
|
|
1153
|
+
return target.selected ?? target.active === true;
|
|
1154
|
+
}
|
|
3214
1155
|
function asRecord(value, label) {
|
|
3215
1156
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3216
1157
|
throw new BrowserPilotError("internal_error", `${label} returned an invalid result`);
|
|
@@ -3226,55 +1167,93 @@ function commandResult(value, method) {
|
|
|
3226
1167
|
return asRecord(outcome.result, `${method} result`);
|
|
3227
1168
|
}
|
|
3228
1169
|
var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
3229
|
-
constructor(transport, initialized, workspace, lease) {
|
|
1170
|
+
constructor(transport, clientSessionId, initialized, workspace, lease, invocation) {
|
|
3230
1171
|
this.transport = transport;
|
|
1172
|
+
this.clientSessionId = clientSessionId;
|
|
3231
1173
|
this.initialized = initialized;
|
|
3232
1174
|
this.workspace = workspace;
|
|
3233
1175
|
this.lease = lease;
|
|
1176
|
+
this.invocation = invocation;
|
|
3234
1177
|
}
|
|
3235
1178
|
commandSequence = 0;
|
|
3236
|
-
static async create(transport, executableVersion) {
|
|
3237
|
-
const
|
|
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", {
|
|
3238
1182
|
client: {
|
|
3239
1183
|
id: "org.browser-pilot.cli",
|
|
3240
1184
|
name: "Browser Pilot CLI",
|
|
3241
1185
|
version: executableVersion,
|
|
3242
|
-
instanceId:
|
|
1186
|
+
instanceId: identity.instanceId
|
|
3243
1187
|
},
|
|
3244
|
-
protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor:
|
|
3245
|
-
requestedCapabilities: [...CAPABILITIES]
|
|
3246
|
-
launchMode: "one-shot"
|
|
1188
|
+
protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 3 } },
|
|
1189
|
+
requestedCapabilities: [...CAPABILITIES]
|
|
3247
1190
|
}), "initialize");
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
remediation: {
|
|
3251
|
-
code: "use_matching_executable_or_isolate",
|
|
3252
|
-
message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
|
|
3253
|
-
actionRequired: true
|
|
3254
|
-
}
|
|
3255
|
-
});
|
|
3256
|
-
}
|
|
3257
|
-
const created = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "workspaces/create", {
|
|
3258
|
-
clientKey: CLIENT_KEY
|
|
1191
|
+
const created = asRecord(await transport.brokerCall(identity.clientSessionId, "workspaces/create", {
|
|
1192
|
+
clientKey
|
|
3259
1193
|
}), "workspaces/create");
|
|
3260
|
-
const leased = asRecord(await transport.brokerCall(
|
|
1194
|
+
const leased = asRecord(await transport.brokerCall(identity.clientSessionId, "leases/create", {
|
|
3261
1195
|
workspaceId: created.workspace.id,
|
|
3262
|
-
clientKey
|
|
1196
|
+
clientKey,
|
|
3263
1197
|
ttlMs: LEASE_TTL_MS
|
|
3264
1198
|
}), "leases/create");
|
|
3265
|
-
return new _CompatibilityBrokerClient(
|
|
1199
|
+
return new _CompatibilityBrokerClient(
|
|
1200
|
+
transport,
|
|
1201
|
+
identity.clientSessionId,
|
|
1202
|
+
initialized,
|
|
1203
|
+
created.workspace,
|
|
1204
|
+
leased.lease,
|
|
1205
|
+
invocation
|
|
1206
|
+
);
|
|
3266
1207
|
}
|
|
3267
1208
|
async callTool(name, args = {}, targetId) {
|
|
3268
1209
|
this.commandSequence += 1;
|
|
3269
|
-
|
|
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", {
|
|
3270
1215
|
name,
|
|
3271
1216
|
arguments: args,
|
|
3272
1217
|
workspaceId: this.workspace.id,
|
|
3273
1218
|
leaseId: this.lease.id,
|
|
3274
1219
|
...targetId ? { targetId } : {},
|
|
3275
|
-
commandId
|
|
3276
|
-
|
|
3277
|
-
|
|
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");
|
|
3278
1257
|
}
|
|
3279
1258
|
async connectBrowser(browserId) {
|
|
3280
1259
|
const selected = browserId ? this.initialized.browsers.find((candidate) => candidate.id === browserId) : this.initialized.browsers.find((candidate) => candidate.state === "ready") ?? this.initialized.browsers[0];
|
|
@@ -3304,6 +1283,16 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3304
1283
|
}
|
|
3305
1284
|
return result.profiles;
|
|
3306
1285
|
}
|
|
1286
|
+
async identifyProfiles(profileContextId, refresh = false) {
|
|
1287
|
+
const result = await this.callTool("browser.profiles.identify", {
|
|
1288
|
+
...profileContextId ? { profileContextId } : {},
|
|
1289
|
+
...refresh ? { refresh: true } : {}
|
|
1290
|
+
});
|
|
1291
|
+
if (!Array.isArray(result.profiles)) {
|
|
1292
|
+
throw new BrowserPilotError("internal_error", "browser.profiles.identify returned invalid Profiles");
|
|
1293
|
+
}
|
|
1294
|
+
return result.profiles;
|
|
1295
|
+
}
|
|
3307
1296
|
async selectProfile(profileContextId) {
|
|
3308
1297
|
const result = await this.callTool("browser.profiles.select", { profileContextId });
|
|
3309
1298
|
const profiles = await this.listProfiles();
|
|
@@ -3317,11 +1306,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3317
1306
|
}
|
|
3318
1307
|
async ensureTarget() {
|
|
3319
1308
|
const targets = await this.listTabs("all");
|
|
3320
|
-
const selected = targets.find(
|
|
1309
|
+
const selected = targets.find(isSelected) ?? targets.find((target) => target.origin !== "user_tab");
|
|
3321
1310
|
if (selected) {
|
|
3322
|
-
if (!selected
|
|
1311
|
+
if (!isSelected(selected)) {
|
|
3323
1312
|
await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
|
|
3324
|
-
selected.
|
|
1313
|
+
selected.selected = true;
|
|
3325
1314
|
}
|
|
3326
1315
|
return selected;
|
|
3327
1316
|
}
|
|
@@ -3329,11 +1318,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3329
1318
|
}
|
|
3330
1319
|
async ensureManagedTarget() {
|
|
3331
1320
|
const targets = await this.listTabs("managed_only");
|
|
3332
|
-
const selected = targets.find(
|
|
1321
|
+
const selected = targets.find(isSelected) ?? targets[0];
|
|
3333
1322
|
if (selected) {
|
|
3334
|
-
if (!selected
|
|
1323
|
+
if (!isSelected(selected)) {
|
|
3335
1324
|
await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
|
|
3336
|
-
selected.
|
|
1325
|
+
selected.selected = true;
|
|
3337
1326
|
}
|
|
3338
1327
|
return selected;
|
|
3339
1328
|
}
|
|
@@ -3350,7 +1339,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3350
1339
|
profileContextId: opened.profileContextId,
|
|
3351
1340
|
title: String(opened.title ?? ""),
|
|
3352
1341
|
url: String(opened.url),
|
|
3353
|
-
|
|
1342
|
+
selected: true,
|
|
3354
1343
|
origin: "managed",
|
|
3355
1344
|
controlState: "controlled"
|
|
3356
1345
|
};
|
|
@@ -3363,7 +1352,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3363
1352
|
return result.observationId;
|
|
3364
1353
|
}
|
|
3365
1354
|
async importArtifact(path, mimeType) {
|
|
3366
|
-
const result = asRecord(await this.transport.brokerCall(
|
|
1355
|
+
const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "artifacts/import", {
|
|
3367
1356
|
workspaceId: this.workspace.id,
|
|
3368
1357
|
leaseId: this.lease.id,
|
|
3369
1358
|
path,
|
|
@@ -3371,8 +1360,19 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3371
1360
|
}), "artifacts/import");
|
|
3372
1361
|
return asRecord(result.artifact, "imported Artifact");
|
|
3373
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
|
+
}
|
|
3374
1374
|
async exportArtifact(artifactId, path) {
|
|
3375
|
-
await this.transport.brokerCall(
|
|
1375
|
+
await this.transport.brokerCall(this.clientSessionId, "artifacts/export", {
|
|
3376
1376
|
workspaceId: this.workspace.id,
|
|
3377
1377
|
leaseId: this.lease.id,
|
|
3378
1378
|
artifactId,
|
|
@@ -3381,113 +1381,88 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
|
|
|
3381
1381
|
});
|
|
3382
1382
|
}
|
|
3383
1383
|
async releaseArtifact(artifactId) {
|
|
3384
|
-
await this.transport.brokerCall(
|
|
1384
|
+
await this.transport.brokerCall(this.clientSessionId, "artifacts/release", {
|
|
3385
1385
|
workspaceId: this.workspace.id,
|
|
3386
1386
|
leaseId: this.lease.id,
|
|
3387
1387
|
artifactId
|
|
3388
1388
|
});
|
|
3389
1389
|
}
|
|
3390
1390
|
async releaseWorkspace() {
|
|
3391
|
-
await this.transport.brokerCall(
|
|
1391
|
+
await this.transport.brokerCall(this.clientSessionId, "workspaces/release", {
|
|
3392
1392
|
workspaceId: this.workspace.id
|
|
3393
1393
|
});
|
|
3394
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
|
+
}
|
|
3395
1402
|
};
|
|
3396
1403
|
async function validateDaemon(client) {
|
|
3397
1404
|
const health = await client.healthInfo();
|
|
3398
1405
|
if (!health.ok) throw new BrowserPilotError("browser_disconnected", "Browser Pilot daemon is unavailable");
|
|
3399
|
-
if (health.brokerProtocol !==
|
|
3400
|
-
throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot
|
|
3401
|
-
remediation: {
|
|
3402
|
-
code: "use_compatible_executable_or_isolate",
|
|
3403
|
-
message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
|
|
3404
|
-
actionRequired: true
|
|
3405
|
-
}
|
|
3406
|
-
});
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
async function validateCompatibilityDaemon(client, executableVersion) {
|
|
3410
|
-
await validateDaemon(client);
|
|
3411
|
-
const health = await client.healthInfo();
|
|
3412
|
-
const requester = createExecutableMetadataSync(
|
|
3413
|
-
executableVersion,
|
|
3414
|
-
publicExecutablePath(import.meta.url)
|
|
3415
|
-
);
|
|
3416
|
-
if (health.executableVersion !== void 0 && health.executableVersion !== requester.version || health.executableIdentity !== void 0 && health.executableIdentity !== requester.identity) {
|
|
3417
|
-
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", {
|
|
3418
1408
|
context: {
|
|
3419
|
-
|
|
3420
|
-
|
|
1409
|
+
brokerRpcVersion: health.brokerProtocol,
|
|
1410
|
+
requiredBrokerRpcVersion: BROKER_RPC_VERSION,
|
|
1411
|
+
serviceVersion: health.serviceVersion,
|
|
1412
|
+
executableVersion: health.executableVersion
|
|
3421
1413
|
},
|
|
3422
1414
|
remediation: {
|
|
3423
|
-
code: "
|
|
3424
|
-
message: "
|
|
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.",
|
|
3425
1417
|
actionRequired: true
|
|
3426
1418
|
}
|
|
3427
1419
|
});
|
|
3428
1420
|
}
|
|
3429
1421
|
}
|
|
3430
|
-
async function connectCompatibility(executableVersion, browserFilter) {
|
|
1422
|
+
async function connectCompatibility(executableVersion, browserFilter, clientKey = CLIENT_KEY, invocation = {}) {
|
|
3431
1423
|
const daemon = await connectDaemon(browserFilter);
|
|
3432
|
-
await
|
|
3433
|
-
return CompatibilityBrokerClient.create(daemon, executableVersion);
|
|
1424
|
+
await validateDaemon(daemon);
|
|
1425
|
+
return CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
|
|
3434
1426
|
}
|
|
3435
|
-
async function resumeCompatibility(executableVersion) {
|
|
1427
|
+
async function resumeCompatibility(executableVersion, clientKey = CLIENT_KEY, invocation = {}) {
|
|
3436
1428
|
if (!isDaemonRunning()) return null;
|
|
3437
1429
|
const daemon = new DaemonClient();
|
|
3438
1430
|
try {
|
|
3439
|
-
await
|
|
3440
|
-
return await CompatibilityBrokerClient.create(daemon, executableVersion);
|
|
1431
|
+
await validateDaemon(daemon);
|
|
1432
|
+
return await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
|
|
3441
1433
|
} catch (error) {
|
|
3442
1434
|
if (error instanceof BrowserPilotError && error.code === "protocol_incompatible") throw error;
|
|
3443
1435
|
return null;
|
|
3444
1436
|
}
|
|
3445
1437
|
}
|
|
3446
|
-
async function withCompatibilityTarget(executableVersion, operation) {
|
|
3447
|
-
const client = await resumeCompatibility(executableVersion);
|
|
1438
|
+
async function withCompatibilityTarget(executableVersion, operation, clientKey = CLIENT_KEY, invocation = {}) {
|
|
1439
|
+
const client = await resumeCompatibility(executableVersion, clientKey, invocation);
|
|
3448
1440
|
if (!client) throw new Error("Not connected");
|
|
3449
1441
|
const target = await client.ensureTarget();
|
|
3450
1442
|
return operation(client, target);
|
|
3451
1443
|
}
|
|
3452
|
-
async function shutdownCompatibility(executableVersion) {
|
|
1444
|
+
async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY) {
|
|
3453
1445
|
if (!isDaemonRunning()) return;
|
|
3454
1446
|
const daemon = new DaemonClient();
|
|
3455
1447
|
await validateDaemon(daemon);
|
|
3456
|
-
const health = await daemon.healthInfo();
|
|
3457
|
-
if (!health.ok || !health.brokerProcessIdentity || !health.executableVersion || !health.executableIdentity) {
|
|
3458
|
-
throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot Broker does not support protected shutdown", {
|
|
3459
|
-
remediation: {
|
|
3460
|
-
code: "use_matching_executable_or_isolate",
|
|
3461
|
-
message: "Use the Browser Pilot installation that started the Broker, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
|
|
3462
|
-
actionRequired: true
|
|
3463
|
-
}
|
|
3464
|
-
});
|
|
3465
|
-
}
|
|
3466
|
-
const requester = createExecutableMetadataSync(
|
|
3467
|
-
executableVersion,
|
|
3468
|
-
publicExecutablePath(import.meta.url)
|
|
3469
|
-
);
|
|
3470
|
-
if (requester.version !== health.executableVersion || requester.identity !== health.executableIdentity) {
|
|
3471
|
-
throw new BrowserPilotError("protocol_incompatible", "This Browser Pilot installation does not own the running Broker", {
|
|
3472
|
-
context: {
|
|
3473
|
-
brokerExecutableVersion: health.executableVersion,
|
|
3474
|
-
requesterExecutableVersion: requester.version
|
|
3475
|
-
},
|
|
3476
|
-
remediation: {
|
|
3477
|
-
code: "use_matching_executable_or_isolate",
|
|
3478
|
-
message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
|
|
3479
|
-
actionRequired: true
|
|
3480
|
-
}
|
|
3481
|
-
});
|
|
3482
|
-
}
|
|
3483
1448
|
try {
|
|
3484
|
-
const client = await CompatibilityBrokerClient.create(daemon, executableVersion);
|
|
1449
|
+
const client = await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
|
|
3485
1450
|
await client.releaseWorkspace();
|
|
3486
1451
|
} catch (error) {
|
|
3487
1452
|
if (!(error instanceof BrowserPilotError) || error.code !== "browser_not_found") throw error;
|
|
3488
1453
|
}
|
|
1454
|
+
const afterRelease = await daemon.healthInfo();
|
|
1455
|
+
if ((afterRelease.clients?.activeLeases ?? 0) > 0) {
|
|
1456
|
+
return;
|
|
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;
|
|
3489
1464
|
await daemon.shutdown({
|
|
3490
|
-
brokerProcessIdentity:
|
|
1465
|
+
brokerProcessIdentity: afterRelease.brokerProcessIdentity,
|
|
3491
1466
|
executableVersion: requester.version,
|
|
3492
1467
|
executableIdentity: requester.identity
|
|
3493
1468
|
});
|
|
@@ -3495,7 +1470,8 @@ async function shutdownCompatibility(executableVersion) {
|
|
|
3495
1470
|
|
|
3496
1471
|
// src/cli.ts
|
|
3497
1472
|
var program = new Command();
|
|
3498
|
-
program.
|
|
1473
|
+
program.exitOverride();
|
|
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", `
|
|
3499
1475
|
Workflow:
|
|
3500
1476
|
bp connect # one-time setup (click Allow in Chrome)
|
|
3501
1477
|
bp profiles # list live Chrome Profile contexts
|
|
@@ -3525,7 +1501,7 @@ Output:
|
|
|
3525
1501
|
JSON by default when piped (for LLM/script use).
|
|
3526
1502
|
Human-readable when run in a terminal (TTY). Force with --human.
|
|
3527
1503
|
Actions return: {"ok":true, "title":"...", "url":"...", "elements":[...]}
|
|
3528
|
-
Errors return: {"ok":false, "error":"...", "
|
|
1504
|
+
Errors return: {"ok":false, "error":"...", "code":"...", "retryable":false}
|
|
3529
1505
|
|
|
3530
1506
|
Canvas editors (Google Docs, Sheets, Figma):
|
|
3531
1507
|
bp keyboard "text" --click ".editor" # click to focus, then type
|
|
@@ -3550,24 +1526,43 @@ Eval (escape hatch for operations without a dedicated command):
|
|
|
3550
1526
|
bp eval "JSON.stringify(localStorage)" # read storage
|
|
3551
1527
|
echo 'complex js here' | bp eval # stdin for complex JS
|
|
3552
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
|
+
}
|
|
3553
1539
|
function useJson() {
|
|
3554
|
-
if (program.opts().human) return false;
|
|
1540
|
+
if (process.argv.slice(2).includes("--human") || program.opts().human) return false;
|
|
3555
1541
|
return !process.stdout.isTTY;
|
|
3556
1542
|
}
|
|
1543
|
+
program.configureOutput({
|
|
1544
|
+
writeErr: (value) => {
|
|
1545
|
+
if (!useJson()) process.stderr.write(value);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
3557
1548
|
function emit(data, human) {
|
|
3558
1549
|
if (useJson()) console.log(JSON.stringify(data));
|
|
3559
1550
|
else if (human) console.log(human);
|
|
3560
1551
|
}
|
|
3561
1552
|
function fail(error, hint, details) {
|
|
1553
|
+
const stable = details ?? new BrowserPilotError("internal_error", error);
|
|
3562
1554
|
if (useJson()) console.log(JSON.stringify({
|
|
3563
1555
|
ok: false,
|
|
3564
1556
|
error,
|
|
1557
|
+
code: stable.code,
|
|
1558
|
+
retryable: stable.retryable,
|
|
3565
1559
|
...hint ? { hint } : {},
|
|
3566
|
-
...
|
|
1560
|
+
...stable.context ? { context: stable.context } : {},
|
|
1561
|
+
...stable.remediation ? { remediation: stable.remediation } : {}
|
|
3567
1562
|
}));
|
|
3568
1563
|
else console.error(`\u2717 ${error}${hint ? `
|
|
3569
1564
|
hint: ${hint}` : ""}`);
|
|
3570
|
-
process.exit(1);
|
|
1565
|
+
process.exit(receivedSignal === "SIGINT" ? 130 : receivedSignal === "SIGTERM" ? 143 : 1);
|
|
3571
1566
|
}
|
|
3572
1567
|
function observationElements(result) {
|
|
3573
1568
|
return Array.isArray(result.elements) ? result.elements : [];
|
|
@@ -3617,13 +1612,17 @@ function emitObservation(result) {
|
|
|
3617
1612
|
function action(fn) {
|
|
3618
1613
|
return (...args) => fn(...args).catch((err) => {
|
|
3619
1614
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3620
|
-
|
|
3621
|
-
|
|
1615
|
+
const stable = err instanceof BrowserPilotError ? err : new BrowserPilotError("internal_error", msg, { cause: err });
|
|
1616
|
+
if (stable.code === "stale_ref") {
|
|
1617
|
+
fail(msg, "Run 'bp snapshot' to refresh element refs.", stable);
|
|
1618
|
+
}
|
|
1619
|
+
if (stable.code === "browser_disconnected") {
|
|
1620
|
+
fail(msg, "Run 'bp connect' first.", stable);
|
|
3622
1621
|
}
|
|
3623
|
-
if (
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
fail(msg, void 0,
|
|
1622
|
+
if (stable.code === "unknown_outcome") {
|
|
1623
|
+
fail(msg, "Inspect the current tab state before deciding whether to retry.", stable);
|
|
1624
|
+
}
|
|
1625
|
+
fail(msg, void 0, stable);
|
|
3627
1626
|
});
|
|
3628
1627
|
}
|
|
3629
1628
|
function normalizeUrl(url) {
|
|
@@ -3631,18 +1630,127 @@ function normalizeUrl(url) {
|
|
|
3631
1630
|
return `https://${url}`;
|
|
3632
1631
|
}
|
|
3633
1632
|
function parseLimit(raw) {
|
|
3634
|
-
const n =
|
|
3635
|
-
if (
|
|
1633
|
+
const n = Number(raw);
|
|
1634
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) {
|
|
1635
|
+
throw invalidArgument("--limit must be a positive integer", "limit");
|
|
1636
|
+
}
|
|
3636
1637
|
return n;
|
|
3637
1638
|
}
|
|
3638
1639
|
function parseRef(raw) {
|
|
3639
1640
|
const ref = Number(raw);
|
|
3640
|
-
if (!Number.isSafeInteger(ref) || ref < 1) {
|
|
3641
|
-
throw
|
|
1641
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(ref) || ref < 1) {
|
|
1642
|
+
throw invalidArgument("Ref must be a positive integer", "ref");
|
|
3642
1643
|
}
|
|
3643
1644
|
return ref;
|
|
3644
1645
|
}
|
|
3645
|
-
function
|
|
1646
|
+
function parseCoordinates(raw) {
|
|
1647
|
+
const parts = raw.split(",");
|
|
1648
|
+
if (parts.length !== 2 || parts.some((part) => part.trim() === "")) {
|
|
1649
|
+
throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
|
|
1650
|
+
}
|
|
1651
|
+
const [x, y] = parts.map((part) => Number(part.trim()));
|
|
1652
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1653
|
+
throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
|
|
1654
|
+
}
|
|
1655
|
+
return { x, y };
|
|
1656
|
+
}
|
|
1657
|
+
function cliClientKey() {
|
|
1658
|
+
const value = program.opts().clientKey ?? process.env.BROWSER_PILOT_CLIENT_KEY;
|
|
1659
|
+
if (value === void 0) return "browser-pilot-cli";
|
|
1660
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(value)) {
|
|
1661
|
+
throw new BrowserPilotError(
|
|
1662
|
+
"invalid_argument",
|
|
1663
|
+
"Client key must be 3-128 characters using letters, digits, dot, underscore, colon, or hyphen",
|
|
1664
|
+
{ context: { field: "clientKey" } }
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
return value;
|
|
1668
|
+
}
|
|
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) {
|
|
3646
1754
|
if (typeof value !== "string") throw new BrowserPilotError("internal_error", `${label} is missing`);
|
|
3647
1755
|
return value;
|
|
3648
1756
|
}
|
|
@@ -3659,12 +1767,21 @@ async function cliElementAddress(client, target, raw) {
|
|
|
3659
1767
|
ref: parseRef(raw)
|
|
3660
1768
|
};
|
|
3661
1769
|
}
|
|
3662
|
-
if (!raw.trim()) throw
|
|
1770
|
+
if (!raw.trim()) throw invalidArgument("Element target must not be empty", "target");
|
|
3663
1771
|
return { selector: raw };
|
|
3664
1772
|
}
|
|
3665
1773
|
async function requireCompatibility() {
|
|
3666
|
-
const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
|
|
3667
|
-
if (!client)
|
|
1774
|
+
const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
|
|
1775
|
+
if (!client) {
|
|
1776
|
+
throw new BrowserPilotError("browser_disconnected", "Browser Pilot is not connected", {
|
|
1777
|
+
retryable: true,
|
|
1778
|
+
remediation: {
|
|
1779
|
+
code: "connect_browser",
|
|
1780
|
+
message: "Run 'bp connect', complete Chrome authorization if prompted, then retry.",
|
|
1781
|
+
actionRequired: true
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
3668
1785
|
return client;
|
|
3669
1786
|
}
|
|
3670
1787
|
async function resolveCliProfile(client, selector) {
|
|
@@ -3673,11 +1790,11 @@ async function resolveCliProfile(client, selector) {
|
|
|
3673
1790
|
if (exactId) return exactId;
|
|
3674
1791
|
if (/^\d+$/.test(selector)) {
|
|
3675
1792
|
const index = Number(selector);
|
|
3676
|
-
if (Number.isSafeInteger(index) && profiles[index]) return profiles[index];
|
|
3677
|
-
throw invalidCliProfile(`Profile index out of range (
|
|
1793
|
+
if (Number.isSafeInteger(index) && index >= 1 && profiles[index - 1]) return profiles[index - 1];
|
|
1794
|
+
throw invalidCliProfile(`Profile index out of range (1-${profiles.length})`, selector);
|
|
3678
1795
|
}
|
|
3679
1796
|
const normalized = selector.trim().toLocaleLowerCase();
|
|
3680
|
-
const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized);
|
|
1797
|
+
const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized || profile.profileName?.toLocaleLowerCase() === normalized || profile.accountName?.toLocaleLowerCase() === normalized || profile.accountEmail?.toLocaleLowerCase() === normalized);
|
|
3681
1798
|
if (matches.length === 1) return matches[0];
|
|
3682
1799
|
if (matches.length > 1) {
|
|
3683
1800
|
throw invalidCliProfile("Profile selector is ambiguous; use its index or Profile context ID", selector);
|
|
@@ -3691,10 +1808,16 @@ function invalidCliProfile(message, selector) {
|
|
|
3691
1808
|
}
|
|
3692
1809
|
function cliProfile(profile, index) {
|
|
3693
1810
|
return {
|
|
3694
|
-
index,
|
|
1811
|
+
index: index + 1,
|
|
3695
1812
|
profileContextId: profile.profileContextId,
|
|
3696
1813
|
label: profile.label,
|
|
3697
1814
|
...profile.displayName ? { displayName: profile.displayName } : {},
|
|
1815
|
+
...profile.identityStatus ? { identityStatus: profile.identityStatus } : {},
|
|
1816
|
+
...profile.profileName ? { profileName: profile.profileName } : {},
|
|
1817
|
+
...profile.accountName ? { accountName: profile.accountName } : {},
|
|
1818
|
+
...profile.accountEmail ? { accountEmail: profile.accountEmail } : {},
|
|
1819
|
+
...profile.profileDirectory ? { profileDirectory: profile.profileDirectory } : {},
|
|
1820
|
+
...profile.identityErrorCode ? { identityErrorCode: profile.identityErrorCode } : {},
|
|
3698
1821
|
tabCount: profile.tabCount,
|
|
3699
1822
|
eligibleTabCount: profile.eligibleTabCount,
|
|
3700
1823
|
selected: profile.selected,
|
|
@@ -3702,7 +1825,7 @@ function cliProfile(profile, index) {
|
|
|
3702
1825
|
};
|
|
3703
1826
|
}
|
|
3704
1827
|
function withCliTarget(operation) {
|
|
3705
|
-
return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation);
|
|
1828
|
+
return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation, cliClientKey(), cliInvocationOptions());
|
|
3706
1829
|
}
|
|
3707
1830
|
function readStdin() {
|
|
3708
1831
|
if (process.stdin.isTTY) return Promise.resolve("");
|
|
@@ -3714,9 +1837,212 @@ function readStdin() {
|
|
|
3714
1837
|
process.stdin.on("end", () => resolve(d.trim()));
|
|
3715
1838
|
});
|
|
3716
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
|
+
}));
|
|
3717
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) => {
|
|
3718
2044
|
const filter = typeof opts.browser === "string" ? opts.browser.toLowerCase() : void 0;
|
|
3719
|
-
const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
|
|
2045
|
+
const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
|
|
3720
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)));
|
|
3721
2047
|
if (useJson()) {
|
|
3722
2048
|
console.log(JSON.stringify({ ok: true, browsers }));
|
|
@@ -3743,7 +2069,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
|
|
|
3743
2069
|
console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
|
|
3744
2070
|
`);
|
|
3745
2071
|
}
|
|
3746
|
-
const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser);
|
|
2072
|
+
const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser, cliClientKey(), cliInvocationOptions());
|
|
3747
2073
|
await client.connectBrowser();
|
|
3748
2074
|
const profiles = await client.listProfiles();
|
|
3749
2075
|
if (profiles.length <= 1) await client.ensureManagedTarget();
|
|
@@ -3753,7 +2079,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
|
|
|
3753
2079
|
emit(
|
|
3754
2080
|
{ ok: true, browser, profileSelectionRequired: true, profiles: listed },
|
|
3755
2081
|
`\u2713 Connected to ${browser}
|
|
3756
|
-
Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.`
|
|
2082
|
+
Multiple Chrome Profiles are open. Run 'bp profiles --identify', then 'bp profile <index>'.`
|
|
3757
2083
|
);
|
|
3758
2084
|
return;
|
|
3759
2085
|
}
|
|
@@ -3765,31 +2091,14 @@ Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.
|
|
|
3765
2091
|
Ready! Try: bp open https://example.com`
|
|
3766
2092
|
);
|
|
3767
2093
|
}));
|
|
3768
|
-
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) => {
|
|
3769
|
-
if (!opts.stdio) {
|
|
3770
|
-
process.stderr.write("bridge currently requires --stdio\n");
|
|
3771
|
-
process.exitCode = 2;
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3774
|
-
void runStdioBridge({
|
|
3775
|
-
input: process.stdin,
|
|
3776
|
-
output: process.stdout,
|
|
3777
|
-
backend: new DaemonBridgeBackend(opts.browser)
|
|
3778
|
-
}).then((result) => {
|
|
3779
|
-
process.exitCode = result.exitCode;
|
|
3780
|
-
}).catch((error) => {
|
|
3781
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3782
|
-
process.stderr.write(`Bridge error: ${message}
|
|
3783
|
-
`);
|
|
3784
|
-
process.exitCode = 1;
|
|
3785
|
-
});
|
|
3786
|
-
});
|
|
3787
2094
|
program.command("disconnect").description("Release CLI browser state and stop an otherwise unused daemon").action(action(async () => {
|
|
3788
|
-
await shutdownCompatibility(BROWSER_PILOT_VERSION);
|
|
2095
|
+
await shutdownCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
|
|
3789
2096
|
emit({ ok: true }, "\u2713 Disconnected");
|
|
3790
2097
|
}));
|
|
3791
|
-
program.command("profiles").description("List live Chrome Profile contexts").action(action(async () => {
|
|
3792
|
-
const
|
|
2098
|
+
program.command("profiles").description("List live Chrome Profile contexts").option("--identify", "explicitly identify Profiles using temporary visible Chrome pages").option("--refresh", "repeat Profile identification instead of using cached results").action(action(async (opts) => {
|
|
2099
|
+
const client = await requireCompatibility();
|
|
2100
|
+
const identified = opts.identify || opts.refresh ? await client.identifyProfiles(void 0, opts.refresh === true) : await client.listProfiles();
|
|
2101
|
+
const profiles = identified.map(cliProfile);
|
|
3793
2102
|
if (useJson()) {
|
|
3794
2103
|
console.log(JSON.stringify({ ok: true, profiles }));
|
|
3795
2104
|
return;
|
|
@@ -3799,8 +2108,10 @@ program.command("profiles").description("List live Chrome Profile contexts").act
|
|
|
3799
2108
|
return;
|
|
3800
2109
|
}
|
|
3801
2110
|
for (const profile of profiles) {
|
|
3802
|
-
const name = profile.
|
|
3803
|
-
|
|
2111
|
+
const name = profile.profileName ?? profile.displayName;
|
|
2112
|
+
const account = profile.accountEmail ?? profile.accountName;
|
|
2113
|
+
const identity = name ? ` ${name}${account ? ` (${account})` : ""} [${profile.label}]` : ` ${profile.label}${profile.identityStatus === "unavailable" ? " (identity unavailable)" : ""}`;
|
|
2114
|
+
console.log(`${profile.selected ? "*" : " "} ${profile.index}${identity} ${profile.tabCount} tab(s)`);
|
|
3804
2115
|
}
|
|
3805
2116
|
}));
|
|
3806
2117
|
program.command("profile <selector>").description("Select a Chrome Profile context for new managed tabs").action(action(async (selector) => {
|
|
@@ -3812,15 +2123,21 @@ program.command("profile <selector>").description("Select a Chrome Profile conte
|
|
|
3812
2123
|
ok: true,
|
|
3813
2124
|
profileContextId: selected.profileContextId,
|
|
3814
2125
|
label: selected.label,
|
|
3815
|
-
...selected.displayName ? { displayName: selected.displayName } : {}
|
|
2126
|
+
...selected.displayName ? { displayName: selected.displayName } : {},
|
|
2127
|
+
...selected.identityStatus ? { identityStatus: selected.identityStatus } : {},
|
|
2128
|
+
...selected.profileName ? { profileName: selected.profileName } : {},
|
|
2129
|
+
...selected.accountName ? { accountName: selected.accountName } : {},
|
|
2130
|
+
...selected.accountEmail ? { accountEmail: selected.accountEmail } : {},
|
|
2131
|
+
...selected.profileDirectory ? { profileDirectory: selected.profileDirectory } : {},
|
|
2132
|
+
...selected.identityErrorCode ? { identityErrorCode: selected.identityErrorCode } : {}
|
|
3816
2133
|
},
|
|
3817
|
-
`\u2713 Selected ${selected.displayName ?? selected.label}`
|
|
2134
|
+
`\u2713 Selected ${selected.profileName ?? selected.displayName ?? selected.label}`
|
|
3818
2135
|
);
|
|
3819
2136
|
}));
|
|
3820
|
-
program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("--profile <selector>", "Profile index, ID, label,
|
|
2137
|
+
program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("--profile <selector>", "Profile index, ID, label, verified name, or email (requires --new)").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --new --profile 1\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
|
|
3821
2138
|
url = normalizeUrl(url);
|
|
3822
2139
|
const limit = parseLimit(opts.limit);
|
|
3823
|
-
if (opts.profile && !opts.new) throw
|
|
2140
|
+
if (opts.profile && !opts.new) throw invalidArgument("--profile requires --new", "profile");
|
|
3824
2141
|
if (opts.new) {
|
|
3825
2142
|
const client = await requireCompatibility();
|
|
3826
2143
|
const profile = opts.profile ? await resolveCliProfile(client, String(opts.profile)) : void 0;
|
|
@@ -3854,20 +2171,22 @@ Examples:
|
|
|
3854
2171
|
bp click --xy 400,300 --double # double-click at coordinates
|
|
3855
2172
|
bp click --xy 400,300 --right # right-click (context menu)
|
|
3856
2173
|
bp click 3 --right # right-click element [3]`).action(action(async (ref, opts) => {
|
|
3857
|
-
if (opts.double && opts.right)
|
|
3858
|
-
|
|
2174
|
+
if (opts.double && opts.right) {
|
|
2175
|
+
throw invalidArgument("--double and --right are mutually exclusive", "button");
|
|
2176
|
+
}
|
|
2177
|
+
if (!ref && !opts.xy) throw invalidArgument("Provide a ref number or --xy coordinates", "target");
|
|
2178
|
+
if (ref && opts.xy) throw invalidArgument("Provide either a ref number or --xy, not both", "target");
|
|
3859
2179
|
const limit = parseLimit(opts.limit);
|
|
2180
|
+
const coordinates = opts.xy ? parseCoordinates(opts.xy) : void 0;
|
|
2181
|
+
const parsedRef = ref ? parseRef(ref) : void 0;
|
|
3860
2182
|
await withCliTarget(async (client, controlledTarget) => {
|
|
3861
2183
|
let target;
|
|
3862
|
-
if (
|
|
3863
|
-
|
|
3864
|
-
const x = parseFloat(xStr), y = parseFloat(yStr);
|
|
3865
|
-
if (isNaN(x) || isNaN(y)) throw new Error("--xy must be x,y (e.g. --xy 400,300)");
|
|
3866
|
-
target = { x, y };
|
|
2184
|
+
if (coordinates) {
|
|
2185
|
+
target = coordinates;
|
|
3867
2186
|
} else {
|
|
3868
2187
|
target = {
|
|
3869
2188
|
observationId: await client.latestObservation(controlledTarget.targetId),
|
|
3870
|
-
ref:
|
|
2189
|
+
ref: parsedRef
|
|
3871
2190
|
};
|
|
3872
2191
|
}
|
|
3873
2192
|
const result = await client.callTool("browser.click", {
|
|
@@ -3906,10 +2225,11 @@ Examples:
|
|
|
3906
2225
|
}));
|
|
3907
2226
|
program.command("type <ref> <text>").description("Type text into element and return page snapshot").option("-c, --clear", "clear field before typing").option("-s, --submit", "press Enter after typing").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", '\nExamples:\n bp type 2 "hello world"\n bp type 5 "query" --submit\n bp type 3 "new value" --clear').action(action(async (ref, text, opts) => {
|
|
3908
2227
|
const limit = parseLimit(opts.limit);
|
|
2228
|
+
const parsedRef = parseRef(ref);
|
|
3909
2229
|
await withCliTarget(async (client, target) => {
|
|
3910
2230
|
const result = await client.callTool("browser.type", {
|
|
3911
2231
|
observationId: await client.latestObservation(target.targetId),
|
|
3912
|
-
ref:
|
|
2232
|
+
ref: parsedRef,
|
|
3913
2233
|
text,
|
|
3914
2234
|
...opts.clear ? { clear: true } : {},
|
|
3915
2235
|
...opts.submit ? { submit: true } : {},
|
|
@@ -3932,17 +2252,19 @@ Examples:
|
|
|
3932
2252
|
bp keyboard "Hello Docs!" --click ".kix-appview-editor"
|
|
3933
2253
|
bp keyboard "slow typing" --delay 50`).action(action(async (text, opts) => {
|
|
3934
2254
|
const limit = parseLimit(opts.limit);
|
|
3935
|
-
let
|
|
2255
|
+
let delay3 = 0;
|
|
3936
2256
|
if (opts.delay) {
|
|
3937
|
-
|
|
3938
|
-
if (
|
|
2257
|
+
delay3 = Number(opts.delay);
|
|
2258
|
+
if (!/^\d+$/.test(opts.delay) || !Number.isSafeInteger(delay3) || delay3 < 0) {
|
|
2259
|
+
throw invalidArgument("--delay must be a non-negative integer", "delay");
|
|
2260
|
+
}
|
|
3939
2261
|
}
|
|
3940
2262
|
await withCliTarget(async (client, target) => {
|
|
3941
2263
|
const result = await client.callTool("browser.keyboard", {
|
|
3942
2264
|
text,
|
|
3943
2265
|
...opts.clear ? { clear: true } : {},
|
|
3944
2266
|
...opts.submit ? { submit: true } : {},
|
|
3945
|
-
delayMs:
|
|
2267
|
+
delayMs: delay3,
|
|
3946
2268
|
...opts.click ? { focusSelector: opts.click } : {},
|
|
3947
2269
|
observationLimit: limit
|
|
3948
2270
|
}, target.targetId);
|
|
@@ -3982,7 +2304,9 @@ Examples:
|
|
|
3982
2304
|
echo 'complex js' | bp eval`).action(action(async (expression) => {
|
|
3983
2305
|
if (!expression) {
|
|
3984
2306
|
expression = await readStdin();
|
|
3985
|
-
if (!expression)
|
|
2307
|
+
if (!expression) {
|
|
2308
|
+
throw invalidArgument("No expression. Pass as argument or pipe via stdin.", "expression");
|
|
2309
|
+
}
|
|
3986
2310
|
}
|
|
3987
2311
|
await withCliTarget(async (client, target) => {
|
|
3988
2312
|
const result = await client.callTool("browser.eval", {
|
|
@@ -4014,8 +2338,10 @@ When to use which command:
|
|
|
4014
2338
|
bp snapshot \u2192 list interactive elements (buttons, links, inputs)
|
|
4015
2339
|
bp read \u2192 page text content (search results, articles)
|
|
4016
2340
|
bp eval \u2192 custom extraction (structured data, attributes)`).action(action(async (selector, opts) => {
|
|
4017
|
-
const limit =
|
|
4018
|
-
if (
|
|
2341
|
+
const limit = Number(opts.limit);
|
|
2342
|
+
if (!/^\d+$/.test(opts.limit) || !Number.isSafeInteger(limit) || limit < 1) {
|
|
2343
|
+
throw invalidArgument("--limit must be a positive integer", "limit");
|
|
2344
|
+
}
|
|
4019
2345
|
await withCliTarget(async (client, target) => {
|
|
4020
2346
|
let data;
|
|
4021
2347
|
try {
|
|
@@ -4096,15 +2422,25 @@ Examples:
|
|
|
4096
2422
|
bp scroll down --selector ".results"
|
|
4097
2423
|
bp scroll --ref 8 --to end
|
|
4098
2424
|
bp scroll --to-text "Payment details"`).action(action(async (direction, opts) => {
|
|
4099
|
-
if (opts.selector && opts.ref)
|
|
2425
|
+
if (opts.selector && opts.ref) {
|
|
2426
|
+
throw invalidArgument("--selector and --ref are mutually exclusive", "target");
|
|
2427
|
+
}
|
|
4100
2428
|
const modes = [direction !== void 0, opts.to !== void 0, opts.toText !== void 0].filter(Boolean).length;
|
|
4101
|
-
if (modes > 1) throw
|
|
2429
|
+
if (modes > 1) throw invalidArgument("Use only one of direction, --to, or --to-text", "mode");
|
|
4102
2430
|
const validDirections = /* @__PURE__ */ new Set(["up", "down", "left", "right"]);
|
|
4103
|
-
if (direction !== void 0 && !validDirections.has(direction))
|
|
4104
|
-
|
|
4105
|
-
|
|
2431
|
+
if (direction !== void 0 && !validDirections.has(direction)) {
|
|
2432
|
+
throw invalidArgument("direction must be up, down, left, or right", "direction");
|
|
2433
|
+
}
|
|
2434
|
+
if (opts.to !== void 0 && !["start", "end"].includes(opts.to)) {
|
|
2435
|
+
throw invalidArgument("--to must be start or end", "to");
|
|
2436
|
+
}
|
|
2437
|
+
if (!["pixels", "viewport"].includes(opts.unit)) {
|
|
2438
|
+
throw invalidArgument("--unit must be pixels or viewport", "unit");
|
|
2439
|
+
}
|
|
4106
2440
|
const amount = opts.amount === void 0 ? void 0 : Number(opts.amount);
|
|
4107
|
-
if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0))
|
|
2441
|
+
if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0)) {
|
|
2442
|
+
throw invalidArgument("--amount must be a positive number", "amount");
|
|
2443
|
+
}
|
|
4108
2444
|
const limit = parseLimit(opts.limit);
|
|
4109
2445
|
await withCliTarget(async (client, target) => {
|
|
4110
2446
|
const rawTarget = opts.selector ?? opts.ref;
|
|
@@ -4141,7 +2477,9 @@ program.command("dropdown <target>").description("List native or ARIA dropdown o
|
|
|
4141
2477
|
});
|
|
4142
2478
|
}));
|
|
4143
2479
|
program.command("select <target> <option>").description("Select and verify a dropdown option").option("--by <mode>", "match by label, value, or index", "label").option("--contains", "use case-insensitive substring matching").option("-l, --limit <n>", "max elements in returned snapshot", "50").addHelpText("after", '\nExamples:\n bp select 4 "United States"\n bp select 4 us --by value\n bp select 4 3 --by index').action(action(async (rawTarget, rawOption, opts) => {
|
|
4144
|
-
if (!["label", "value", "index"].includes(opts.by))
|
|
2480
|
+
if (!["label", "value", "index"].includes(opts.by)) {
|
|
2481
|
+
throw invalidArgument("--by must be label, value, or index", "by");
|
|
2482
|
+
}
|
|
4145
2483
|
const choice = opts.by === "index" ? { by: "index", index: parseRef(rawOption) } : { by: opts.by, [opts.by]: rawOption, exact: opts.contains !== true };
|
|
4146
2484
|
const limit = parseLimit(opts.limit);
|
|
4147
2485
|
await withCliTarget(async (client, target) => {
|
|
@@ -4155,9 +2493,11 @@ program.command("select <target> <option>").description("Select and verify a dro
|
|
|
4155
2493
|
}));
|
|
4156
2494
|
program.command("upload <filepath>").description('Upload file (auto-finds <input type="file"> on the page)').option("--nth <n>", "which file input to use if multiple exist", "1").addHelpText("after", "\nAuto-detects file inputs on the page. No ref needed.\n\nExamples:\n bp upload ./photo.jpg\n bp upload /tmp/resume.pdf\n bp upload ./doc.pdf --nth 2 # if multiple file inputs").action(action(async (filepath, opts) => {
|
|
4157
2495
|
const absPath = resolvePath(filepath);
|
|
4158
|
-
if (!existsSync4(absPath)) throw
|
|
4159
|
-
const inputIndex =
|
|
4160
|
-
if (
|
|
2496
|
+
if (!existsSync4(absPath)) throw invalidArgument(`File not found: ${absPath}`, "filepath");
|
|
2497
|
+
const inputIndex = Number(opts.nth);
|
|
2498
|
+
if (!/^\d+$/.test(opts.nth) || !Number.isSafeInteger(inputIndex) || inputIndex < 1) {
|
|
2499
|
+
throw invalidArgument("--nth must be a positive integer", "nth");
|
|
2500
|
+
}
|
|
4161
2501
|
await withCliTarget(async (client, target) => {
|
|
4162
2502
|
const artifact = await client.importArtifact(absPath);
|
|
4163
2503
|
try {
|
|
@@ -4173,9 +2513,46 @@ program.command("upload <filepath>").description('Upload file (auto-finds <input
|
|
|
4173
2513
|
}
|
|
4174
2514
|
});
|
|
4175
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
|
+
}));
|
|
4176
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) => {
|
|
4177
2554
|
if (opts.annotate !== void 0 && (opts.full || opts.selector)) {
|
|
4178
|
-
throw
|
|
2555
|
+
throw invalidArgument("--annotate cannot be combined with --full or --selector", "annotate");
|
|
4179
2556
|
}
|
|
4180
2557
|
await withCliTarget(async (client, target) => {
|
|
4181
2558
|
let annotations;
|
|
@@ -4194,9 +2571,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
|
|
|
4194
2571
|
}, target.targetId);
|
|
4195
2572
|
const artifact = artifactFrom(result);
|
|
4196
2573
|
const file = filename ?? `screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.png`;
|
|
4197
|
-
const
|
|
2574
|
+
const destination = outputPath(file);
|
|
4198
2575
|
try {
|
|
4199
|
-
await client.exportArtifact(artifact.id,
|
|
2576
|
+
await client.exportArtifact(artifact.id, destination);
|
|
4200
2577
|
} finally {
|
|
4201
2578
|
await client.releaseArtifact(artifact.id).catch(() => {
|
|
4202
2579
|
});
|
|
@@ -4208,9 +2585,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
|
|
|
4208
2585
|
}
|
|
4209
2586
|
emit({
|
|
4210
2587
|
ok: true,
|
|
4211
|
-
|
|
2588
|
+
...artifactFileResult(artifact, destination),
|
|
4212
2589
|
...typeof result.annotationCount === "number" ? { annotationCount: result.annotationCount } : {}
|
|
4213
|
-
}, `\u2713 Screenshot saved to ${
|
|
2590
|
+
}, `\u2713 Screenshot saved to ${destination}`);
|
|
4214
2591
|
});
|
|
4215
2592
|
}));
|
|
4216
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) => {
|
|
@@ -4220,14 +2597,14 @@ program.command("pdf [filename]").description("Save page as PDF").option("--land
|
|
|
4220
2597
|
}, target.targetId);
|
|
4221
2598
|
const artifact = artifactFrom(result);
|
|
4222
2599
|
const file = filename ?? `page-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.pdf`;
|
|
4223
|
-
const
|
|
2600
|
+
const destination = outputPath(file);
|
|
4224
2601
|
try {
|
|
4225
|
-
await client.exportArtifact(artifact.id,
|
|
2602
|
+
await client.exportArtifact(artifact.id, destination);
|
|
4226
2603
|
} finally {
|
|
4227
2604
|
await client.releaseArtifact(artifact.id).catch(() => {
|
|
4228
2605
|
});
|
|
4229
2606
|
}
|
|
4230
|
-
emit({ ok: true,
|
|
2607
|
+
emit({ ok: true, ...artifactFileResult(artifact, destination) }, `\u2713 PDF saved to ${destination}`);
|
|
4231
2608
|
});
|
|
4232
2609
|
}));
|
|
4233
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) => {
|
|
@@ -4263,12 +2640,15 @@ program.command("frame [target]").description("List frames, or switch to a frame
|
|
|
4263
2640
|
}
|
|
4264
2641
|
}
|
|
4265
2642
|
} else {
|
|
4266
|
-
const idx =
|
|
2643
|
+
const idx = Number(target);
|
|
4267
2644
|
if (!Number.isSafeInteger(idx) || idx < 0 || idx >= frames.length) {
|
|
4268
|
-
throw
|
|
2645
|
+
throw invalidArgument(
|
|
2646
|
+
`Frame index out of range (0-${Math.max(0, frames.length - 1)})`,
|
|
2647
|
+
"target"
|
|
2648
|
+
);
|
|
4269
2649
|
}
|
|
4270
2650
|
const frame = frames[idx];
|
|
4271
|
-
await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId:
|
|
2651
|
+
await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString(frame.frameId, "frameId") }, controlledTarget.targetId);
|
|
4272
2652
|
emit(
|
|
4273
2653
|
{ ok: true, frame: idx, url: frame.url },
|
|
4274
2654
|
`\u2713 Switched to frame ${idx}: ${frame.url}`
|
|
@@ -4283,7 +2663,7 @@ program.command("auth [username] [password]").description("Set or clear HTTP Bas
|
|
|
4283
2663
|
emit({ ok: true }, "\u2713 Auth credentials cleared");
|
|
4284
2664
|
return;
|
|
4285
2665
|
}
|
|
4286
|
-
if (!password) throw
|
|
2666
|
+
if (!password) throw invalidArgument("Usage: bp auth <username> <password>", "password");
|
|
4287
2667
|
await client.callTool("browser.auth.set", { username, password });
|
|
4288
2668
|
emit({ ok: true }, "\u2713 Auth credentials set (scoped to HTTP 401 challenges)");
|
|
4289
2669
|
}));
|
|
@@ -4302,7 +2682,7 @@ program.command("dialogs").description("List pending JavaScript dialogs").action
|
|
|
4302
2682
|
}));
|
|
4303
2683
|
program.command("dialog <dialogId>").description("Accept or dismiss a pending JavaScript dialog").option("--accept", "accept the dialog").option("--dismiss", "dismiss the dialog").option("--prompt <text>", "text to submit to a prompt dialog").action(action(async (dialogId, opts) => {
|
|
4304
2684
|
if (Boolean(opts.accept) === Boolean(opts.dismiss)) {
|
|
4305
|
-
throw
|
|
2685
|
+
throw invalidArgument("Choose exactly one of --accept or --dismiss", "action");
|
|
4306
2686
|
}
|
|
4307
2687
|
const client = await requireCompatibility();
|
|
4308
2688
|
const listed = await client.callTool("browser.dialogs.list");
|
|
@@ -4316,7 +2696,7 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
|
|
|
4316
2696
|
dialogId,
|
|
4317
2697
|
action: opts.accept ? "accept" : "dismiss",
|
|
4318
2698
|
...opts.prompt !== void 0 ? { promptText: opts.prompt } : {}
|
|
4319
|
-
},
|
|
2699
|
+
}, requireString(dialog.targetId, "dialog targetId"));
|
|
4320
2700
|
emit(
|
|
4321
2701
|
{ ok: true, dialogId: result.dialogId, action: result.action },
|
|
4322
2702
|
`\u2713 ${result.action === "accept" ? "Accepted" : "Dismissed"} dialog ${result.dialogId}`
|
|
@@ -4324,9 +2704,10 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
|
|
|
4324
2704
|
}));
|
|
4325
2705
|
program.command("tabs").description("List all controllable browser tabs").action(action(async () => {
|
|
4326
2706
|
const targets = await (await requireCompatibility()).listTabs("all");
|
|
4327
|
-
const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, ...tab }, index) => ({
|
|
4328
|
-
index,
|
|
2707
|
+
const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, active, selected, ...tab }, index) => ({
|
|
2708
|
+
index: index + 1,
|
|
4329
2709
|
...tab,
|
|
2710
|
+
selected: selected ?? active === true,
|
|
4330
2711
|
controlState
|
|
4331
2712
|
}));
|
|
4332
2713
|
if (useJson()) {
|
|
@@ -4334,20 +2715,20 @@ program.command("tabs").description("List all controllable browser tabs").action
|
|
|
4334
2715
|
} else if (tabs.length === 0) {
|
|
4335
2716
|
console.log("No controllable tabs open.");
|
|
4336
2717
|
} else {
|
|
4337
|
-
for (const t of tabs) console.log(`${t.
|
|
2718
|
+
for (const t of tabs) console.log(`${t.selected ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
|
|
4338
2719
|
}
|
|
4339
2720
|
}));
|
|
4340
|
-
program.command("tab <index>").description("
|
|
2721
|
+
program.command("tab <index>").description("Select a tab by one-based index").action(action(async (indexStr) => {
|
|
4341
2722
|
const client = await requireCompatibility();
|
|
4342
|
-
const index =
|
|
2723
|
+
const index = Number(indexStr);
|
|
4343
2724
|
const targets = await client.listTabs("all");
|
|
4344
|
-
if (!Number.isSafeInteger(index) || index <
|
|
4345
|
-
throw
|
|
2725
|
+
if (!Number.isSafeInteger(index) || index < 1 || index > targets.length) {
|
|
2726
|
+
throw invalidArgument(`Tab index out of range (1-${targets.length})`, "index");
|
|
4346
2727
|
}
|
|
4347
|
-
await client.callTool("browser.tabs.switch", { targetId: targets[index].targetId });
|
|
4348
|
-
emit({ ok: true, index }, `\u2713
|
|
2728
|
+
await client.callTool("browser.tabs.switch", { targetId: targets[index - 1].targetId });
|
|
2729
|
+
emit({ ok: true, index }, `\u2713 Selected tab ${index}`);
|
|
4349
2730
|
}));
|
|
4350
|
-
program.command("close").description("Close current browser tab").option("-a, --all", "close all tabs in
|
|
2731
|
+
program.command("close").description("Close current browser tab").option("-a, --all", "close all managed tabs in this Agent Workspace").action(action(async (opts) => {
|
|
4351
2732
|
const client = await requireCompatibility();
|
|
4352
2733
|
if (opts.all) {
|
|
4353
2734
|
const managed = await client.listTabs("managed_only");
|
|
@@ -4377,7 +2758,7 @@ program.command("close").description("Close current browser tab").option("-a, --
|
|
|
4377
2758
|
await client.callTool("browser.tabs.close", {}, target.targetId);
|
|
4378
2759
|
const remainingTabs = await client.listTabs("all");
|
|
4379
2760
|
if (remainingTabs.length > 0) {
|
|
4380
|
-
if (!remainingTabs.some((tab) => tab.active)) {
|
|
2761
|
+
if (!remainingTabs.some((tab) => tab.selected ?? tab.active === true)) {
|
|
4381
2762
|
const fallback = remainingTabs.find((tab) => tab.origin !== "user_tab");
|
|
4382
2763
|
if (fallback) await client.callTool("browser.tabs.switch", { targetId: fallback.targetId });
|
|
4383
2764
|
}
|
|
@@ -4404,7 +2785,7 @@ function cliNetworkRequest(request) {
|
|
|
4404
2785
|
}
|
|
4405
2786
|
async function findNetworkRequest(client, sequence) {
|
|
4406
2787
|
if (!Number.isSafeInteger(sequence) || sequence < 1) {
|
|
4407
|
-
throw
|
|
2788
|
+
throw invalidArgument("Request ID must be a positive integer", "id");
|
|
4408
2789
|
}
|
|
4409
2790
|
const listed = await client.callTool("browser.network.requests", {
|
|
4410
2791
|
after: sequence - 1,
|
|
@@ -4428,14 +2809,18 @@ Examples:
|
|
|
4428
2809
|
bp net rules # list active rules
|
|
4429
2810
|
bp net remove --all # clear rules`).action(action(async (opts) => {
|
|
4430
2811
|
const client = await requireCompatibility();
|
|
4431
|
-
const limit =
|
|
2812
|
+
const limit = parseLimit(opts.limit ?? "20");
|
|
2813
|
+
const after = opts.after === void 0 ? void 0 : Number(opts.after);
|
|
2814
|
+
if (after !== void 0 && (!/^\d+$/.test(opts.after) || !Number.isSafeInteger(after))) {
|
|
2815
|
+
throw invalidArgument("--after must be a non-negative integer", "after");
|
|
2816
|
+
}
|
|
4432
2817
|
const result = await client.callTool("browser.network.requests", {
|
|
4433
2818
|
limit,
|
|
4434
2819
|
...opts.url ? { url: opts.url } : {},
|
|
4435
2820
|
...opts.method ? { method: opts.method } : {},
|
|
4436
2821
|
...opts.status ? { status: opts.status } : {},
|
|
4437
2822
|
...opts.type ? { type: String(opts.type).split(",").map((value) => value.trim()).filter(Boolean) } : {},
|
|
4438
|
-
...
|
|
2823
|
+
...after !== void 0 ? { after } : {}
|
|
4439
2824
|
});
|
|
4440
2825
|
const requests = networkRequests(result).map(cliNetworkRequest);
|
|
4441
2826
|
if (useJson()) {
|
|
@@ -4459,20 +2844,31 @@ Examples:
|
|
|
4459
2844
|
}));
|
|
4460
2845
|
netCmd.command("show <id>").description("Show full request/response details").option("--save <file>", "save response body to file").action(action(async (idStr, opts) => {
|
|
4461
2846
|
const client = await requireCompatibility();
|
|
4462
|
-
const id =
|
|
2847
|
+
const id = Number(idStr);
|
|
4463
2848
|
const summary = await findNetworkRequest(client, id);
|
|
4464
2849
|
const result = await client.callTool("browser.network.request", {
|
|
4465
|
-
requestId:
|
|
2850
|
+
requestId: requireString(summary.requestId, "requestId"),
|
|
4466
2851
|
includeBody: true
|
|
4467
2852
|
});
|
|
4468
2853
|
const request = result.request && typeof result.request === "object" && !Array.isArray(result.request) ? result.request : {};
|
|
4469
2854
|
const responseBody = typeof result.body === "string" ? result.body : void 0;
|
|
4470
2855
|
if (opts.save) {
|
|
4471
|
-
if (responseBody === void 0)
|
|
4472
|
-
|
|
2856
|
+
if (responseBody === void 0) {
|
|
2857
|
+
throw new BrowserPilotError(
|
|
2858
|
+
"action_not_verified",
|
|
2859
|
+
`Response body for request #${id} is unavailable`,
|
|
2860
|
+
{ context: { sequence: id } }
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
const destination = outputPath(opts.save);
|
|
4473
2864
|
const bytes = result.bodyEncoding === "base64" ? Buffer.from(responseBody, "base64") : Buffer.from(responseBody, "utf8");
|
|
4474
|
-
writeFileSync2(
|
|
4475
|
-
emit({
|
|
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}`);
|
|
4476
2872
|
return;
|
|
4477
2873
|
}
|
|
4478
2874
|
const detail = { id: request.sequence, ...request, responseBody };
|
|
@@ -4497,15 +2893,18 @@ netCmd.command("block <pattern>").description("Block requests matching URL patte
|
|
|
4497
2893
|
const rule = { id: result.ruleId, type: "block", pattern };
|
|
4498
2894
|
emit({ ok: true, rule }, `Rule #${rule.id}: blocking "${pattern}"`);
|
|
4499
2895
|
}));
|
|
4500
|
-
netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <
|
|
2896
|
+
netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <text>", "response body text").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
|
|
4501
2897
|
const client = await requireCompatibility();
|
|
4502
2898
|
let body = opts.body || "";
|
|
4503
2899
|
if (opts.file) {
|
|
4504
2900
|
const filePath = resolvePath(opts.file);
|
|
4505
|
-
if (!existsSync4(filePath)) throw
|
|
2901
|
+
if (!existsSync4(filePath)) throw invalidArgument(`File not found: ${filePath}`, "file");
|
|
4506
2902
|
body = readFileSync4(filePath, "utf-8");
|
|
4507
2903
|
}
|
|
4508
|
-
const status =
|
|
2904
|
+
const status = Number(opts.status);
|
|
2905
|
+
if (!/^\d+$/.test(opts.status) || !Number.isSafeInteger(status) || status < 100 || status > 599) {
|
|
2906
|
+
throw invalidArgument("--status must be an HTTP status from 100 through 599", "status");
|
|
2907
|
+
}
|
|
4509
2908
|
const result = await client.callTool("browser.network.rules.add", {
|
|
4510
2909
|
type: "mock",
|
|
4511
2910
|
pattern,
|
|
@@ -4552,10 +2951,32 @@ netCmd.command("remove [ruleId]").description("Remove interception rule(s)").opt
|
|
|
4552
2951
|
} else if (ruleId) {
|
|
4553
2952
|
await client.callTool("browser.network.rules.remove", { ruleId });
|
|
4554
2953
|
emit({ ok: true }, `Rule #${ruleId} removed`);
|
|
4555
|
-
} else throw
|
|
2954
|
+
} else throw invalidArgument("Specify a rule ID or use --all", "ruleId");
|
|
4556
2955
|
}));
|
|
4557
2956
|
netCmd.command("clear").description("Clear captured request log").action(action(async () => {
|
|
4558
2957
|
await (await requireCompatibility()).callTool("browser.network.clear");
|
|
4559
2958
|
emit({ ok: true }, "Request log cleared");
|
|
4560
2959
|
}));
|
|
4561
|
-
|
|
2960
|
+
async function main() {
|
|
2961
|
+
try {
|
|
2962
|
+
await program.parseAsync();
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
if (error instanceof CommanderError) {
|
|
2965
|
+
if (error.exitCode === 0) {
|
|
2966
|
+
process.exitCode = 0;
|
|
2967
|
+
} else if (useJson()) {
|
|
2968
|
+
const message = error.message.replace(/^error:\s*/i, "");
|
|
2969
|
+
const stable = new BrowserPilotError("invalid_argument", message, {
|
|
2970
|
+
context: { parserCode: error.code }
|
|
2971
|
+
});
|
|
2972
|
+
fail(message, void 0, stable);
|
|
2973
|
+
} else {
|
|
2974
|
+
process.exitCode = error.exitCode;
|
|
2975
|
+
}
|
|
2976
|
+
} else {
|
|
2977
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2978
|
+
fail(message, void 0, error instanceof BrowserPilotError ? error : void 0);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
void main();
|