@flrande/browserctl 0.1.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/LICENSE +21 -0
- package/README-CN.md +1155 -0
- package/README.md +1155 -0
- package/apps/browserctl/src/commands/a11y-snapshot.ts +20 -0
- package/apps/browserctl/src/commands/act.ts +20 -0
- package/apps/browserctl/src/commands/common.test.ts +87 -0
- package/apps/browserctl/src/commands/common.ts +191 -0
- package/apps/browserctl/src/commands/console-list.ts +20 -0
- package/apps/browserctl/src/commands/cookie-clear.ts +18 -0
- package/apps/browserctl/src/commands/cookie-get.ts +18 -0
- package/apps/browserctl/src/commands/cookie-set.ts +22 -0
- package/apps/browserctl/src/commands/dialog-arm.ts +20 -0
- package/apps/browserctl/src/commands/dom-query-all.ts +18 -0
- package/apps/browserctl/src/commands/dom-query.ts +18 -0
- package/apps/browserctl/src/commands/download-trigger.ts +22 -0
- package/apps/browserctl/src/commands/download-wait.test.ts +67 -0
- package/apps/browserctl/src/commands/download-wait.ts +27 -0
- package/apps/browserctl/src/commands/element-screenshot.ts +20 -0
- package/apps/browserctl/src/commands/frame-list.ts +16 -0
- package/apps/browserctl/src/commands/frame-snapshot.ts +18 -0
- package/apps/browserctl/src/commands/network-wait-for.ts +100 -0
- package/apps/browserctl/src/commands/profile-list.ts +16 -0
- package/apps/browserctl/src/commands/profile-use.ts +18 -0
- package/apps/browserctl/src/commands/response-body.ts +24 -0
- package/apps/browserctl/src/commands/screenshot.ts +16 -0
- package/apps/browserctl/src/commands/snapshot.ts +16 -0
- package/apps/browserctl/src/commands/status.ts +10 -0
- package/apps/browserctl/src/commands/storage-get.ts +20 -0
- package/apps/browserctl/src/commands/storage-set.ts +22 -0
- package/apps/browserctl/src/commands/tab-close.ts +20 -0
- package/apps/browserctl/src/commands/tab-focus.ts +20 -0
- package/apps/browserctl/src/commands/tab-open.ts +19 -0
- package/apps/browserctl/src/commands/tabs.ts +13 -0
- package/apps/browserctl/src/commands/upload-arm.ts +26 -0
- package/apps/browserctl/src/daemon-client.test.ts +253 -0
- package/apps/browserctl/src/daemon-client.ts +632 -0
- package/apps/browserctl/src/e2e.test.ts +99 -0
- package/apps/browserctl/src/main.test.ts +215 -0
- package/apps/browserctl/src/main.ts +372 -0
- package/apps/browserctl/src/smoke.test.ts +16 -0
- package/apps/browserctl/src/smoke.ts +5 -0
- package/apps/browserd/src/bootstrap.ts +432 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +275 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +506 -0
- package/apps/browserd/src/container.ts +1531 -0
- package/apps/browserd/src/main.test.ts +864 -0
- package/apps/browserd/src/main.ts +7 -0
- package/bin/browserctl.cjs +21 -0
- package/bin/browserd.cjs +21 -0
- package/extensions/chrome-relay/README.md +36 -0
- package/extensions/chrome-relay/background.js +1687 -0
- package/extensions/chrome-relay/manifest.json +15 -0
- package/extensions/chrome-relay/popup.html +369 -0
- package/extensions/chrome-relay/popup.js +972 -0
- package/package.json +51 -0
- package/packages/core/src/bootstrap.test.ts +10 -0
- package/packages/core/src/driver-registry.test.ts +45 -0
- package/packages/core/src/driver-registry.ts +22 -0
- package/packages/core/src/driver.ts +47 -0
- package/packages/core/src/index.ts +5 -0
- package/packages/core/src/ref-cache.test.ts +61 -0
- package/packages/core/src/ref-cache.ts +28 -0
- package/packages/core/src/session-store.test.ts +49 -0
- package/packages/core/src/session-store.ts +33 -0
- package/packages/core/src/types.ts +9 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
- package/packages/driver-chrome-relay/src/index.ts +26 -0
- package/packages/driver-managed/src/index.ts +22 -0
- package/packages/driver-managed/src/managed-driver.test.ts +59 -0
- package/packages/driver-managed/src/managed-driver.ts +125 -0
- package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
- package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
- package/packages/driver-remote-cdp/src/index.ts +19 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
- package/packages/protocol/src/envelope.test.ts +25 -0
- package/packages/protocol/src/envelope.ts +31 -0
- package/packages/protocol/src/errors.test.ts +17 -0
- package/packages/protocol/src/errors.ts +11 -0
- package/packages/protocol/src/index.ts +3 -0
- package/packages/protocol/src/tools.ts +3 -0
- package/packages/transport-mcp-stdio/src/index.ts +3 -0
- package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
- package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
- package/packages/transport-mcp-stdio/src/server.ts +183 -0
- package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
- package/scripts/smoke.ps1 +127 -0
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DriverRegistry,
|
|
3
|
+
SessionStore,
|
|
4
|
+
type BrowserDriver,
|
|
5
|
+
type BrowserDriverScreenshot
|
|
6
|
+
} from "../../../packages/core/src";
|
|
7
|
+
import {
|
|
8
|
+
createChromeRelayDriver,
|
|
9
|
+
createChromeRelayExtensionRuntime
|
|
10
|
+
} from "../../../packages/driver-chrome-relay/src";
|
|
11
|
+
import {
|
|
12
|
+
createManagedDriver,
|
|
13
|
+
createManagedLocalDriver,
|
|
14
|
+
type ManagedLocalBrowserName,
|
|
15
|
+
type ManagedLocalTelemetryDriverExtensions
|
|
16
|
+
} from "../../../packages/driver-managed/src";
|
|
17
|
+
import { createRemoteCdpDriver } from "../../../packages/driver-remote-cdp/src";
|
|
18
|
+
import { ErrorCode, createErr, createOk, type ToolResponse } from "../../../packages/protocol/src";
|
|
19
|
+
import {
|
|
20
|
+
buildToolMap,
|
|
21
|
+
createMcpStdioServer,
|
|
22
|
+
type McpStdioServer,
|
|
23
|
+
type ToolCallArgs,
|
|
24
|
+
type ToolMap
|
|
25
|
+
} from "../../../packages/transport-mcp-stdio/src";
|
|
26
|
+
import { win32 as windowsPath } from "node:path";
|
|
27
|
+
import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
|
|
28
|
+
|
|
29
|
+
type CapabilityScope = "read" | "act" | "upload" | "download";
|
|
30
|
+
type ChromeRelayMode = "cdp" | "extension";
|
|
31
|
+
|
|
32
|
+
export type BrowserdConfig = {
|
|
33
|
+
chromeRelayUrl: string;
|
|
34
|
+
chromeRelayMode: ChromeRelayMode;
|
|
35
|
+
chromeRelayExtensionToken?: string;
|
|
36
|
+
chromeRelayExtensionRequestTimeoutMs: number;
|
|
37
|
+
remoteCdpUrl: string;
|
|
38
|
+
defaultDriver: string;
|
|
39
|
+
managedLocalEnabled: boolean;
|
|
40
|
+
uploadRoot?: string;
|
|
41
|
+
downloadRoot?: string;
|
|
42
|
+
authToken?: string;
|
|
43
|
+
authScopes: CapabilityScope[];
|
|
44
|
+
managedLocalLaunch: {
|
|
45
|
+
browserName: ManagedLocalBrowserName;
|
|
46
|
+
headless: boolean;
|
|
47
|
+
channel?: string;
|
|
48
|
+
executablePath?: string;
|
|
49
|
+
launchTimeoutMs?: number;
|
|
50
|
+
args: string[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DEFAULT_DRIVER_KEY = "managed";
|
|
55
|
+
const MANAGED_LOCAL_DRIVER_KEY = "managed-local";
|
|
56
|
+
const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
|
|
57
|
+
const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
|
|
58
|
+
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "cdp";
|
|
59
|
+
const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
|
|
60
|
+
const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
|
|
61
|
+
const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
|
|
62
|
+
const ALL_CAPABILITY_SCOPES: readonly CapabilityScope[] = ["read", "act", "upload", "download"];
|
|
63
|
+
const TOOL_SCOPE_BY_NAME: Readonly<Record<string, CapabilityScope>> = {
|
|
64
|
+
"browser.status": "read",
|
|
65
|
+
"browser.profile.list": "read",
|
|
66
|
+
"browser.profile.use": "act",
|
|
67
|
+
"browser.tab.list": "read",
|
|
68
|
+
"browser.tab.open": "act",
|
|
69
|
+
"browser.tab.focus": "act",
|
|
70
|
+
"browser.tab.close": "act",
|
|
71
|
+
"browser.snapshot": "read",
|
|
72
|
+
"browser.screenshot": "read",
|
|
73
|
+
"browser.dom.query": "read",
|
|
74
|
+
"browser.dom.queryAll": "read",
|
|
75
|
+
"browser.element.screenshot": "read",
|
|
76
|
+
"browser.a11y.snapshot": "read",
|
|
77
|
+
"browser.act": "act",
|
|
78
|
+
"browser.upload.arm": "upload",
|
|
79
|
+
"browser.dialog.arm": "act",
|
|
80
|
+
"browser.download.wait": "download",
|
|
81
|
+
"browser.download.trigger": "download",
|
|
82
|
+
"browser.network.waitFor": "read",
|
|
83
|
+
"browser.cookie.get": "read",
|
|
84
|
+
"browser.cookie.set": "act",
|
|
85
|
+
"browser.cookie.clear": "act",
|
|
86
|
+
"browser.storage.get": "read",
|
|
87
|
+
"browser.storage.set": "act",
|
|
88
|
+
"browser.frame.list": "read",
|
|
89
|
+
"browser.frame.snapshot": "read",
|
|
90
|
+
"browser.console.list": "read",
|
|
91
|
+
"browser.network.responseBody": "read"
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function resolveNonEmptyString(value: string | undefined): string | undefined {
|
|
95
|
+
if (value === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trimmedValue = value.trim();
|
|
100
|
+
return trimmedValue.length === 0 ? undefined : trimmedValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseBooleanFlag(value: string | undefined, fallback: boolean): boolean {
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
109
|
+
if (normalizedValue === "1" || normalizedValue === "true" || normalizedValue === "yes" || normalizedValue === "on") {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (normalizedValue === "0" || normalizedValue === "false" || normalizedValue === "no" || normalizedValue === "off") {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return fallback;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseOptionalNumber(value: string | undefined): number | undefined {
|
|
121
|
+
const parsedValue = resolveNonEmptyString(value);
|
|
122
|
+
if (parsedValue === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parsedNumber = Number(parsedValue);
|
|
127
|
+
if (!Number.isFinite(parsedNumber) || parsedNumber < 0) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return parsedNumber;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseCsv(value: string | undefined): string[] {
|
|
135
|
+
const rawValue = resolveNonEmptyString(value);
|
|
136
|
+
if (rawValue === undefined) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return rawValue
|
|
141
|
+
.split(",")
|
|
142
|
+
.map((item) => item.trim())
|
|
143
|
+
.filter((item) => item.length > 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isCapabilityScope(value: string): value is CapabilityScope {
|
|
147
|
+
return (
|
|
148
|
+
value === "read" ||
|
|
149
|
+
value === "act" ||
|
|
150
|
+
value === "upload" ||
|
|
151
|
+
value === "download"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseCapabilityScopes(value: string | undefined): CapabilityScope[] {
|
|
156
|
+
const parsed = parseCsv(value).map((item) => item.toLowerCase());
|
|
157
|
+
if (parsed.length === 0) {
|
|
158
|
+
return [...ALL_CAPABILITY_SCOPES];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const scopes: CapabilityScope[] = [];
|
|
162
|
+
for (const item of parsed) {
|
|
163
|
+
if (!isCapabilityScope(item) || scopes.includes(item)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
scopes.push(item);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return scopes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBrowserName {
|
|
174
|
+
const normalizedValue = value?.trim().toLowerCase();
|
|
175
|
+
if (normalizedValue === "firefox" || normalizedValue === "webkit" || normalizedValue === "chromium") {
|
|
176
|
+
return normalizedValue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return DEFAULT_MANAGED_LOCAL_BROWSER_NAME;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
|
|
183
|
+
const normalizedValue = value?.trim().toLowerCase();
|
|
184
|
+
return normalizedValue === "extension" ? "extension" : DEFAULT_CHROME_RELAY_MODE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type BrowserdContainer = {
|
|
188
|
+
config: BrowserdConfig;
|
|
189
|
+
drivers: Map<string, BrowserDriver>;
|
|
190
|
+
driverRegistry: DriverRegistry;
|
|
191
|
+
sessions: SessionStore;
|
|
192
|
+
mcpServer: McpStdioServer;
|
|
193
|
+
close(): void;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export function loadBrowserdConfig(
|
|
197
|
+
env: Record<string, string | undefined> = process.env
|
|
198
|
+
): BrowserdConfig {
|
|
199
|
+
const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
|
|
200
|
+
const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
|
|
201
|
+
const chromeRelayExtensionToken = resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN);
|
|
202
|
+
if (chromeRelayMode === "extension" && chromeRelayExtensionToken === undefined) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"BROWSERD_CHROME_RELAY_EXTENSION_TOKEN is required when BROWSERD_CHROME_RELAY_MODE=extension."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const defaultDriver =
|
|
209
|
+
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ??
|
|
210
|
+
(managedLocalEnabled ? MANAGED_LOCAL_DRIVER_KEY : DEFAULT_DRIVER_KEY);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
|
|
214
|
+
chromeRelayMode,
|
|
215
|
+
chromeRelayExtensionToken,
|
|
216
|
+
chromeRelayExtensionRequestTimeoutMs:
|
|
217
|
+
parseOptionalNumber(env.BROWSERD_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS) ??
|
|
218
|
+
DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS,
|
|
219
|
+
remoteCdpUrl:
|
|
220
|
+
env.BROWSERD_REMOTE_CDP_URL ?? "http://127.0.0.1:9222/devtools/browser/default",
|
|
221
|
+
defaultDriver,
|
|
222
|
+
managedLocalEnabled,
|
|
223
|
+
uploadRoot: resolveNonEmptyString(env.BROWSERD_UPLOAD_ROOT),
|
|
224
|
+
downloadRoot: resolveNonEmptyString(env.BROWSERD_DOWNLOAD_ROOT),
|
|
225
|
+
authToken: resolveNonEmptyString(env.BROWSERD_AUTH_TOKEN),
|
|
226
|
+
authScopes: parseCapabilityScopes(env.BROWSERD_AUTH_SCOPES),
|
|
227
|
+
managedLocalLaunch: {
|
|
228
|
+
browserName: parseManagedLocalBrowserName(env.BROWSERD_MANAGED_LOCAL_BROWSER),
|
|
229
|
+
headless: parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_HEADLESS, DEFAULT_MANAGED_LOCAL_HEADLESS),
|
|
230
|
+
channel: resolveNonEmptyString(env.BROWSERD_MANAGED_LOCAL_CHANNEL),
|
|
231
|
+
executablePath: resolveNonEmptyString(env.BROWSERD_MANAGED_LOCAL_EXECUTABLE_PATH),
|
|
232
|
+
launchTimeoutMs: parseOptionalNumber(env.BROWSERD_MANAGED_LOCAL_LAUNCH_TIMEOUT_MS),
|
|
233
|
+
args: parseCsv(env.BROWSERD_MANAGED_LOCAL_ARGS)
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
239
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isStringArray(value: unknown): value is string[] {
|
|
243
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function toErrorMessage(error: unknown): string {
|
|
247
|
+
return error instanceof Error ? error.message : "Unexpected browserd failure.";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function mapDriverError(error: unknown): ToolResponse<never> {
|
|
251
|
+
const message = toErrorMessage(error);
|
|
252
|
+
if (message.startsWith("Unknown targetId:")) {
|
|
253
|
+
return createErr(ErrorCode.E_NOT_FOUND, message);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (message.startsWith("Unknown driver:")) {
|
|
257
|
+
return createErr(ErrorCode.E_NOT_FOUND, message);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return createErr(ErrorCode.E_INTERNAL, message);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function requireStringArg(args: ToolCallArgs, key: string): ToolResponse<string> {
|
|
264
|
+
const value = args[key];
|
|
265
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
266
|
+
return createErr(ErrorCode.E_INVALID_ARG, `${key} is required and must be a non-empty string.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return createOk(value.trim());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function resolveTargetId(args: ToolCallArgs, sessions: SessionStore): ToolResponse<string> {
|
|
273
|
+
const explicit = requireStringArg(args, "targetId");
|
|
274
|
+
if (explicit.ok) {
|
|
275
|
+
sessions.useTarget(args.sessionId, explicit.data);
|
|
276
|
+
return explicit;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sessionTarget = sessions.get(args.sessionId)?.targetId;
|
|
280
|
+
if (sessionTarget === undefined) {
|
|
281
|
+
return explicit;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return createOk(sessionTarget);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resolveAction(args: ToolCallArgs): ToolResponse<{ type: string; payload?: Record<string, unknown> }> {
|
|
288
|
+
const rawAction = args.action;
|
|
289
|
+
if (!isObjectRecord(rawAction)) {
|
|
290
|
+
return createErr(ErrorCode.E_INVALID_ARG, "action is required and must be an object.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const actionType = rawAction.type;
|
|
294
|
+
if (typeof actionType !== "string" || actionType.trim().length === 0) {
|
|
295
|
+
return createErr(ErrorCode.E_INVALID_ARG, "action.type is required and must be a non-empty string.");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rawPayload = rawAction.payload;
|
|
299
|
+
if (rawPayload !== undefined && !isObjectRecord(rawPayload)) {
|
|
300
|
+
return createErr(ErrorCode.E_INVALID_ARG, "action.payload must be an object when provided.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return createOk({
|
|
304
|
+
type: actionType.trim(),
|
|
305
|
+
payload: rawPayload as Record<string, unknown> | undefined
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveFiles(args: ToolCallArgs): ToolResponse<string[]> {
|
|
310
|
+
const files = args.files;
|
|
311
|
+
if (!isStringArray(files)) {
|
|
312
|
+
return createErr(ErrorCode.E_INVALID_ARG, "files is required and must be a string array.");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const normalizedFiles = files.map((file) => file.trim());
|
|
316
|
+
if (normalizedFiles.some((file) => file.length === 0)) {
|
|
317
|
+
return createErr(
|
|
318
|
+
ErrorCode.E_INVALID_ARG,
|
|
319
|
+
"files must contain only non-empty string paths."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return createOk(normalizedFiles);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveRequestId(args: ToolCallArgs): ToolResponse<string> {
|
|
327
|
+
return requireStringArg(args, "requestId");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolveOptionalPathArg(args: ToolCallArgs, key: string): ToolResponse<string | undefined> {
|
|
331
|
+
const value = args[key];
|
|
332
|
+
if (value === undefined) {
|
|
333
|
+
return createOk(undefined);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
337
|
+
return createErr(
|
|
338
|
+
ErrorCode.E_INVALID_ARG,
|
|
339
|
+
`${key} must be a non-empty string when provided.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return createOk(value.trim());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function resolveDriver(
|
|
347
|
+
args: ToolCallArgs,
|
|
348
|
+
drivers: Map<string, BrowserDriver>,
|
|
349
|
+
sessions: SessionStore,
|
|
350
|
+
defaultDriverKey: string
|
|
351
|
+
): ToolResponse<{ driverKey: string; driver: BrowserDriver }> {
|
|
352
|
+
const requestedProfile = args.profile;
|
|
353
|
+
if (requestedProfile !== undefined) {
|
|
354
|
+
const requestedDriver = drivers.get(requestedProfile);
|
|
355
|
+
if (requestedDriver === undefined) {
|
|
356
|
+
return createErr(ErrorCode.E_NOT_FOUND, `Unknown driver profile: ${requestedProfile}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
sessions.useProfile(args.sessionId, requestedProfile);
|
|
360
|
+
return createOk({
|
|
361
|
+
driverKey: requestedProfile,
|
|
362
|
+
driver: requestedDriver
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const sessionProfile = sessions.get(args.sessionId)?.profile;
|
|
367
|
+
if (sessionProfile !== undefined) {
|
|
368
|
+
const sessionDriver = drivers.get(sessionProfile);
|
|
369
|
+
if (sessionDriver !== undefined) {
|
|
370
|
+
return createOk({
|
|
371
|
+
driverKey: sessionProfile,
|
|
372
|
+
driver: sessionDriver
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const defaultDriver = drivers.get(defaultDriverKey);
|
|
378
|
+
if (defaultDriver === undefined) {
|
|
379
|
+
return createErr(ErrorCode.E_DRIVER_UNAVAILABLE, `Default driver is not registered: ${defaultDriverKey}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
sessions.useProfile(args.sessionId, defaultDriverKey);
|
|
383
|
+
return createOk({
|
|
384
|
+
driverKey: defaultDriverKey,
|
|
385
|
+
driver: defaultDriver
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function runWithDriver<TData>(
|
|
390
|
+
args: ToolCallArgs,
|
|
391
|
+
drivers: Map<string, BrowserDriver>,
|
|
392
|
+
sessions: SessionStore,
|
|
393
|
+
defaultDriverKey: string,
|
|
394
|
+
operation: (
|
|
395
|
+
driver: BrowserDriver,
|
|
396
|
+
driverKey: string
|
|
397
|
+
) => Promise<ToolResponse<TData>> | ToolResponse<TData>
|
|
398
|
+
): Promise<ToolResponse<TData>> {
|
|
399
|
+
const resolvedDriver = resolveDriver(args, drivers, sessions, defaultDriverKey);
|
|
400
|
+
if (!resolvedDriver.ok) {
|
|
401
|
+
return resolvedDriver;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
return await operation(resolvedDriver.data.driver, resolvedDriver.data.driverKey);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return mapDriverError(error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isTargetKnownSnapshot(snapshot: unknown): boolean {
|
|
412
|
+
if (!isObjectRecord(snapshot)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return snapshot.hasTarget === true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hasConsoleEntriesGetter(
|
|
420
|
+
driver: BrowserDriver
|
|
421
|
+
): driver is BrowserDriver & {
|
|
422
|
+
getConsoleEntries: NonNullable<ManagedLocalTelemetryDriverExtensions["getConsoleEntries"]>;
|
|
423
|
+
} {
|
|
424
|
+
return typeof (driver as ManagedLocalTelemetryDriverExtensions).getConsoleEntries === "function";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function hasNetworkResponseBodyGetter(
|
|
428
|
+
driver: BrowserDriver
|
|
429
|
+
): driver is BrowserDriver & {
|
|
430
|
+
getNetworkResponseBody: NonNullable<ManagedLocalTelemetryDriverExtensions["getNetworkResponseBody"]>;
|
|
431
|
+
} {
|
|
432
|
+
return typeof (driver as ManagedLocalTelemetryDriverExtensions).getNetworkResponseBody === "function";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function hasScreenshotGetter(
|
|
436
|
+
driver: BrowserDriver
|
|
437
|
+
): driver is BrowserDriver & BrowserDriverScreenshot {
|
|
438
|
+
return typeof (driver as Partial<BrowserDriverScreenshot>).screenshot === "function";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function resolveOptionalStringArg(args: ToolCallArgs, key: string): ToolResponse<string | undefined> {
|
|
442
|
+
const value = args[key];
|
|
443
|
+
if (value === undefined) {
|
|
444
|
+
return createOk(undefined);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (typeof value !== "string") {
|
|
448
|
+
return createErr(
|
|
449
|
+
ErrorCode.E_INVALID_ARG,
|
|
450
|
+
`${key} must be a string when provided.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const trimmedValue = value.trim();
|
|
455
|
+
return trimmedValue.length === 0 ? createOk(undefined) : createOk(trimmedValue);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function resolveOptionalNumberArg(args: ToolCallArgs, key: string): ToolResponse<number | undefined> {
|
|
459
|
+
const value = args[key];
|
|
460
|
+
if (value === undefined) {
|
|
461
|
+
return createOk(undefined);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
465
|
+
return createErr(
|
|
466
|
+
ErrorCode.E_INVALID_ARG,
|
|
467
|
+
`${key} must be a finite number when provided.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return createOk(value);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function resolveOptionalIntegerArg(
|
|
475
|
+
args: ToolCallArgs,
|
|
476
|
+
key: string,
|
|
477
|
+
minimum: number
|
|
478
|
+
): ToolResponse<number | undefined> {
|
|
479
|
+
const resolved = resolveOptionalNumberArg(args, key);
|
|
480
|
+
if (!resolved.ok || resolved.data === undefined) {
|
|
481
|
+
return resolved;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!Number.isInteger(resolved.data) || resolved.data < minimum) {
|
|
485
|
+
return createErr(
|
|
486
|
+
ErrorCode.E_INVALID_ARG,
|
|
487
|
+
`${key} must be an integer greater than or equal to ${minimum}.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return resolved;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function resolveDriverActionData(
|
|
495
|
+
actionResult: unknown,
|
|
496
|
+
actionType: string
|
|
497
|
+
): ToolResponse<Record<string, unknown>> {
|
|
498
|
+
if (!isObjectRecord(actionResult)) {
|
|
499
|
+
return createErr(ErrorCode.E_INTERNAL, `Invalid action result payload for ${actionType}.`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (actionResult.targetKnown === false) {
|
|
503
|
+
return createErr(
|
|
504
|
+
ErrorCode.E_NOT_FOUND,
|
|
505
|
+
typeof actionResult.error === "string"
|
|
506
|
+
? actionResult.error
|
|
507
|
+
: `Unknown target for action ${actionType}.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (actionResult.ok !== true) {
|
|
512
|
+
return createErr(
|
|
513
|
+
ErrorCode.E_INTERNAL,
|
|
514
|
+
typeof actionResult.error === "string"
|
|
515
|
+
? actionResult.error
|
|
516
|
+
: `Driver action failed: ${actionType}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (actionResult.executed !== true) {
|
|
521
|
+
return createErr(
|
|
522
|
+
ErrorCode.E_DRIVER_UNAVAILABLE,
|
|
523
|
+
`Driver does not support action: ${actionType}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const data = actionResult.data;
|
|
528
|
+
if (data === undefined) {
|
|
529
|
+
return createOk({});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!isObjectRecord(data)) {
|
|
533
|
+
return createErr(ErrorCode.E_INTERNAL, `Invalid action data payload for ${actionType}.`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return createOk(data);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
type NetworkRequestSummary = {
|
|
540
|
+
requestId: string;
|
|
541
|
+
url: string;
|
|
542
|
+
method?: string;
|
|
543
|
+
status?: number;
|
|
544
|
+
resourceType?: string;
|
|
545
|
+
timestamp?: string;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
function readNetworkRequestSummaries(snapshot: unknown): NetworkRequestSummary[] {
|
|
549
|
+
if (!isObjectRecord(snapshot)) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const rawSummaries = snapshot.requestSummaries;
|
|
554
|
+
if (!Array.isArray(rawSummaries)) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const summaries: NetworkRequestSummary[] = [];
|
|
559
|
+
for (const rawSummary of rawSummaries) {
|
|
560
|
+
if (!isObjectRecord(rawSummary)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const requestId = rawSummary.requestId;
|
|
565
|
+
const url = rawSummary.url;
|
|
566
|
+
if (typeof requestId !== "string" || typeof url !== "string") {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const method =
|
|
571
|
+
typeof rawSummary.method === "string" && rawSummary.method.trim().length > 0
|
|
572
|
+
? rawSummary.method
|
|
573
|
+
: undefined;
|
|
574
|
+
const status =
|
|
575
|
+
typeof rawSummary.status === "number" && Number.isFinite(rawSummary.status)
|
|
576
|
+
? rawSummary.status
|
|
577
|
+
: undefined;
|
|
578
|
+
const resourceType =
|
|
579
|
+
typeof rawSummary.resourceType === "string" && rawSummary.resourceType.trim().length > 0
|
|
580
|
+
? rawSummary.resourceType
|
|
581
|
+
: undefined;
|
|
582
|
+
const timestamp =
|
|
583
|
+
typeof rawSummary.timestamp === "string" && rawSummary.timestamp.trim().length > 0
|
|
584
|
+
? rawSummary.timestamp
|
|
585
|
+
: undefined;
|
|
586
|
+
|
|
587
|
+
summaries.push({
|
|
588
|
+
requestId,
|
|
589
|
+
url,
|
|
590
|
+
...(method !== undefined ? { method } : {}),
|
|
591
|
+
...(status !== undefined ? { status } : {}),
|
|
592
|
+
...(resourceType !== undefined ? { resourceType } : {}),
|
|
593
|
+
...(timestamp !== undefined ? { timestamp } : {})
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return summaries;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function delay(ms: number): Promise<void> {
|
|
601
|
+
return new Promise((resolve) => {
|
|
602
|
+
setTimeout(resolve, ms);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function resolveRequiredScope(toolName: string): CapabilityScope {
|
|
607
|
+
return TOOL_SCOPE_BY_NAME[toolName] ?? "read";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function resolveProvidedAuthToken(args: ToolCallArgs): string | undefined {
|
|
611
|
+
const rawToken = args.authToken;
|
|
612
|
+
if (typeof rawToken !== "string") {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const trimmedToken = rawToken.trim();
|
|
617
|
+
return trimmedToken.length === 0 ? undefined : trimmedToken;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function authorizeToolCall(
|
|
621
|
+
args: ToolCallArgs,
|
|
622
|
+
toolName: string,
|
|
623
|
+
configuredToken: string | undefined,
|
|
624
|
+
allowedScopes: ReadonlySet<CapabilityScope>
|
|
625
|
+
): ToolResponse<null> {
|
|
626
|
+
if (configuredToken !== undefined) {
|
|
627
|
+
const providedToken = resolveProvidedAuthToken(args);
|
|
628
|
+
if (providedToken === undefined || providedToken !== configuredToken) {
|
|
629
|
+
return createErr(ErrorCode.E_PERMISSION, "Invalid auth token.");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const requiredScope = resolveRequiredScope(toolName);
|
|
634
|
+
if (!allowedScopes.has(requiredScope)) {
|
|
635
|
+
return createErr(
|
|
636
|
+
ErrorCode.E_PERMISSION,
|
|
637
|
+
`Tool scope is not allowed: ${toolName} requires ${requiredScope}.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return createOk(null);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resolveWindowsPathWithinRoot(
|
|
645
|
+
inputPath: string,
|
|
646
|
+
rootPath: string,
|
|
647
|
+
context: "upload" | "download"
|
|
648
|
+
): ToolResponse<string> {
|
|
649
|
+
const resolvedRoot = windowsPath.resolve(rootPath);
|
|
650
|
+
const resolvedPath = windowsPath.isAbsolute(inputPath)
|
|
651
|
+
? windowsPath.resolve(inputPath)
|
|
652
|
+
: windowsPath.resolve(resolvedRoot, inputPath);
|
|
653
|
+
const relativePath = windowsPath.relative(
|
|
654
|
+
resolvedRoot.toLowerCase(),
|
|
655
|
+
resolvedPath.toLowerCase()
|
|
656
|
+
);
|
|
657
|
+
const isInRoot =
|
|
658
|
+
relativePath === "" ||
|
|
659
|
+
(!relativePath.startsWith("..") && !windowsPath.isAbsolute(relativePath));
|
|
660
|
+
if (!isInRoot) {
|
|
661
|
+
return createErr(
|
|
662
|
+
ErrorCode.E_PERMISSION,
|
|
663
|
+
`${context} path is outside configured allowlist root: ${inputPath}`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return createOk(resolvedPath);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function applyUploadRoot(files: string[], uploadRoot: string | undefined): ToolResponse<string[]> {
|
|
671
|
+
if (uploadRoot === undefined) {
|
|
672
|
+
return createErr(
|
|
673
|
+
ErrorCode.E_PERMISSION,
|
|
674
|
+
"Upload root is not configured. Set BROWSERD_UPLOAD_ROOT to enable browser.upload.arm."
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const rootedFiles: string[] = [];
|
|
679
|
+
for (const file of files) {
|
|
680
|
+
const rootedPath = resolveWindowsPathWithinRoot(file, uploadRoot, "upload");
|
|
681
|
+
if (!rootedPath.ok) {
|
|
682
|
+
return rootedPath;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
rootedFiles.push(rootedPath.data);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return createOk(rootedFiles);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function normalizeDownloadPayload(
|
|
692
|
+
rawDownload: unknown,
|
|
693
|
+
requestedPath: string | undefined,
|
|
694
|
+
downloadRoot: string | undefined
|
|
695
|
+
): ToolResponse<Record<string, unknown>> {
|
|
696
|
+
if (downloadRoot === undefined) {
|
|
697
|
+
return createErr(
|
|
698
|
+
ErrorCode.E_PERMISSION,
|
|
699
|
+
"Download root is not configured. Set BROWSERD_DOWNLOAD_ROOT to enable browser.download.wait."
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!isObjectRecord(rawDownload)) {
|
|
704
|
+
return createErr(ErrorCode.E_INTERNAL, "Invalid driver download payload.");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const download = { ...rawDownload };
|
|
708
|
+
const downloadPath =
|
|
709
|
+
requestedPath ??
|
|
710
|
+
(typeof download.path === "string" && download.path.trim().length > 0
|
|
711
|
+
? download.path.trim()
|
|
712
|
+
: undefined);
|
|
713
|
+
if (downloadPath === undefined) {
|
|
714
|
+
return createErr(
|
|
715
|
+
ErrorCode.E_INTERNAL,
|
|
716
|
+
"Driver download payload is missing path required for allowlist enforcement."
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const rootedPath = resolveWindowsPathWithinRoot(downloadPath, downloadRoot, "download");
|
|
721
|
+
if (!rootedPath.ok) {
|
|
722
|
+
return rootedPath;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return createOk({
|
|
726
|
+
...download,
|
|
727
|
+
path: rootedPath.data
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function createBrowserdToolMap(
|
|
732
|
+
drivers: Map<string, BrowserDriver>,
|
|
733
|
+
sessions: SessionStore,
|
|
734
|
+
defaultDriverKey: string,
|
|
735
|
+
config: BrowserdConfig
|
|
736
|
+
): ToolMap {
|
|
737
|
+
const toolMap = buildToolMap();
|
|
738
|
+
const allowedScopes = new Set<CapabilityScope>(config.authScopes);
|
|
739
|
+
const runWithDefaultDriver = <TData>(
|
|
740
|
+
args: ToolCallArgs,
|
|
741
|
+
operation: (
|
|
742
|
+
driver: BrowserDriver,
|
|
743
|
+
driverKey: string
|
|
744
|
+
) => Promise<ToolResponse<TData>> | ToolResponse<TData>
|
|
745
|
+
): Promise<ToolResponse<TData>> =>
|
|
746
|
+
runWithDriver(args, drivers, sessions, defaultDriverKey, operation);
|
|
747
|
+
|
|
748
|
+
const runStructuredAction = async (
|
|
749
|
+
args: ToolCallArgs,
|
|
750
|
+
actionType: string,
|
|
751
|
+
payload: Record<string, unknown>
|
|
752
|
+
): Promise<
|
|
753
|
+
ToolResponse<{
|
|
754
|
+
driver: string;
|
|
755
|
+
targetId: string;
|
|
756
|
+
data: Record<string, unknown>;
|
|
757
|
+
}>
|
|
758
|
+
> =>
|
|
759
|
+
await runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
760
|
+
const targetId = resolveTargetId(args, sessions);
|
|
761
|
+
if (!targetId.ok) {
|
|
762
|
+
return targetId;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const actionResult = await driver.act(
|
|
766
|
+
{
|
|
767
|
+
type: actionType,
|
|
768
|
+
payload
|
|
769
|
+
},
|
|
770
|
+
targetId.data
|
|
771
|
+
);
|
|
772
|
+
const resolvedData = resolveDriverActionData(actionResult, actionType);
|
|
773
|
+
if (!resolvedData.ok) {
|
|
774
|
+
return resolvedData;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return createOk({
|
|
778
|
+
driver: driverKey,
|
|
779
|
+
targetId: targetId.data,
|
|
780
|
+
data: resolvedData.data
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
toolMap.set("browser.status", async (args) => {
|
|
785
|
+
const resolvedDriver = resolveDriver(args, drivers, sessions, defaultDriverKey);
|
|
786
|
+
if (!resolvedDriver.ok) {
|
|
787
|
+
return resolvedDriver;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const status = await resolvedDriver.data.driver.status();
|
|
792
|
+
return createOk({
|
|
793
|
+
kind: "browserd",
|
|
794
|
+
ready: true,
|
|
795
|
+
driver: resolvedDriver.data.driverKey,
|
|
796
|
+
drivers: [...drivers.keys()].sort(),
|
|
797
|
+
status
|
|
798
|
+
});
|
|
799
|
+
} catch (error) {
|
|
800
|
+
return mapDriverError(error);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
toolMap.set("browser.profile.use", async (args) => {
|
|
805
|
+
const requestedProfile = requireStringArg(args, "profile");
|
|
806
|
+
if (!requestedProfile.ok) {
|
|
807
|
+
return requestedProfile;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!drivers.has(requestedProfile.data)) {
|
|
811
|
+
return createErr(ErrorCode.E_NOT_FOUND, `Unknown driver profile: ${requestedProfile.data}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
sessions.useProfile(args.sessionId, requestedProfile.data);
|
|
815
|
+
return createOk({
|
|
816
|
+
profile: requestedProfile.data
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
toolMap.set("browser.profile.list", async (args) =>
|
|
821
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
822
|
+
const profiles = await driver.listProfiles();
|
|
823
|
+
return createOk({
|
|
824
|
+
driver: driverKey,
|
|
825
|
+
profiles
|
|
826
|
+
});
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
toolMap.set("browser.tab.list", async (args) =>
|
|
831
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
832
|
+
const tabs = await driver.listTabs();
|
|
833
|
+
return createOk({
|
|
834
|
+
driver: driverKey,
|
|
835
|
+
tabs
|
|
836
|
+
});
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
toolMap.set("browser.tab.open", async (args) =>
|
|
841
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
842
|
+
const url = requireStringArg(args, "url");
|
|
843
|
+
if (!url.ok) {
|
|
844
|
+
return url;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const targetId = await driver.openTab(url.data);
|
|
848
|
+
sessions.useTarget(args.sessionId, targetId);
|
|
849
|
+
return createOk({
|
|
850
|
+
driver: driverKey,
|
|
851
|
+
targetId
|
|
852
|
+
});
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
toolMap.set("browser.tab.focus", async (args) =>
|
|
857
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
858
|
+
const targetId = resolveTargetId(args, sessions);
|
|
859
|
+
if (!targetId.ok) {
|
|
860
|
+
return targetId;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
await driver.focusTab(targetId.data);
|
|
864
|
+
sessions.useTarget(args.sessionId, targetId.data);
|
|
865
|
+
return createOk({
|
|
866
|
+
driver: driverKey,
|
|
867
|
+
targetId: targetId.data,
|
|
868
|
+
focused: true
|
|
869
|
+
});
|
|
870
|
+
})
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
toolMap.set("browser.tab.close", async (args) =>
|
|
874
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
875
|
+
const targetId = resolveTargetId(args, sessions);
|
|
876
|
+
if (!targetId.ok) {
|
|
877
|
+
return targetId;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
await driver.closeTab(targetId.data);
|
|
881
|
+
return createOk({
|
|
882
|
+
driver: driverKey,
|
|
883
|
+
targetId: targetId.data,
|
|
884
|
+
closed: true
|
|
885
|
+
});
|
|
886
|
+
})
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
toolMap.set("browser.snapshot", async (args) =>
|
|
890
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
891
|
+
const targetId = resolveTargetId(args, sessions);
|
|
892
|
+
if (!targetId.ok) {
|
|
893
|
+
return targetId;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const snapshot = await driver.snapshot(targetId.data);
|
|
897
|
+
return createOk({
|
|
898
|
+
driver: driverKey,
|
|
899
|
+
targetId: targetId.data,
|
|
900
|
+
snapshot
|
|
901
|
+
});
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
toolMap.set("browser.screenshot", async (args) =>
|
|
906
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
907
|
+
const targetId = resolveTargetId(args, sessions);
|
|
908
|
+
if (!targetId.ok) {
|
|
909
|
+
return targetId;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (!hasScreenshotGetter(driver)) {
|
|
913
|
+
return createErr(
|
|
914
|
+
ErrorCode.E_DRIVER_UNAVAILABLE,
|
|
915
|
+
`Driver does not support screenshots: ${driverKey}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const screenshot = await driver.screenshot(targetId.data);
|
|
920
|
+
return createOk({
|
|
921
|
+
driver: driverKey,
|
|
922
|
+
targetId: targetId.data,
|
|
923
|
+
screenshot
|
|
924
|
+
});
|
|
925
|
+
})
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
toolMap.set("browser.dom.query", async (args) => {
|
|
929
|
+
const selector = requireStringArg(args, "selector");
|
|
930
|
+
if (!selector.ok) {
|
|
931
|
+
return selector;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const result = await runStructuredAction(args, "domQuery", {
|
|
935
|
+
selector: selector.data
|
|
936
|
+
});
|
|
937
|
+
if (!result.ok) {
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return createOk({
|
|
942
|
+
driver: result.data.driver,
|
|
943
|
+
targetId: result.data.targetId,
|
|
944
|
+
query: result.data.data
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
toolMap.set("browser.dom.queryAll", async (args) => {
|
|
949
|
+
const selector = requireStringArg(args, "selector");
|
|
950
|
+
if (!selector.ok) {
|
|
951
|
+
return selector;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const result = await runStructuredAction(args, "domQueryAll", {
|
|
955
|
+
selector: selector.data
|
|
956
|
+
});
|
|
957
|
+
if (!result.ok) {
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return createOk({
|
|
962
|
+
driver: result.data.driver,
|
|
963
|
+
targetId: result.data.targetId,
|
|
964
|
+
query: result.data.data
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
toolMap.set("browser.element.screenshot", async (args) => {
|
|
969
|
+
const selector = requireStringArg(args, "selector");
|
|
970
|
+
if (!selector.ok) {
|
|
971
|
+
return selector;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const result = await runStructuredAction(args, "elementScreenshot", {
|
|
975
|
+
selector: selector.data
|
|
976
|
+
});
|
|
977
|
+
if (!result.ok) {
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return createOk({
|
|
982
|
+
driver: result.data.driver,
|
|
983
|
+
targetId: result.data.targetId,
|
|
984
|
+
screenshot: result.data.data
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
toolMap.set("browser.a11y.snapshot", async (args) => {
|
|
989
|
+
const selector = resolveOptionalStringArg(args, "selector");
|
|
990
|
+
if (!selector.ok) {
|
|
991
|
+
return selector;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const result = await runStructuredAction(args, "a11ySnapshot", {
|
|
995
|
+
...(selector.data !== undefined ? { selector: selector.data } : {})
|
|
996
|
+
});
|
|
997
|
+
if (!result.ok) {
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return createOk({
|
|
1002
|
+
driver: result.data.driver,
|
|
1003
|
+
targetId: result.data.targetId,
|
|
1004
|
+
snapshot: result.data.data
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
toolMap.set("browser.cookie.get", async (args) => {
|
|
1009
|
+
const name = resolveOptionalStringArg(args, "name");
|
|
1010
|
+
if (!name.ok) {
|
|
1011
|
+
return name;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const result = await runStructuredAction(args, "cookieGet", {
|
|
1015
|
+
...(name.data !== undefined ? { name: name.data } : {})
|
|
1016
|
+
});
|
|
1017
|
+
if (!result.ok) {
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return createOk({
|
|
1022
|
+
driver: result.data.driver,
|
|
1023
|
+
targetId: result.data.targetId,
|
|
1024
|
+
...result.data.data
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
toolMap.set("browser.cookie.set", async (args) => {
|
|
1029
|
+
const name = requireStringArg(args, "name");
|
|
1030
|
+
if (!name.ok) {
|
|
1031
|
+
return name;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const value = requireStringArg(args, "value");
|
|
1035
|
+
if (!value.ok) {
|
|
1036
|
+
return value;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const url = resolveOptionalStringArg(args, "url");
|
|
1040
|
+
if (!url.ok) {
|
|
1041
|
+
return url;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const result = await runStructuredAction(args, "cookieSet", {
|
|
1045
|
+
name: name.data,
|
|
1046
|
+
value: value.data,
|
|
1047
|
+
...(url.data !== undefined ? { url: url.data } : {})
|
|
1048
|
+
});
|
|
1049
|
+
if (!result.ok) {
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return createOk({
|
|
1054
|
+
driver: result.data.driver,
|
|
1055
|
+
targetId: result.data.targetId,
|
|
1056
|
+
...result.data.data
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
toolMap.set("browser.cookie.clear", async (args) => {
|
|
1061
|
+
const name = resolveOptionalStringArg(args, "name");
|
|
1062
|
+
if (!name.ok) {
|
|
1063
|
+
return name;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const result = await runStructuredAction(args, "cookieClear", {
|
|
1067
|
+
...(name.data !== undefined ? { name: name.data } : {})
|
|
1068
|
+
});
|
|
1069
|
+
if (!result.ok) {
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return createOk({
|
|
1074
|
+
driver: result.data.driver,
|
|
1075
|
+
targetId: result.data.targetId,
|
|
1076
|
+
...result.data.data
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
toolMap.set("browser.storage.get", async (args) => {
|
|
1081
|
+
const scope = requireStringArg(args, "scope");
|
|
1082
|
+
if (!scope.ok) {
|
|
1083
|
+
return scope;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (scope.data !== "local" && scope.data !== "session") {
|
|
1087
|
+
return createErr(ErrorCode.E_INVALID_ARG, "scope must be local or session.");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const key = requireStringArg(args, "key");
|
|
1091
|
+
if (!key.ok) {
|
|
1092
|
+
return key;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const result = await runStructuredAction(args, "storageGet", {
|
|
1096
|
+
scope: scope.data,
|
|
1097
|
+
key: key.data
|
|
1098
|
+
});
|
|
1099
|
+
if (!result.ok) {
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return createOk({
|
|
1104
|
+
driver: result.data.driver,
|
|
1105
|
+
targetId: result.data.targetId,
|
|
1106
|
+
...result.data.data
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
toolMap.set("browser.storage.set", async (args) => {
|
|
1111
|
+
const scope = requireStringArg(args, "scope");
|
|
1112
|
+
if (!scope.ok) {
|
|
1113
|
+
return scope;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (scope.data !== "local" && scope.data !== "session") {
|
|
1117
|
+
return createErr(ErrorCode.E_INVALID_ARG, "scope must be local or session.");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const key = requireStringArg(args, "key");
|
|
1121
|
+
if (!key.ok) {
|
|
1122
|
+
return key;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const value = requireStringArg(args, "value");
|
|
1126
|
+
if (!value.ok) {
|
|
1127
|
+
return value;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const result = await runStructuredAction(args, "storageSet", {
|
|
1131
|
+
scope: scope.data,
|
|
1132
|
+
key: key.data,
|
|
1133
|
+
value: value.data
|
|
1134
|
+
});
|
|
1135
|
+
if (!result.ok) {
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return createOk({
|
|
1140
|
+
driver: result.data.driver,
|
|
1141
|
+
targetId: result.data.targetId,
|
|
1142
|
+
...result.data.data
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
toolMap.set("browser.frame.list", async (args) => {
|
|
1147
|
+
const result = await runStructuredAction(args, "frameList", {});
|
|
1148
|
+
if (!result.ok) {
|
|
1149
|
+
return result;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return createOk({
|
|
1153
|
+
driver: result.data.driver,
|
|
1154
|
+
targetId: result.data.targetId,
|
|
1155
|
+
...result.data.data
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
toolMap.set("browser.frame.snapshot", async (args) => {
|
|
1160
|
+
const frameId = requireStringArg(args, "frameId");
|
|
1161
|
+
if (!frameId.ok) {
|
|
1162
|
+
return frameId;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const result = await runStructuredAction(args, "frameSnapshot", {
|
|
1166
|
+
frameId: frameId.data
|
|
1167
|
+
});
|
|
1168
|
+
if (!result.ok) {
|
|
1169
|
+
return result;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return createOk({
|
|
1173
|
+
driver: result.data.driver,
|
|
1174
|
+
targetId: result.data.targetId,
|
|
1175
|
+
...result.data.data
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
toolMap.set("browser.act", async (args) =>
|
|
1180
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1181
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1182
|
+
if (!targetId.ok) {
|
|
1183
|
+
return targetId;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const action = resolveAction(args);
|
|
1187
|
+
if (!action.ok) {
|
|
1188
|
+
return action;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const result = await driver.act(action.data, targetId.data);
|
|
1192
|
+
return createOk({
|
|
1193
|
+
driver: driverKey,
|
|
1194
|
+
targetId: targetId.data,
|
|
1195
|
+
result
|
|
1196
|
+
});
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
toolMap.set("browser.upload.arm", async (args) =>
|
|
1201
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1202
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1203
|
+
if (!targetId.ok) {
|
|
1204
|
+
return targetId;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const files = resolveFiles(args);
|
|
1208
|
+
if (!files.ok) {
|
|
1209
|
+
return files;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const rootedFiles = applyUploadRoot(files.data, config.uploadRoot);
|
|
1213
|
+
if (!rootedFiles.ok) {
|
|
1214
|
+
return rootedFiles;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
await driver.armUpload(targetId.data, rootedFiles.data);
|
|
1218
|
+
return createOk({
|
|
1219
|
+
driver: driverKey,
|
|
1220
|
+
targetId: targetId.data,
|
|
1221
|
+
armed: true,
|
|
1222
|
+
files: rootedFiles.data
|
|
1223
|
+
});
|
|
1224
|
+
})
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
toolMap.set("browser.dialog.arm", async (args) =>
|
|
1228
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1229
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1230
|
+
if (!targetId.ok) {
|
|
1231
|
+
return targetId;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
await driver.armDialog(targetId.data);
|
|
1235
|
+
return createOk({
|
|
1236
|
+
driver: driverKey,
|
|
1237
|
+
targetId: targetId.data,
|
|
1238
|
+
armed: true
|
|
1239
|
+
});
|
|
1240
|
+
})
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
toolMap.set("browser.download.trigger", async (args) =>
|
|
1244
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1245
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1246
|
+
if (!targetId.ok) {
|
|
1247
|
+
return targetId;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
await driver.triggerDownload(targetId.data);
|
|
1251
|
+
return createOk({
|
|
1252
|
+
driver: driverKey,
|
|
1253
|
+
targetId: targetId.data,
|
|
1254
|
+
triggered: true
|
|
1255
|
+
});
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
toolMap.set("browser.download.wait", async (args) =>
|
|
1260
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1261
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1262
|
+
if (!targetId.ok) {
|
|
1263
|
+
return targetId;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const requestedPath = resolveOptionalPathArg(args, "path");
|
|
1267
|
+
if (!requestedPath.ok) {
|
|
1268
|
+
return requestedPath;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const download = await driver.waitDownload(targetId.data);
|
|
1272
|
+
const normalizedDownload = normalizeDownloadPayload(
|
|
1273
|
+
download,
|
|
1274
|
+
requestedPath.data,
|
|
1275
|
+
config.downloadRoot
|
|
1276
|
+
);
|
|
1277
|
+
if (!normalizedDownload.ok) {
|
|
1278
|
+
return normalizedDownload;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return createOk({
|
|
1282
|
+
driver: driverKey,
|
|
1283
|
+
targetId: targetId.data,
|
|
1284
|
+
download: normalizedDownload.data
|
|
1285
|
+
});
|
|
1286
|
+
})
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
toolMap.set("browser.console.list", async (args) =>
|
|
1290
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1291
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1292
|
+
if (!targetId.ok) {
|
|
1293
|
+
return targetId;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const snapshot = await driver.snapshot(targetId.data);
|
|
1297
|
+
if (!isTargetKnownSnapshot(snapshot)) {
|
|
1298
|
+
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (hasConsoleEntriesGetter(driver)) {
|
|
1302
|
+
return createOk({
|
|
1303
|
+
driver: driverKey,
|
|
1304
|
+
targetId: targetId.data,
|
|
1305
|
+
entries: driver.getConsoleEntries(targetId.data)
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return createOk({
|
|
1310
|
+
driver: driverKey,
|
|
1311
|
+
targetId: targetId.data,
|
|
1312
|
+
entries: []
|
|
1313
|
+
});
|
|
1314
|
+
})
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
toolMap.set("browser.network.waitFor", async (args) =>
|
|
1318
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1319
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1320
|
+
if (!targetId.ok) {
|
|
1321
|
+
return targetId;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const urlPattern = requireStringArg(args, "urlPattern");
|
|
1325
|
+
if (!urlPattern.ok) {
|
|
1326
|
+
return urlPattern;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const method = resolveOptionalStringArg(args, "method");
|
|
1330
|
+
if (!method.ok) {
|
|
1331
|
+
return method;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const status = resolveOptionalIntegerArg(args, "status", 100);
|
|
1335
|
+
if (!status.ok) {
|
|
1336
|
+
return status;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const timeoutMs = resolveOptionalIntegerArg(args, "timeoutMs", 1);
|
|
1340
|
+
if (!timeoutMs.ok) {
|
|
1341
|
+
return timeoutMs;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const pollMs = resolveOptionalIntegerArg(args, "pollMs", 1);
|
|
1345
|
+
if (!pollMs.ok) {
|
|
1346
|
+
return pollMs;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const methodPattern = method.data?.toUpperCase();
|
|
1350
|
+
const expectedStatus = status.data;
|
|
1351
|
+
const maxWaitMs = timeoutMs.data ?? DEFAULT_NETWORK_WAIT_TIMEOUT_MS;
|
|
1352
|
+
const intervalMs = pollMs.data ?? DEFAULT_NETWORK_WAIT_POLL_MS;
|
|
1353
|
+
const startTime = Date.now();
|
|
1354
|
+
|
|
1355
|
+
while (Date.now() - startTime <= maxWaitMs) {
|
|
1356
|
+
const snapshot = await driver.snapshot(targetId.data);
|
|
1357
|
+
if (!isTargetKnownSnapshot(snapshot)) {
|
|
1358
|
+
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const summaries = readNetworkRequestSummaries(snapshot);
|
|
1362
|
+
const matchedSummary = summaries
|
|
1363
|
+
.slice()
|
|
1364
|
+
.reverse()
|
|
1365
|
+
.find((summary) => {
|
|
1366
|
+
if (!summary.url.includes(urlPattern.data)) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (
|
|
1371
|
+
methodPattern !== undefined &&
|
|
1372
|
+
(summary.method === undefined || summary.method.toUpperCase() !== methodPattern)
|
|
1373
|
+
) {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (expectedStatus !== undefined && summary.status !== expectedStatus) {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return true;
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
if (matchedSummary !== undefined) {
|
|
1385
|
+
return createOk({
|
|
1386
|
+
driver: driverKey,
|
|
1387
|
+
targetId: targetId.data,
|
|
1388
|
+
request: matchedSummary,
|
|
1389
|
+
elapsedMs: Date.now() - startTime
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
await delay(intervalMs);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return createErr(
|
|
1397
|
+
ErrorCode.E_TIMEOUT,
|
|
1398
|
+
`Timed out waiting for network response: ${urlPattern.data}`
|
|
1399
|
+
);
|
|
1400
|
+
})
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
toolMap.set("browser.network.responseBody", async (args) =>
|
|
1404
|
+
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1405
|
+
const targetId = resolveTargetId(args, sessions);
|
|
1406
|
+
if (!targetId.ok) {
|
|
1407
|
+
return targetId;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const requestId = resolveRequestId(args);
|
|
1411
|
+
if (!requestId.ok) {
|
|
1412
|
+
return requestId;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const snapshot = await driver.snapshot(targetId.data);
|
|
1416
|
+
if (!isTargetKnownSnapshot(snapshot)) {
|
|
1417
|
+
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (hasNetworkResponseBodyGetter(driver)) {
|
|
1421
|
+
const responseBody = driver.getNetworkResponseBody(requestId.data, targetId.data);
|
|
1422
|
+
if (responseBody === undefined) {
|
|
1423
|
+
return createErr(
|
|
1424
|
+
ErrorCode.E_NOT_FOUND,
|
|
1425
|
+
`Unknown requestId: ${requestId.data} (target: ${targetId.data})`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return createOk({
|
|
1430
|
+
driver: driverKey,
|
|
1431
|
+
targetId: targetId.data,
|
|
1432
|
+
requestId: requestId.data,
|
|
1433
|
+
body: responseBody.body,
|
|
1434
|
+
encoding: responseBody.encoding
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return createOk({
|
|
1439
|
+
driver: driverKey,
|
|
1440
|
+
targetId: targetId.data,
|
|
1441
|
+
requestId: requestId.data,
|
|
1442
|
+
body: "",
|
|
1443
|
+
encoding: "utf8"
|
|
1444
|
+
});
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
|
|
1448
|
+
for (const [toolName, handler] of [...toolMap.entries()]) {
|
|
1449
|
+
toolMap.set(toolName, async (args) => {
|
|
1450
|
+
const authorization = authorizeToolCall(args, toolName, config.authToken, allowedScopes);
|
|
1451
|
+
if (!authorization.ok) {
|
|
1452
|
+
return authorization;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return await handler(args);
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return toolMap;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
export function createContainer(config: BrowserdConfig = loadBrowserdConfig()): BrowserdContainer {
|
|
1463
|
+
const sessions = new SessionStore();
|
|
1464
|
+
const driverRegistry = new DriverRegistry();
|
|
1465
|
+
const drivers = new Map<string, BrowserDriver>();
|
|
1466
|
+
const cleanupHandlers: Array<() => void> = [];
|
|
1467
|
+
|
|
1468
|
+
function registerDriver(driverKey: string, driver: BrowserDriver): void {
|
|
1469
|
+
driverRegistry.register(driverKey, driver);
|
|
1470
|
+
drivers.set(driverKey, driver);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
registerDriver(DEFAULT_DRIVER_KEY, createManagedDriver());
|
|
1474
|
+
if (config.managedLocalEnabled) {
|
|
1475
|
+
registerDriver(
|
|
1476
|
+
MANAGED_LOCAL_DRIVER_KEY,
|
|
1477
|
+
createManagedLocalDriver({
|
|
1478
|
+
browserName: config.managedLocalLaunch.browserName,
|
|
1479
|
+
headless: config.managedLocalLaunch.headless,
|
|
1480
|
+
channel: config.managedLocalLaunch.channel,
|
|
1481
|
+
executablePath: config.managedLocalLaunch.executablePath,
|
|
1482
|
+
launchTimeoutMs: config.managedLocalLaunch.launchTimeoutMs,
|
|
1483
|
+
args: [...config.managedLocalLaunch.args]
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const chromeRelayDriver =
|
|
1488
|
+
config.chromeRelayMode === "extension"
|
|
1489
|
+
? (() => {
|
|
1490
|
+
const extensionBridge = createChromeRelayExtensionBridge({
|
|
1491
|
+
relayUrl: config.chromeRelayUrl,
|
|
1492
|
+
token: config.chromeRelayExtensionToken,
|
|
1493
|
+
requestTimeoutMs: config.chromeRelayExtensionRequestTimeoutMs
|
|
1494
|
+
});
|
|
1495
|
+
cleanupHandlers.push(() => {
|
|
1496
|
+
void extensionBridge.close();
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
return createChromeRelayDriver({
|
|
1500
|
+
relayUrl: config.chromeRelayUrl,
|
|
1501
|
+
runtime: createChromeRelayExtensionRuntime({
|
|
1502
|
+
transport: {
|
|
1503
|
+
invoke: async (method, params) => await extensionBridge.invoke(method, params),
|
|
1504
|
+
isConnected: () => extensionBridge.isConnected(),
|
|
1505
|
+
onEvent: (listener) => extensionBridge.onEvent(listener)
|
|
1506
|
+
}
|
|
1507
|
+
})
|
|
1508
|
+
});
|
|
1509
|
+
})()
|
|
1510
|
+
: createChromeRelayDriver({ relayUrl: config.chromeRelayUrl });
|
|
1511
|
+
registerDriver("chrome-relay", chromeRelayDriver);
|
|
1512
|
+
registerDriver("remote-cdp", createRemoteCdpDriver({ cdpUrl: config.remoteCdpUrl }));
|
|
1513
|
+
const defaultDriverKey = drivers.has(config.defaultDriver)
|
|
1514
|
+
? config.defaultDriver
|
|
1515
|
+
: DEFAULT_DRIVER_KEY;
|
|
1516
|
+
|
|
1517
|
+
return {
|
|
1518
|
+
config,
|
|
1519
|
+
drivers,
|
|
1520
|
+
driverRegistry,
|
|
1521
|
+
sessions,
|
|
1522
|
+
mcpServer: createMcpStdioServer(
|
|
1523
|
+
createBrowserdToolMap(drivers, sessions, defaultDriverKey, config)
|
|
1524
|
+
),
|
|
1525
|
+
close() {
|
|
1526
|
+
for (const cleanupHandler of [...cleanupHandlers].reverse()) {
|
|
1527
|
+
cleanupHandler();
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|