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