@flrande/browserctl 0.5.0-dev.22.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +34 -0
- package/dist/client.js +138 -0
- package/dist/commandRegistry.d.ts +16 -0
- package/dist/commandRegistry.js +21 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/runCli.d.ts +5 -0
- package/dist/runCli.js +170 -0
- package/package.json +32 -59
- package/INSTALL-CN.md +0 -92
- package/INSTALL.md +0 -92
- package/LICENSE +0 -21
- package/README-CN.md +0 -69
- package/README.md +0 -69
- package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
- package/apps/browserctl/src/commands/act.test.ts +0 -71
- package/apps/browserctl/src/commands/act.ts +0 -64
- package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
- package/apps/browserctl/src/commands/common.test.ts +0 -87
- package/apps/browserctl/src/commands/common.ts +0 -191
- package/apps/browserctl/src/commands/console-list.test.ts +0 -102
- package/apps/browserctl/src/commands/console-list.ts +0 -108
- package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
- package/apps/browserctl/src/commands/cookie-get.ts +0 -18
- package/apps/browserctl/src/commands/cookie-set.ts +0 -22
- package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
- package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
- package/apps/browserctl/src/commands/dom-query.ts +0 -18
- package/apps/browserctl/src/commands/download-trigger.ts +0 -22
- package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
- package/apps/browserctl/src/commands/download-wait.ts +0 -27
- package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
- package/apps/browserctl/src/commands/frame-list.ts +0 -16
- package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
- package/apps/browserctl/src/commands/har-export.test.ts +0 -112
- package/apps/browserctl/src/commands/har-export.ts +0 -120
- package/apps/browserctl/src/commands/memory-delete.ts +0 -20
- package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
- package/apps/browserctl/src/commands/memory-list.ts +0 -90
- package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
- package/apps/browserctl/src/commands/memory-purge.ts +0 -16
- package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
- package/apps/browserctl/src/commands/memory-status.ts +0 -16
- package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
- package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
- package/apps/browserctl/src/commands/network-list.test.ts +0 -110
- package/apps/browserctl/src/commands/network-list.ts +0 -112
- package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
- package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
- package/apps/browserctl/src/commands/profile-list.ts +0 -16
- package/apps/browserctl/src/commands/profile-use.ts +0 -18
- package/apps/browserctl/src/commands/response-body.ts +0 -24
- package/apps/browserctl/src/commands/screenshot.ts +0 -16
- package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
- package/apps/browserctl/src/commands/session-drop.ts +0 -16
- package/apps/browserctl/src/commands/session-list.test.ts +0 -81
- package/apps/browserctl/src/commands/session-list.ts +0 -70
- package/apps/browserctl/src/commands/snapshot.ts +0 -16
- package/apps/browserctl/src/commands/status.ts +0 -10
- package/apps/browserctl/src/commands/storage-get.ts +0 -20
- package/apps/browserctl/src/commands/storage-set.ts +0 -22
- package/apps/browserctl/src/commands/tab-close.ts +0 -20
- package/apps/browserctl/src/commands/tab-focus.ts +0 -20
- package/apps/browserctl/src/commands/tab-open.ts +0 -19
- package/apps/browserctl/src/commands/tabs.ts +0 -13
- package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
- package/apps/browserctl/src/commands/trace-get.ts +0 -62
- package/apps/browserctl/src/commands/upload-arm.ts +0 -26
- package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-element.ts +0 -76
- package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
- package/apps/browserctl/src/commands/wait-text.ts +0 -93
- package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-url.ts +0 -76
- package/apps/browserctl/src/daemon-client.test.ts +0 -512
- package/apps/browserctl/src/daemon-client.ts +0 -632
- package/apps/browserctl/src/e2e.test.ts +0 -103
- package/apps/browserctl/src/main.dispatch.test.ts +0 -461
- package/apps/browserctl/src/main.test.ts +0 -334
- package/apps/browserctl/src/main.ts +0 -957
- package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
- package/apps/browserctl/src/test-port.ts +0 -26
- package/apps/browserd/src/bootstrap.ts +0 -432
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
- package/apps/browserd/src/container.ts +0 -3088
- package/apps/browserd/src/main.test.ts +0 -1522
- package/apps/browserd/src/main.ts +0 -7
- package/apps/browserd/src/test-port.ts +0 -26
- package/apps/browserd/src/tool-matrix.test.ts +0 -887
- package/bin/browserctl.cjs +0 -21
- package/bin/browserd.cjs +0 -21
- package/extensions/chrome-relay/README-CN.md +0 -39
- package/extensions/chrome-relay/README.md +0 -39
- package/extensions/chrome-relay/background.js +0 -1687
- package/extensions/chrome-relay/manifest.json +0 -15
- package/extensions/chrome-relay/popup.html +0 -369
- package/extensions/chrome-relay/popup.js +0 -972
- package/packages/core/src/bootstrap.test.ts +0 -10
- package/packages/core/src/driver-registry.test.ts +0 -45
- package/packages/core/src/driver-registry.ts +0 -22
- package/packages/core/src/driver.ts +0 -47
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/navigation-memory.test.ts +0 -259
- package/packages/core/src/navigation-memory.ts +0 -360
- package/packages/core/src/ref-cache.test.ts +0 -61
- package/packages/core/src/ref-cache.ts +0 -28
- package/packages/core/src/session-store.test.ts +0 -82
- package/packages/core/src/session-store.ts +0 -138
- package/packages/core/src/types.ts +0 -9
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
- package/packages/driver-chrome-relay/src/index.ts +0 -26
- package/packages/driver-managed/src/index.ts +0 -22
- package/packages/driver-managed/src/managed-driver.test.ts +0 -183
- package/packages/driver-managed/src/managed-driver.ts +0 -341
- package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
- package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
- package/packages/driver-remote-cdp/src/index.ts +0 -19
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
- package/packages/protocol/src/envelope.test.ts +0 -25
- package/packages/protocol/src/envelope.ts +0 -31
- package/packages/protocol/src/errors.test.ts +0 -17
- package/packages/protocol/src/errors.ts +0 -11
- package/packages/protocol/src/index.ts +0 -3
- package/packages/protocol/src/tools.ts +0 -3
- package/packages/transport-mcp-stdio/src/index.ts +0 -3
- package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
- package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
- package/packages/transport-mcp-stdio/src/server.ts +0 -183
- package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
|
@@ -1,3088 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DriverRegistry,
|
|
3
|
-
SessionStore,
|
|
4
|
-
createNavigationMemoryStore,
|
|
5
|
-
type BrowserDriver,
|
|
6
|
-
type BrowserDriverScreenshot,
|
|
7
|
-
type NavigationMemoryMode,
|
|
8
|
-
type NavigationSignal
|
|
9
|
-
} from "../../../packages/core/src";
|
|
10
|
-
import {
|
|
11
|
-
createChromeRelayDriver,
|
|
12
|
-
createChromeRelayExtensionRuntime
|
|
13
|
-
} from "../../../packages/driver-chrome-relay/src";
|
|
14
|
-
import {
|
|
15
|
-
createManagedDriver,
|
|
16
|
-
createManagedLocalDriver,
|
|
17
|
-
type ManagedLocalBrowserName,
|
|
18
|
-
type ManagedLocalTelemetryDriverExtensions
|
|
19
|
-
} from "../../../packages/driver-managed/src";
|
|
20
|
-
import { createRemoteCdpDriver } from "../../../packages/driver-remote-cdp/src";
|
|
21
|
-
import { ErrorCode, createErr, createOk, type ToolResponse } from "../../../packages/protocol/src";
|
|
22
|
-
import {
|
|
23
|
-
buildToolMap,
|
|
24
|
-
createMcpStdioServer,
|
|
25
|
-
type McpStdioServer,
|
|
26
|
-
type ToolCallArgs,
|
|
27
|
-
type ToolMap
|
|
28
|
-
} from "../../../packages/transport-mcp-stdio/src";
|
|
29
|
-
import { posix as posixPath, win32 as windowsPath } from "node:path";
|
|
30
|
-
import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
|
|
31
|
-
|
|
32
|
-
type CapabilityScope = "read" | "act" | "upload" | "download";
|
|
33
|
-
type ChromeRelayMode = "cdp" | "extension";
|
|
34
|
-
type DomainAllowlistMode = "off" | "enforce";
|
|
35
|
-
|
|
36
|
-
export type BrowserdConfig = {
|
|
37
|
-
chromeRelayUrl: string;
|
|
38
|
-
chromeRelayMode: ChromeRelayMode;
|
|
39
|
-
chromeRelayExtensionToken?: string;
|
|
40
|
-
chromeRelayExtensionRequestTimeoutMs: number;
|
|
41
|
-
remoteCdpUrl: string;
|
|
42
|
-
defaultDriver: string;
|
|
43
|
-
managedLocalEnabled: boolean;
|
|
44
|
-
uploadRoot?: string;
|
|
45
|
-
downloadRoot?: string;
|
|
46
|
-
authToken?: string;
|
|
47
|
-
authScopes: CapabilityScope[];
|
|
48
|
-
sessionTtlMs: number;
|
|
49
|
-
sessionCleanupIntervalMs: number;
|
|
50
|
-
domainAllowlistMode: DomainAllowlistMode;
|
|
51
|
-
domainAllowlist: string[];
|
|
52
|
-
sessionMaxTotal: number;
|
|
53
|
-
sessionMaxPerTenant: number;
|
|
54
|
-
sessionRequireTenantPrefix: boolean;
|
|
55
|
-
tenantAllowlist: string[];
|
|
56
|
-
memoryEnabled: boolean;
|
|
57
|
-
memoryMode: NavigationMemoryMode;
|
|
58
|
-
memoryTtlDays: number;
|
|
59
|
-
memoryPath: string;
|
|
60
|
-
managedLocalLaunch: {
|
|
61
|
-
browserName: ManagedLocalBrowserName;
|
|
62
|
-
headless: boolean;
|
|
63
|
-
channel?: string;
|
|
64
|
-
executablePath?: string;
|
|
65
|
-
launchTimeoutMs?: number;
|
|
66
|
-
args: string[];
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const DEFAULT_DRIVER_KEY = "managed";
|
|
71
|
-
const MANAGED_LOCAL_DRIVER_KEY = "managed-local";
|
|
72
|
-
const DEFAULT_CONFIG_DRIVER_KEY = "chrome-relay";
|
|
73
|
-
const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
|
|
74
|
-
const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
|
|
75
|
-
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "extension";
|
|
76
|
-
const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
|
|
77
|
-
const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
|
|
78
|
-
const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
|
|
79
|
-
const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
|
|
80
|
-
const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1_000;
|
|
81
|
-
const DEFAULT_SESSION_CLEANUP_INTERVAL_MS = 60 * 1_000;
|
|
82
|
-
const DEFAULT_DOMAIN_ALLOWLIST_MODE: DomainAllowlistMode = "off";
|
|
83
|
-
const DEFAULT_SESSION_MAX_TOTAL = 200;
|
|
84
|
-
const DEFAULT_SESSION_MAX_PER_TENANT = 50;
|
|
85
|
-
const DEFAULT_MEMORY_MODE: NavigationMemoryMode = "ask";
|
|
86
|
-
const DEFAULT_MEMORY_TTL_DAYS = 30;
|
|
87
|
-
const DEFAULT_MEMORY_PATH = ".browserctl-runtime/navigation-memory.json";
|
|
88
|
-
const ALL_CAPABILITY_SCOPES: readonly CapabilityScope[] = ["read", "act", "upload", "download"];
|
|
89
|
-
const TOOL_SCOPE_BY_NAME: Readonly<Record<string, CapabilityScope>> = {
|
|
90
|
-
"browser.status": "read",
|
|
91
|
-
"browser.profile.list": "read",
|
|
92
|
-
"browser.profile.use": "act",
|
|
93
|
-
"browser.tab.list": "read",
|
|
94
|
-
"browser.tab.open": "act",
|
|
95
|
-
"browser.tab.focus": "act",
|
|
96
|
-
"browser.tab.close": "act",
|
|
97
|
-
"browser.snapshot": "read",
|
|
98
|
-
"browser.screenshot": "read",
|
|
99
|
-
"browser.dom.query": "read",
|
|
100
|
-
"browser.dom.queryAll": "read",
|
|
101
|
-
"browser.element.screenshot": "read",
|
|
102
|
-
"browser.a11y.snapshot": "read",
|
|
103
|
-
"browser.wait.element": "read",
|
|
104
|
-
"browser.wait.text": "read",
|
|
105
|
-
"browser.wait.url": "read",
|
|
106
|
-
"browser.network.list": "read",
|
|
107
|
-
"browser.network.harExport": "read",
|
|
108
|
-
"browser.act": "act",
|
|
109
|
-
"browser.upload.arm": "upload",
|
|
110
|
-
"browser.dialog.arm": "act",
|
|
111
|
-
"browser.download.wait": "download",
|
|
112
|
-
"browser.download.trigger": "download",
|
|
113
|
-
"browser.network.waitFor": "read",
|
|
114
|
-
"browser.cookie.get": "read",
|
|
115
|
-
"browser.cookie.set": "act",
|
|
116
|
-
"browser.cookie.clear": "act",
|
|
117
|
-
"browser.storage.get": "read",
|
|
118
|
-
"browser.storage.set": "act",
|
|
119
|
-
"browser.frame.list": "read",
|
|
120
|
-
"browser.frame.snapshot": "read",
|
|
121
|
-
"browser.console.list": "read",
|
|
122
|
-
"browser.network.responseBody": "read",
|
|
123
|
-
"browser.memory.status": "read",
|
|
124
|
-
"browser.memory.resolve": "read",
|
|
125
|
-
"browser.memory.upsert": "act",
|
|
126
|
-
"browser.memory.list": "read",
|
|
127
|
-
"browser.memory.inspect": "read",
|
|
128
|
-
"browser.memory.delete": "act",
|
|
129
|
-
"browser.memory.purge": "act",
|
|
130
|
-
"browser.memory.mode.set": "act",
|
|
131
|
-
"browser.memory.ttl.set": "act",
|
|
132
|
-
"browser.trace.get": "read",
|
|
133
|
-
"browser.session.list": "read",
|
|
134
|
-
"browser.session.drop": "act"
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
function resolveNonEmptyString(value: string | undefined): string | undefined {
|
|
138
|
-
if (value === undefined) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const trimmedValue = value.trim();
|
|
143
|
-
return trimmedValue.length === 0 ? undefined : trimmedValue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function parseBooleanFlag(value: string | undefined, fallback: boolean): boolean {
|
|
147
|
-
if (value === undefined) {
|
|
148
|
-
return fallback;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const normalizedValue = value.trim().toLowerCase();
|
|
152
|
-
if (normalizedValue === "1" || normalizedValue === "true" || normalizedValue === "yes" || normalizedValue === "on") {
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (normalizedValue === "0" || normalizedValue === "false" || normalizedValue === "no" || normalizedValue === "off") {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return fallback;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function parseOptionalNumber(value: string | undefined): number | undefined {
|
|
164
|
-
const parsedValue = resolveNonEmptyString(value);
|
|
165
|
-
if (parsedValue === undefined) {
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const parsedNumber = Number(parsedValue);
|
|
170
|
-
if (!Number.isFinite(parsedNumber) || parsedNumber < 0) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return parsedNumber;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function parseOptionalPositiveInteger(value: string | undefined): number | undefined {
|
|
178
|
-
const parsedNumber = parseOptionalNumber(value);
|
|
179
|
-
if (parsedNumber === undefined || parsedNumber <= 0) {
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return Math.trunc(parsedNumber);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function parseCsv(value: string | undefined): string[] {
|
|
187
|
-
const rawValue = resolveNonEmptyString(value);
|
|
188
|
-
if (rawValue === undefined) {
|
|
189
|
-
return [];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return rawValue
|
|
193
|
-
.split(",")
|
|
194
|
-
.map((item) => item.trim())
|
|
195
|
-
.filter((item) => item.length > 0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function isCapabilityScope(value: string): value is CapabilityScope {
|
|
199
|
-
return (
|
|
200
|
-
value === "read" ||
|
|
201
|
-
value === "act" ||
|
|
202
|
-
value === "upload" ||
|
|
203
|
-
value === "download"
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function parseCapabilityScopes(value: string | undefined): CapabilityScope[] {
|
|
208
|
-
const parsed = parseCsv(value).map((item) => item.toLowerCase());
|
|
209
|
-
if (parsed.length === 0) {
|
|
210
|
-
return [...ALL_CAPABILITY_SCOPES];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const scopes: CapabilityScope[] = [];
|
|
214
|
-
for (const item of parsed) {
|
|
215
|
-
if (!isCapabilityScope(item) || scopes.includes(item)) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
scopes.push(item);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return scopes;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBrowserName {
|
|
226
|
-
const normalizedValue = value?.trim().toLowerCase();
|
|
227
|
-
if (normalizedValue === "firefox" || normalizedValue === "webkit" || normalizedValue === "chromium") {
|
|
228
|
-
return normalizedValue;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return DEFAULT_MANAGED_LOCAL_BROWSER_NAME;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
|
|
235
|
-
const normalizedValue = value?.trim().toLowerCase();
|
|
236
|
-
if (normalizedValue === "extension" || normalizedValue === "cdp") {
|
|
237
|
-
return normalizedValue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return DEFAULT_CHROME_RELAY_MODE;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function parseDomainAllowlistMode(value: string | undefined): DomainAllowlistMode {
|
|
244
|
-
const normalizedValue = value?.trim().toLowerCase();
|
|
245
|
-
if (normalizedValue === "off" || normalizedValue === "enforce") {
|
|
246
|
-
return normalizedValue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return DEFAULT_DOMAIN_ALLOWLIST_MODE;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function parseNavigationMemoryMode(value: string | undefined): NavigationMemoryMode {
|
|
253
|
-
const normalizedValue = value?.trim().toLowerCase();
|
|
254
|
-
if (normalizedValue === "off" || normalizedValue === "ask" || normalizedValue === "auto") {
|
|
255
|
-
return normalizedValue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return DEFAULT_MEMORY_MODE;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export type BrowserdContainer = {
|
|
262
|
-
config: BrowserdConfig;
|
|
263
|
-
drivers: Map<string, BrowserDriver>;
|
|
264
|
-
driverRegistry: DriverRegistry;
|
|
265
|
-
sessions: SessionStore;
|
|
266
|
-
mcpServer: McpStdioServer;
|
|
267
|
-
close(): void;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
export function loadBrowserdConfig(
|
|
271
|
-
env: Record<string, string | undefined> = process.env
|
|
272
|
-
): BrowserdConfig {
|
|
273
|
-
const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
|
|
274
|
-
const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
|
|
275
|
-
const chromeRelayExtensionToken =
|
|
276
|
-
resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN) ??
|
|
277
|
-
(chromeRelayMode === "extension" ? DEFAULT_CHROME_RELAY_EXTENSION_TOKEN : undefined);
|
|
278
|
-
|
|
279
|
-
const defaultDriver =
|
|
280
|
-
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ?? DEFAULT_CONFIG_DRIVER_KEY;
|
|
281
|
-
const domainAllowlistMode = parseDomainAllowlistMode(env.BROWSERD_DOMAIN_ALLOWLIST_MODE);
|
|
282
|
-
const memoryEnabled = parseBooleanFlag(env.BROWSERD_MEMORY_ENABLED, true);
|
|
283
|
-
const memoryMode = parseNavigationMemoryMode(env.BROWSERD_MEMORY_MODE);
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
|
|
287
|
-
chromeRelayMode,
|
|
288
|
-
chromeRelayExtensionToken,
|
|
289
|
-
chromeRelayExtensionRequestTimeoutMs:
|
|
290
|
-
parseOptionalNumber(env.BROWSERD_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS) ??
|
|
291
|
-
DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS,
|
|
292
|
-
remoteCdpUrl:
|
|
293
|
-
env.BROWSERD_REMOTE_CDP_URL ?? "http://127.0.0.1:9222/devtools/browser/default",
|
|
294
|
-
defaultDriver,
|
|
295
|
-
managedLocalEnabled,
|
|
296
|
-
uploadRoot: resolveNonEmptyString(env.BROWSERD_UPLOAD_ROOT),
|
|
297
|
-
downloadRoot: resolveNonEmptyString(env.BROWSERD_DOWNLOAD_ROOT),
|
|
298
|
-
authToken: resolveNonEmptyString(env.BROWSERD_AUTH_TOKEN),
|
|
299
|
-
authScopes: parseCapabilityScopes(env.BROWSERD_AUTH_SCOPES),
|
|
300
|
-
sessionTtlMs: parseOptionalNumber(env.BROWSERD_SESSION_TTL_MS) ?? DEFAULT_SESSION_TTL_MS,
|
|
301
|
-
sessionCleanupIntervalMs:
|
|
302
|
-
parseOptionalNumber(env.BROWSERD_SESSION_CLEANUP_INTERVAL_MS) ??
|
|
303
|
-
DEFAULT_SESSION_CLEANUP_INTERVAL_MS,
|
|
304
|
-
domainAllowlistMode,
|
|
305
|
-
domainAllowlist: parseCsv(env.BROWSERD_DOMAIN_ALLOWLIST).map((entry) => entry.toLowerCase()),
|
|
306
|
-
sessionMaxTotal:
|
|
307
|
-
parseOptionalPositiveInteger(env.BROWSERD_SESSION_MAX_TOTAL) ?? DEFAULT_SESSION_MAX_TOTAL,
|
|
308
|
-
sessionMaxPerTenant:
|
|
309
|
-
parseOptionalPositiveInteger(env.BROWSERD_SESSION_MAX_PER_TENANT) ?? DEFAULT_SESSION_MAX_PER_TENANT,
|
|
310
|
-
sessionRequireTenantPrefix: parseBooleanFlag(env.BROWSERD_SESSION_REQUIRE_TENANT_PREFIX, false),
|
|
311
|
-
tenantAllowlist: parseCsv(env.BROWSERD_TENANT_ALLOWLIST).map((entry) => entry.toLowerCase()),
|
|
312
|
-
memoryEnabled,
|
|
313
|
-
memoryMode,
|
|
314
|
-
memoryTtlDays: parseOptionalNumber(env.BROWSERD_MEMORY_TTL_DAYS) ?? DEFAULT_MEMORY_TTL_DAYS,
|
|
315
|
-
memoryPath: resolveNonEmptyString(env.BROWSERD_MEMORY_PATH) ?? DEFAULT_MEMORY_PATH,
|
|
316
|
-
managedLocalLaunch: {
|
|
317
|
-
browserName: parseManagedLocalBrowserName(env.BROWSERD_MANAGED_LOCAL_BROWSER),
|
|
318
|
-
headless: parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_HEADLESS, DEFAULT_MANAGED_LOCAL_HEADLESS),
|
|
319
|
-
channel: resolveNonEmptyString(env.BROWSERD_MANAGED_LOCAL_CHANNEL),
|
|
320
|
-
executablePath: resolveNonEmptyString(env.BROWSERD_MANAGED_LOCAL_EXECUTABLE_PATH),
|
|
321
|
-
launchTimeoutMs: parseOptionalNumber(env.BROWSERD_MANAGED_LOCAL_LAUNCH_TIMEOUT_MS),
|
|
322
|
-
args: parseCsv(env.BROWSERD_MANAGED_LOCAL_ARGS)
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
328
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function isStringArray(value: unknown): value is string[] {
|
|
332
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function toErrorMessage(error: unknown): string {
|
|
336
|
-
return error instanceof Error ? error.message : "Unexpected browserd failure.";
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function mapDriverError(error: unknown): ToolResponse<never> {
|
|
340
|
-
const message = toErrorMessage(error);
|
|
341
|
-
if (message.startsWith("Unknown targetId:")) {
|
|
342
|
-
return createErr(ErrorCode.E_NOT_FOUND, message);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (message.startsWith("Unknown driver:")) {
|
|
346
|
-
return createErr(ErrorCode.E_NOT_FOUND, message);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return createErr(ErrorCode.E_INTERNAL, message);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function requireStringArg(args: ToolCallArgs, key: string): ToolResponse<string> {
|
|
353
|
-
const value = args[key];
|
|
354
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
355
|
-
return createErr(ErrorCode.E_INVALID_ARG, `${key} is required and must be a non-empty string.`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return createOk(value.trim());
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function resolveTargetId(args: ToolCallArgs, sessions: SessionStore): ToolResponse<string> {
|
|
362
|
-
const explicit = requireStringArg(args, "targetId");
|
|
363
|
-
if (explicit.ok) {
|
|
364
|
-
sessions.useTarget(args.sessionId, explicit.data);
|
|
365
|
-
return explicit;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const sessionTarget = sessions.get(args.sessionId)?.targetId;
|
|
369
|
-
if (sessionTarget === undefined) {
|
|
370
|
-
return explicit;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return createOk(sessionTarget);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function resolveAction(args: ToolCallArgs): ToolResponse<{ type: string; payload?: Record<string, unknown> }> {
|
|
377
|
-
const rawAction = args.action;
|
|
378
|
-
if (!isObjectRecord(rawAction)) {
|
|
379
|
-
return createErr(ErrorCode.E_INVALID_ARG, "action is required and must be an object.");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const actionType = rawAction.type;
|
|
383
|
-
if (typeof actionType !== "string" || actionType.trim().length === 0) {
|
|
384
|
-
return createErr(ErrorCode.E_INVALID_ARG, "action.type is required and must be a non-empty string.");
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const rawPayload = rawAction.payload;
|
|
388
|
-
if (rawPayload !== undefined && !isObjectRecord(rawPayload)) {
|
|
389
|
-
return createErr(ErrorCode.E_INVALID_ARG, "action.payload must be an object when provided.");
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return createOk({
|
|
393
|
-
type: actionType.trim(),
|
|
394
|
-
payload: rawPayload as Record<string, unknown> | undefined
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function resolveFiles(args: ToolCallArgs): ToolResponse<string[]> {
|
|
399
|
-
const files = args.files;
|
|
400
|
-
if (!isStringArray(files)) {
|
|
401
|
-
return createErr(ErrorCode.E_INVALID_ARG, "files is required and must be a string array.");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const normalizedFiles = files.map((file) => file.trim());
|
|
405
|
-
if (normalizedFiles.some((file) => file.length === 0)) {
|
|
406
|
-
return createErr(
|
|
407
|
-
ErrorCode.E_INVALID_ARG,
|
|
408
|
-
"files must contain only non-empty string paths."
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return createOk(normalizedFiles);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function resolveRequestId(args: ToolCallArgs): ToolResponse<string> {
|
|
416
|
-
return requireStringArg(args, "requestId");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function resolveOptionalPathArg(args: ToolCallArgs, key: string): ToolResponse<string | undefined> {
|
|
420
|
-
const value = args[key];
|
|
421
|
-
if (value === undefined) {
|
|
422
|
-
return createOk(undefined);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
426
|
-
return createErr(
|
|
427
|
-
ErrorCode.E_INVALID_ARG,
|
|
428
|
-
`${key} must be a non-empty string when provided.`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return createOk(value.trim());
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function resolveDriver(
|
|
436
|
-
args: ToolCallArgs,
|
|
437
|
-
drivers: Map<string, BrowserDriver>,
|
|
438
|
-
sessions: SessionStore,
|
|
439
|
-
defaultDriverKey: string
|
|
440
|
-
): ToolResponse<{ driverKey: string; driver: BrowserDriver }> {
|
|
441
|
-
const requestedProfile = args.profile;
|
|
442
|
-
if (requestedProfile !== undefined) {
|
|
443
|
-
const requestedDriver = drivers.get(requestedProfile);
|
|
444
|
-
if (requestedDriver === undefined) {
|
|
445
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown driver profile: ${requestedProfile}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
sessions.useProfile(args.sessionId, requestedProfile);
|
|
449
|
-
return createOk({
|
|
450
|
-
driverKey: requestedProfile,
|
|
451
|
-
driver: requestedDriver
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const sessionProfile = sessions.get(args.sessionId)?.profile;
|
|
456
|
-
if (sessionProfile !== undefined) {
|
|
457
|
-
const sessionDriver = drivers.get(sessionProfile);
|
|
458
|
-
if (sessionDriver !== undefined) {
|
|
459
|
-
return createOk({
|
|
460
|
-
driverKey: sessionProfile,
|
|
461
|
-
driver: sessionDriver
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const defaultDriver = drivers.get(defaultDriverKey);
|
|
467
|
-
if (defaultDriver === undefined) {
|
|
468
|
-
return createErr(ErrorCode.E_DRIVER_UNAVAILABLE, `Default driver is not registered: ${defaultDriverKey}`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
sessions.useProfile(args.sessionId, defaultDriverKey);
|
|
472
|
-
return createOk({
|
|
473
|
-
driverKey: defaultDriverKey,
|
|
474
|
-
driver: defaultDriver
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async function runWithDriver<TData>(
|
|
479
|
-
args: ToolCallArgs,
|
|
480
|
-
drivers: Map<string, BrowserDriver>,
|
|
481
|
-
sessions: SessionStore,
|
|
482
|
-
defaultDriverKey: string,
|
|
483
|
-
operation: (
|
|
484
|
-
driver: BrowserDriver,
|
|
485
|
-
driverKey: string
|
|
486
|
-
) => Promise<ToolResponse<TData>> | ToolResponse<TData>
|
|
487
|
-
): Promise<ToolResponse<TData>> {
|
|
488
|
-
const resolvedDriver = resolveDriver(args, drivers, sessions, defaultDriverKey);
|
|
489
|
-
if (!resolvedDriver.ok) {
|
|
490
|
-
return resolvedDriver;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
return await operation(resolvedDriver.data.driver, resolvedDriver.data.driverKey);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
return mapDriverError(error);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function isTargetKnownSnapshot(snapshot: unknown): boolean {
|
|
501
|
-
if (!isObjectRecord(snapshot)) {
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return snapshot.hasTarget === true;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function hasConsoleEntriesGetter(
|
|
509
|
-
driver: BrowserDriver
|
|
510
|
-
): driver is BrowserDriver & {
|
|
511
|
-
getConsoleEntries: NonNullable<ManagedLocalTelemetryDriverExtensions["getConsoleEntries"]>;
|
|
512
|
-
} {
|
|
513
|
-
return typeof (driver as ManagedLocalTelemetryDriverExtensions).getConsoleEntries === "function";
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function hasNetworkResponseBodyGetter(
|
|
517
|
-
driver: BrowserDriver
|
|
518
|
-
): driver is BrowserDriver & {
|
|
519
|
-
getNetworkResponseBody: NonNullable<ManagedLocalTelemetryDriverExtensions["getNetworkResponseBody"]>;
|
|
520
|
-
} {
|
|
521
|
-
return typeof (driver as ManagedLocalTelemetryDriverExtensions).getNetworkResponseBody === "function";
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function hasScreenshotGetter(
|
|
525
|
-
driver: BrowserDriver
|
|
526
|
-
): driver is BrowserDriver & BrowserDriverScreenshot {
|
|
527
|
-
return typeof (driver as Partial<BrowserDriverScreenshot>).screenshot === "function";
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function resolveOptionalStringArg(args: ToolCallArgs, key: string): ToolResponse<string | undefined> {
|
|
531
|
-
const value = args[key];
|
|
532
|
-
if (value === undefined) {
|
|
533
|
-
return createOk(undefined);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (typeof value !== "string") {
|
|
537
|
-
return createErr(
|
|
538
|
-
ErrorCode.E_INVALID_ARG,
|
|
539
|
-
`${key} must be a string when provided.`
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const trimmedValue = value.trim();
|
|
544
|
-
return trimmedValue.length === 0 ? createOk(undefined) : createOk(trimmedValue);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function resolveOptionalNumberArg(args: ToolCallArgs, key: string): ToolResponse<number | undefined> {
|
|
548
|
-
const value = args[key];
|
|
549
|
-
if (value === undefined) {
|
|
550
|
-
return createOk(undefined);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
554
|
-
return createErr(
|
|
555
|
-
ErrorCode.E_INVALID_ARG,
|
|
556
|
-
`${key} must be a finite number when provided.`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return createOk(value);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function resolveOptionalIntegerArg(
|
|
564
|
-
args: ToolCallArgs,
|
|
565
|
-
key: string,
|
|
566
|
-
minimum: number
|
|
567
|
-
): ToolResponse<number | undefined> {
|
|
568
|
-
const resolved = resolveOptionalNumberArg(args, key);
|
|
569
|
-
if (!resolved.ok || resolved.data === undefined) {
|
|
570
|
-
return resolved;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (!Number.isInteger(resolved.data) || resolved.data < minimum) {
|
|
574
|
-
return createErr(
|
|
575
|
-
ErrorCode.E_INVALID_ARG,
|
|
576
|
-
`${key} must be an integer greater than or equal to ${minimum}.`
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return resolved;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function resolveOptionalTimestampArg(
|
|
584
|
-
args: ToolCallArgs,
|
|
585
|
-
key: string
|
|
586
|
-
): ToolResponse<number | undefined> {
|
|
587
|
-
const value = args[key];
|
|
588
|
-
if (value === undefined) {
|
|
589
|
-
return createOk(undefined);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (typeof value !== "string") {
|
|
593
|
-
return createErr(
|
|
594
|
-
ErrorCode.E_INVALID_ARG,
|
|
595
|
-
`${key} must be an ISO timestamp string when provided.`
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const trimmedValue = value.trim();
|
|
600
|
-
if (trimmedValue.length === 0) {
|
|
601
|
-
return createErr(
|
|
602
|
-
ErrorCode.E_INVALID_ARG,
|
|
603
|
-
`${key} must be an ISO timestamp string when provided.`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const parsedValue = Date.parse(trimmedValue);
|
|
608
|
-
if (!Number.isFinite(parsedValue)) {
|
|
609
|
-
return createErr(
|
|
610
|
-
ErrorCode.E_INVALID_ARG,
|
|
611
|
-
`${key} must be a valid ISO timestamp string.`
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return createOk(parsedValue);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function mapNavigationMemoryError(error: unknown): ToolResponse<never> {
|
|
619
|
-
const message = toErrorMessage(error);
|
|
620
|
-
if (
|
|
621
|
-
message === "Navigation memory write requires confirmation in ask mode." ||
|
|
622
|
-
message === "Navigation memory write is disabled when mode is off."
|
|
623
|
-
) {
|
|
624
|
-
return createErr(ErrorCode.E_PERMISSION, message);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return createErr(ErrorCode.E_INTERNAL, message);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function resolveNavigationMemoryScope(
|
|
631
|
-
args: ToolCallArgs
|
|
632
|
-
): ToolResponse<{ domain: string; profileId: string; intentKey: string }> {
|
|
633
|
-
const domain = requireStringArg(args, "domain");
|
|
634
|
-
if (!domain.ok) {
|
|
635
|
-
return domain;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const profileId = requireStringArg(args, "profileId");
|
|
639
|
-
if (!profileId.ok) {
|
|
640
|
-
return profileId;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const intentKey = requireStringArg(args, "intentKey");
|
|
644
|
-
if (!intentKey.ok) {
|
|
645
|
-
return intentKey;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return createOk({
|
|
649
|
-
domain: domain.data,
|
|
650
|
-
profileId: profileId.data,
|
|
651
|
-
intentKey: intentKey.data
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function resolveNavigationMemoryMode(args: ToolCallArgs): ToolResponse<NavigationMemoryMode> {
|
|
656
|
-
const mode = requireStringArg(args, "mode");
|
|
657
|
-
if (!mode.ok) {
|
|
658
|
-
return mode;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (mode.data !== "off" && mode.data !== "ask" && mode.data !== "auto") {
|
|
662
|
-
return createErr(ErrorCode.E_INVALID_ARG, "mode must be one of: off, ask, auto.");
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
return createOk(mode.data);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const MAX_NAVIGATION_SIGNAL_LENGTH = 256;
|
|
669
|
-
const MAX_NAVIGATION_SELECTOR_TOKEN_COUNT = 16;
|
|
670
|
-
const STRUCTURED_SELECTOR_MARKER = /[#.\[:>+~*=]/;
|
|
671
|
-
const SIMPLE_SELECTOR_TOKEN_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
672
|
-
|
|
673
|
-
function containsControlWhitespace(value: string): boolean {
|
|
674
|
-
return /[\r\n\t]/.test(value);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function isStructuredRoutePattern(value: string): boolean {
|
|
678
|
-
if (value.length === 0 || value.length > MAX_NAVIGATION_SIGNAL_LENGTH) {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (containsControlWhitespace(value) || /\s/.test(value)) {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return (
|
|
687
|
-
value.startsWith("/") ||
|
|
688
|
-
value.startsWith("http://") ||
|
|
689
|
-
value.startsWith("https://") ||
|
|
690
|
-
value.includes("://") ||
|
|
691
|
-
value.includes("?") ||
|
|
692
|
-
value.includes("#") ||
|
|
693
|
-
value.includes("*")
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function isStructuredSelectorPattern(value: string): boolean {
|
|
698
|
-
if (value.length === 0 || value.length > MAX_NAVIGATION_SIGNAL_LENGTH) {
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (containsControlWhitespace(value)) {
|
|
703
|
-
return false;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const tokens = value.split(/\s+/).filter((token) => token.length > 0);
|
|
707
|
-
if (tokens.length === 0 || tokens.length > MAX_NAVIGATION_SELECTOR_TOKEN_COUNT) {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (tokens.length === 1 && SIMPLE_SELECTOR_TOKEN_PATTERN.test(tokens[0])) {
|
|
712
|
-
return true;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return STRUCTURED_SELECTOR_MARKER.test(value);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function resolveNavigationSignals(args: ToolCallArgs): ToolResponse<NavigationSignal[]> {
|
|
719
|
-
const rawSignals = args.signals;
|
|
720
|
-
if (!Array.isArray(rawSignals)) {
|
|
721
|
-
return createErr(ErrorCode.E_INVALID_ARG, "signals is required and must be an array.");
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const signals: NavigationSignal[] = [];
|
|
725
|
-
for (const [index, rawSignal] of rawSignals.entries()) {
|
|
726
|
-
if (!isObjectRecord(rawSignal)) {
|
|
727
|
-
return createErr(
|
|
728
|
-
ErrorCode.E_INVALID_ARG,
|
|
729
|
-
`signals[${index}] must be an object.`
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const kind = rawSignal.kind;
|
|
734
|
-
if (kind === "urlPattern" || kind === "selector") {
|
|
735
|
-
const value = rawSignal.value;
|
|
736
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
737
|
-
return createErr(
|
|
738
|
-
ErrorCode.E_INVALID_ARG,
|
|
739
|
-
`signals[${index}].value must be a non-empty string.`
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const normalizedValue = value.trim();
|
|
744
|
-
if (
|
|
745
|
-
(kind === "urlPattern" && !isStructuredRoutePattern(normalizedValue)) ||
|
|
746
|
-
(kind === "selector" && !isStructuredSelectorPattern(normalizedValue))
|
|
747
|
-
) {
|
|
748
|
-
const expectedFormat =
|
|
749
|
-
kind === "urlPattern"
|
|
750
|
-
? "a structured URL pattern like /route/* or https://host/path*"
|
|
751
|
-
: "a structured selector like #id, .class, [data-x], or main .item";
|
|
752
|
-
return createErr(
|
|
753
|
-
ErrorCode.E_INVALID_ARG,
|
|
754
|
-
`signals[${index}].value must be ${expectedFormat}.`
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
signals.push({
|
|
759
|
-
kind,
|
|
760
|
-
value: normalizedValue
|
|
761
|
-
});
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (kind === "hop") {
|
|
766
|
-
const from = rawSignal.from;
|
|
767
|
-
const to = rawSignal.to;
|
|
768
|
-
if (
|
|
769
|
-
typeof from !== "string" ||
|
|
770
|
-
from.trim().length === 0 ||
|
|
771
|
-
typeof to !== "string" ||
|
|
772
|
-
to.trim().length === 0
|
|
773
|
-
) {
|
|
774
|
-
return createErr(
|
|
775
|
-
ErrorCode.E_INVALID_ARG,
|
|
776
|
-
`signals[${index}] hop requires non-empty string from/to values.`
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const normalizedFrom = from.trim();
|
|
781
|
-
const normalizedTo = to.trim();
|
|
782
|
-
if (!isStructuredRoutePattern(normalizedFrom) || !isStructuredRoutePattern(normalizedTo)) {
|
|
783
|
-
return createErr(
|
|
784
|
-
ErrorCode.E_INVALID_ARG,
|
|
785
|
-
`signals[${index}] hop from/to must be structured route patterns like /from and /to.`
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
signals.push({
|
|
790
|
-
kind: "hop",
|
|
791
|
-
from: normalizedFrom,
|
|
792
|
-
to: normalizedTo
|
|
793
|
-
});
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return createErr(
|
|
798
|
-
ErrorCode.E_INVALID_ARG,
|
|
799
|
-
`signals[${index}].kind must be one of: urlPattern, selector, hop.`
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
return createOk(signals);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function resolveDriverActionData(
|
|
807
|
-
actionResult: unknown,
|
|
808
|
-
actionType: string
|
|
809
|
-
): ToolResponse<Record<string, unknown>> {
|
|
810
|
-
if (!isObjectRecord(actionResult)) {
|
|
811
|
-
return createErr(ErrorCode.E_INTERNAL, `Invalid action result payload for ${actionType}.`);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (actionResult.targetKnown === false) {
|
|
815
|
-
return createErr(
|
|
816
|
-
ErrorCode.E_NOT_FOUND,
|
|
817
|
-
typeof actionResult.error === "string"
|
|
818
|
-
? actionResult.error
|
|
819
|
-
: `Unknown target for action ${actionType}.`
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (actionResult.ok !== true) {
|
|
824
|
-
return createErr(
|
|
825
|
-
ErrorCode.E_INTERNAL,
|
|
826
|
-
typeof actionResult.error === "string"
|
|
827
|
-
? actionResult.error
|
|
828
|
-
: `Driver action failed: ${actionType}`
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (actionResult.executed !== true) {
|
|
833
|
-
return createErr(
|
|
834
|
-
ErrorCode.E_DRIVER_UNAVAILABLE,
|
|
835
|
-
`Driver does not support action: ${actionType}`
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const data = actionResult.data;
|
|
840
|
-
if (data === undefined) {
|
|
841
|
-
return createOk({});
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
if (!isObjectRecord(data)) {
|
|
845
|
-
return createErr(ErrorCode.E_INTERNAL, `Invalid action data payload for ${actionType}.`);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return createOk(data);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
type NetworkRequestSummary = {
|
|
852
|
-
requestId: string;
|
|
853
|
-
url: string;
|
|
854
|
-
method?: string;
|
|
855
|
-
status?: number;
|
|
856
|
-
resourceType?: string;
|
|
857
|
-
timestamp?: string;
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
function readNetworkRequestSummaries(snapshot: unknown): NetworkRequestSummary[] {
|
|
861
|
-
if (!isObjectRecord(snapshot)) {
|
|
862
|
-
return [];
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const rawSummaries = snapshot.requestSummaries;
|
|
866
|
-
if (!Array.isArray(rawSummaries)) {
|
|
867
|
-
return [];
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const summaries: NetworkRequestSummary[] = [];
|
|
871
|
-
for (const rawSummary of rawSummaries) {
|
|
872
|
-
if (!isObjectRecord(rawSummary)) {
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const requestId = rawSummary.requestId;
|
|
877
|
-
const url = rawSummary.url;
|
|
878
|
-
if (typeof requestId !== "string" || typeof url !== "string") {
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const method =
|
|
883
|
-
typeof rawSummary.method === "string" && rawSummary.method.trim().length > 0
|
|
884
|
-
? rawSummary.method
|
|
885
|
-
: undefined;
|
|
886
|
-
const status =
|
|
887
|
-
typeof rawSummary.status === "number" && Number.isFinite(rawSummary.status)
|
|
888
|
-
? rawSummary.status
|
|
889
|
-
: undefined;
|
|
890
|
-
const resourceType =
|
|
891
|
-
typeof rawSummary.resourceType === "string" && rawSummary.resourceType.trim().length > 0
|
|
892
|
-
? rawSummary.resourceType
|
|
893
|
-
: undefined;
|
|
894
|
-
const timestamp =
|
|
895
|
-
typeof rawSummary.timestamp === "string" && rawSummary.timestamp.trim().length > 0
|
|
896
|
-
? rawSummary.timestamp
|
|
897
|
-
: undefined;
|
|
898
|
-
|
|
899
|
-
summaries.push({
|
|
900
|
-
requestId,
|
|
901
|
-
url,
|
|
902
|
-
...(method !== undefined ? { method } : {}),
|
|
903
|
-
...(status !== undefined ? { status } : {}),
|
|
904
|
-
...(resourceType !== undefined ? { resourceType } : {}),
|
|
905
|
-
...(timestamp !== undefined ? { timestamp } : {})
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
return summaries;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
type NetworkSummaryFilters = {
|
|
913
|
-
urlContains?: string;
|
|
914
|
-
method?: string;
|
|
915
|
-
status?: number;
|
|
916
|
-
sinceMs?: number;
|
|
917
|
-
limit?: number;
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
type ConsoleEntry = {
|
|
921
|
-
type?: string;
|
|
922
|
-
text?: string;
|
|
923
|
-
timestamp?: string;
|
|
924
|
-
[key: string]: unknown;
|
|
925
|
-
};
|
|
926
|
-
|
|
927
|
-
type ConsoleFilters = {
|
|
928
|
-
type?: string;
|
|
929
|
-
contains?: string;
|
|
930
|
-
sinceMs?: number;
|
|
931
|
-
limit?: number;
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
function readConsoleEntries(value: unknown): ConsoleEntry[] {
|
|
935
|
-
if (!Array.isArray(value)) {
|
|
936
|
-
return [];
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return value
|
|
940
|
-
.filter(isObjectRecord)
|
|
941
|
-
.map((entry) => ({ ...entry }));
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function applyLimitToTail<TValue>(values: TValue[], limit: number | undefined): TValue[] {
|
|
945
|
-
if (limit === undefined || values.length <= limit) {
|
|
946
|
-
return values;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
return values.slice(values.length - limit);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function filterConsoleEntries(
|
|
953
|
-
entries: ConsoleEntry[],
|
|
954
|
-
filters: ConsoleFilters
|
|
955
|
-
): ConsoleEntry[] {
|
|
956
|
-
const normalizedType = filters.type?.toLowerCase();
|
|
957
|
-
const normalizedContains = filters.contains?.toLowerCase();
|
|
958
|
-
|
|
959
|
-
const filtered = entries.filter((entry) => {
|
|
960
|
-
if (
|
|
961
|
-
normalizedType !== undefined &&
|
|
962
|
-
(typeof entry.type !== "string" || entry.type.toLowerCase() !== normalizedType)
|
|
963
|
-
) {
|
|
964
|
-
return false;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
normalizedContains !== undefined &&
|
|
969
|
-
(typeof entry.text !== "string" || !entry.text.toLowerCase().includes(normalizedContains))
|
|
970
|
-
) {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (filters.sinceMs !== undefined) {
|
|
975
|
-
const timestampMs =
|
|
976
|
-
typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
|
|
977
|
-
if (!Number.isFinite(timestampMs) || timestampMs < filters.sinceMs) {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return true;
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
return applyLimitToTail(filtered, filters.limit);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function filterNetworkSummaries(
|
|
989
|
-
summaries: NetworkRequestSummary[],
|
|
990
|
-
filters: NetworkSummaryFilters
|
|
991
|
-
): NetworkRequestSummary[] {
|
|
992
|
-
const normalizedMethod = filters.method?.toUpperCase();
|
|
993
|
-
const filtered = summaries.filter((summary) => {
|
|
994
|
-
if (filters.urlContains !== undefined && !summary.url.includes(filters.urlContains)) {
|
|
995
|
-
return false;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (
|
|
999
|
-
normalizedMethod !== undefined &&
|
|
1000
|
-
(summary.method === undefined || summary.method.toUpperCase() !== normalizedMethod)
|
|
1001
|
-
) {
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (filters.status !== undefined && summary.status !== filters.status) {
|
|
1006
|
-
return false;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (filters.sinceMs !== undefined) {
|
|
1010
|
-
const timestampMs =
|
|
1011
|
-
summary.timestamp === undefined ? Number.NaN : Date.parse(summary.timestamp);
|
|
1012
|
-
if (!Number.isFinite(timestampMs) || timestampMs < filters.sinceMs) {
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
return true;
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
return applyLimitToTail(filtered, filters.limit);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function createHarLog(
|
|
1024
|
-
summaries: NetworkRequestSummary[],
|
|
1025
|
-
includeBodies: boolean,
|
|
1026
|
-
readBody: (requestId: string) => { body: string; encoding: "utf8" | "base64" } | undefined
|
|
1027
|
-
): Record<string, unknown> {
|
|
1028
|
-
const startedDateTime =
|
|
1029
|
-
summaries[0]?.timestamp ?? new Date().toISOString();
|
|
1030
|
-
const entries = summaries.map((summary) => {
|
|
1031
|
-
const bodyPayload = includeBodies ? readBody(summary.requestId) : undefined;
|
|
1032
|
-
const bodyText = bodyPayload?.body;
|
|
1033
|
-
const bodyEncoding = bodyPayload?.encoding;
|
|
1034
|
-
const bodySize = bodyText === undefined ? -1 : bodyText.length;
|
|
1035
|
-
|
|
1036
|
-
return {
|
|
1037
|
-
startedDateTime: summary.timestamp ?? startedDateTime,
|
|
1038
|
-
time: 0,
|
|
1039
|
-
request: {
|
|
1040
|
-
method: summary.method ?? "GET",
|
|
1041
|
-
url: summary.url,
|
|
1042
|
-
httpVersion: "HTTP/1.1",
|
|
1043
|
-
headers: [],
|
|
1044
|
-
queryString: [],
|
|
1045
|
-
cookies: [],
|
|
1046
|
-
headersSize: -1,
|
|
1047
|
-
bodySize: -1
|
|
1048
|
-
},
|
|
1049
|
-
response: {
|
|
1050
|
-
status: summary.status ?? 0,
|
|
1051
|
-
statusText: "",
|
|
1052
|
-
httpVersion: "HTTP/1.1",
|
|
1053
|
-
headers: [],
|
|
1054
|
-
cookies: [],
|
|
1055
|
-
content: {
|
|
1056
|
-
size: bodySize,
|
|
1057
|
-
mimeType: "",
|
|
1058
|
-
...(bodyText !== undefined ? { text: bodyText } : {}),
|
|
1059
|
-
...(bodyEncoding !== undefined ? { encoding: bodyEncoding } : {})
|
|
1060
|
-
},
|
|
1061
|
-
redirectURL: "",
|
|
1062
|
-
headersSize: -1,
|
|
1063
|
-
bodySize
|
|
1064
|
-
},
|
|
1065
|
-
cache: {},
|
|
1066
|
-
timings: {
|
|
1067
|
-
send: 0,
|
|
1068
|
-
wait: 0,
|
|
1069
|
-
receive: 0
|
|
1070
|
-
}
|
|
1071
|
-
};
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
return {
|
|
1075
|
-
log: {
|
|
1076
|
-
version: "1.2",
|
|
1077
|
-
creator: {
|
|
1078
|
-
name: "browserctl",
|
|
1079
|
-
version: "0.5.0"
|
|
1080
|
-
},
|
|
1081
|
-
pages: [
|
|
1082
|
-
{
|
|
1083
|
-
id: "page:1",
|
|
1084
|
-
startedDateTime,
|
|
1085
|
-
title: "browserctl",
|
|
1086
|
-
pageTimings: {}
|
|
1087
|
-
}
|
|
1088
|
-
],
|
|
1089
|
-
entries
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
function readSnapshotUrl(snapshot: unknown): string | undefined {
|
|
1095
|
-
if (!isObjectRecord(snapshot)) {
|
|
1096
|
-
return undefined;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
const value = snapshot.url;
|
|
1100
|
-
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function readSnapshotHtml(snapshot: unknown): string | undefined {
|
|
1104
|
-
if (!isObjectRecord(snapshot)) {
|
|
1105
|
-
return undefined;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const value = snapshot.html;
|
|
1109
|
-
return typeof value === "string" ? value : undefined;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function readElementList(value: unknown): Array<Record<string, unknown>> {
|
|
1113
|
-
if (!Array.isArray(value)) {
|
|
1114
|
-
return [];
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
return value.filter(isObjectRecord);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function delay(ms: number): Promise<void> {
|
|
1121
|
-
return new Promise((resolve) => {
|
|
1122
|
-
setTimeout(resolve, ms);
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function resolveRequiredScope(toolName: string): CapabilityScope {
|
|
1127
|
-
return TOOL_SCOPE_BY_NAME[toolName] ?? "read";
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
function resolveProvidedAuthToken(args: ToolCallArgs): string | undefined {
|
|
1131
|
-
const rawToken = args.authToken;
|
|
1132
|
-
if (typeof rawToken !== "string") {
|
|
1133
|
-
return undefined;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const trimmedToken = rawToken.trim();
|
|
1137
|
-
return trimmedToken.length === 0 ? undefined : trimmedToken;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function authorizeToolCall(
|
|
1141
|
-
args: ToolCallArgs,
|
|
1142
|
-
toolName: string,
|
|
1143
|
-
configuredToken: string | undefined,
|
|
1144
|
-
allowedScopes: ReadonlySet<CapabilityScope>
|
|
1145
|
-
): ToolResponse<null> {
|
|
1146
|
-
if (configuredToken !== undefined) {
|
|
1147
|
-
const providedToken = resolveProvidedAuthToken(args);
|
|
1148
|
-
if (providedToken === undefined || providedToken !== configuredToken) {
|
|
1149
|
-
return createErr(ErrorCode.E_PERMISSION, "Invalid auth token.");
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
const requiredScope = resolveRequiredScope(toolName);
|
|
1154
|
-
if (!allowedScopes.has(requiredScope)) {
|
|
1155
|
-
return createErr(
|
|
1156
|
-
ErrorCode.E_PERMISSION,
|
|
1157
|
-
`Tool scope is not allowed: ${toolName} requires ${requiredScope}.`
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
return createOk(null);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function resolveSessionTenant(sessionId: string): string | undefined {
|
|
1165
|
-
const delimiterIndex = sessionId.indexOf(":");
|
|
1166
|
-
if (delimiterIndex <= 0) {
|
|
1167
|
-
return undefined;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const tenant = sessionId.slice(0, delimiterIndex).trim();
|
|
1171
|
-
return tenant.length === 0 ? undefined : tenant.toLowerCase();
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function authorizeSessionDrop(
|
|
1175
|
-
requesterSessionId: string,
|
|
1176
|
-
sessionIdToDelete: string
|
|
1177
|
-
): ToolResponse<null> {
|
|
1178
|
-
if (requesterSessionId === sessionIdToDelete) {
|
|
1179
|
-
return createOk(null);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const requesterTenant = resolveSessionTenant(requesterSessionId);
|
|
1183
|
-
const targetTenant = resolveSessionTenant(sessionIdToDelete);
|
|
1184
|
-
if (
|
|
1185
|
-
requesterTenant === undefined ||
|
|
1186
|
-
targetTenant === undefined ||
|
|
1187
|
-
requesterTenant !== targetTenant
|
|
1188
|
-
) {
|
|
1189
|
-
return createErr(
|
|
1190
|
-
ErrorCode.E_PERMISSION,
|
|
1191
|
-
"browser.session.drop can only delete sessions in the same tenant."
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
return createOk(null);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function parseHostnameFromUrl(rawUrl: string): ToolResponse<string> {
|
|
1199
|
-
const trimmedUrl = rawUrl.trim();
|
|
1200
|
-
if (trimmedUrl.length === 0) {
|
|
1201
|
-
return createErr(ErrorCode.E_INVALID_ARG, "URL must be a non-empty string.");
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
let parsedUrl: URL;
|
|
1205
|
-
try {
|
|
1206
|
-
parsedUrl = new URL(trimmedUrl);
|
|
1207
|
-
} catch {
|
|
1208
|
-
return createErr(ErrorCode.E_INVALID_ARG, `Invalid URL: ${trimmedUrl}`);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (parsedUrl.hostname.trim().length === 0) {
|
|
1212
|
-
return createErr(ErrorCode.E_INVALID_ARG, `URL hostname is missing: ${trimmedUrl}`);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
return createOk(parsedUrl.hostname.toLowerCase());
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
function isHostnameAllowed(hostname: string, allowlistEntry: string): boolean {
|
|
1219
|
-
if (allowlistEntry.startsWith("*.")) {
|
|
1220
|
-
const suffix = allowlistEntry.slice(2);
|
|
1221
|
-
if (suffix.length === 0) {
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
return hostname === allowlistEntry;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function resolveRestrictedUrlFromToolArgs(
|
|
1232
|
-
toolName: string,
|
|
1233
|
-
args: ToolCallArgs
|
|
1234
|
-
): ToolResponse<string | undefined> {
|
|
1235
|
-
if (toolName === "browser.tab.open") {
|
|
1236
|
-
const url = args.url;
|
|
1237
|
-
if (typeof url !== "string" || url.trim().length === 0) {
|
|
1238
|
-
return createErr(ErrorCode.E_INVALID_ARG, "url is required and must be a non-empty string.");
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
return createOk(url.trim());
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (toolName === "browser.cookie.set") {
|
|
1245
|
-
const url = args.url;
|
|
1246
|
-
if (url === undefined) {
|
|
1247
|
-
return createOk(undefined);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
if (typeof url !== "string" || url.trim().length === 0) {
|
|
1251
|
-
return createErr(
|
|
1252
|
-
ErrorCode.E_INVALID_ARG,
|
|
1253
|
-
"url must be a non-empty string when provided."
|
|
1254
|
-
);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
return createOk(url.trim());
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
if (toolName === "browser.act") {
|
|
1261
|
-
const action = args.action;
|
|
1262
|
-
if (!isObjectRecord(action)) {
|
|
1263
|
-
return createOk(undefined);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const actionType = typeof action.type === "string" ? action.type.trim().toLowerCase() : "";
|
|
1267
|
-
if (actionType !== "navigate" && actionType !== "goto") {
|
|
1268
|
-
return createOk(undefined);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
const payload = action.payload;
|
|
1272
|
-
if (!isObjectRecord(payload)) {
|
|
1273
|
-
return createErr(ErrorCode.E_INVALID_ARG, "action.payload.url is required for navigate action.");
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const url = payload.url;
|
|
1277
|
-
if (typeof url !== "string" || url.trim().length === 0) {
|
|
1278
|
-
return createErr(ErrorCode.E_INVALID_ARG, "action.payload.url is required for navigate action.");
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
return createOk(url.trim());
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
return createOk(undefined);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function authorizeDomainAllowlist(
|
|
1288
|
-
toolName: string,
|
|
1289
|
-
args: ToolCallArgs,
|
|
1290
|
-
config: BrowserdConfig
|
|
1291
|
-
): ToolResponse<null> {
|
|
1292
|
-
if (config.domainAllowlistMode !== "enforce") {
|
|
1293
|
-
return createOk(null);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
if (config.domainAllowlist.length === 0) {
|
|
1297
|
-
return createErr(
|
|
1298
|
-
ErrorCode.E_PERMISSION,
|
|
1299
|
-
"Domain allowlist mode is enforce but BROWSERD_DOMAIN_ALLOWLIST is empty."
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const restrictedUrl = resolveRestrictedUrlFromToolArgs(toolName, args);
|
|
1304
|
-
if (!restrictedUrl.ok) {
|
|
1305
|
-
return restrictedUrl;
|
|
1306
|
-
}
|
|
1307
|
-
if (restrictedUrl.data === undefined) {
|
|
1308
|
-
return createOk(null);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const hostname = parseHostnameFromUrl(restrictedUrl.data);
|
|
1312
|
-
if (!hostname.ok) {
|
|
1313
|
-
return hostname;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const matched = config.domainAllowlist.some((entry) => isHostnameAllowed(hostname.data, entry));
|
|
1317
|
-
if (!matched) {
|
|
1318
|
-
return createErr(
|
|
1319
|
-
ErrorCode.E_PERMISSION,
|
|
1320
|
-
`URL host is outside domain allowlist: ${hostname.data}`
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
return createOk(null);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
function authorizeSessionGovernance(
|
|
1328
|
-
args: ToolCallArgs,
|
|
1329
|
-
sessions: SessionStore,
|
|
1330
|
-
config: BrowserdConfig
|
|
1331
|
-
): ToolResponse<null> {
|
|
1332
|
-
const sessionId = args.sessionId;
|
|
1333
|
-
const tenant = resolveSessionTenant(sessionId);
|
|
1334
|
-
if (config.sessionRequireTenantPrefix && tenant === undefined) {
|
|
1335
|
-
return createErr(
|
|
1336
|
-
ErrorCode.E_PERMISSION,
|
|
1337
|
-
"Session governance requires tenant-prefixed sessionId format: <tenant>:<session>."
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
config.tenantAllowlist.length > 0 &&
|
|
1343
|
-
(tenant === undefined || !config.tenantAllowlist.includes(tenant))
|
|
1344
|
-
) {
|
|
1345
|
-
return createErr(
|
|
1346
|
-
ErrorCode.E_PERMISSION,
|
|
1347
|
-
`Tenant is not allowed for sessionId: ${sessionId}`
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
if (sessions.has(sessionId)) {
|
|
1352
|
-
return createOk(null);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
const activeSessions = sessions.listSnapshots();
|
|
1356
|
-
if (activeSessions.length >= config.sessionMaxTotal) {
|
|
1357
|
-
return createErr(
|
|
1358
|
-
ErrorCode.E_CONFLICT,
|
|
1359
|
-
`Active session limit exceeded: ${config.sessionMaxTotal}.`
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
if (tenant !== undefined) {
|
|
1364
|
-
const tenantSessionCount = activeSessions.reduce((count, snapshot) => {
|
|
1365
|
-
const snapshotTenant = resolveSessionTenant(snapshot.state.sessionId);
|
|
1366
|
-
return snapshotTenant === tenant ? count + 1 : count;
|
|
1367
|
-
}, 0);
|
|
1368
|
-
if (tenantSessionCount >= config.sessionMaxPerTenant) {
|
|
1369
|
-
return createErr(
|
|
1370
|
-
ErrorCode.E_CONFLICT,
|
|
1371
|
-
`Active session limit exceeded for tenant ${tenant}: ${config.sessionMaxPerTenant}.`
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
return createOk(null);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
function isWindowsPathStyle(pathValue: string): boolean {
|
|
1380
|
-
return (
|
|
1381
|
-
/^[a-zA-Z]:[\\/]/.test(pathValue) ||
|
|
1382
|
-
pathValue.startsWith("\\\\") ||
|
|
1383
|
-
pathValue.includes("\\")
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
function resolvePathWithinRoot(
|
|
1388
|
-
inputPath: string,
|
|
1389
|
-
rootPath: string,
|
|
1390
|
-
context: "upload" | "download"
|
|
1391
|
-
): ToolResponse<string> {
|
|
1392
|
-
const useWindowsSemantics = isWindowsPathStyle(rootPath);
|
|
1393
|
-
const pathRuntime = useWindowsSemantics ? windowsPath : posixPath;
|
|
1394
|
-
const resolvedRoot = pathRuntime.resolve(rootPath);
|
|
1395
|
-
const resolvedPath = pathRuntime.isAbsolute(inputPath)
|
|
1396
|
-
? pathRuntime.resolve(inputPath)
|
|
1397
|
-
: pathRuntime.resolve(resolvedRoot, inputPath);
|
|
1398
|
-
|
|
1399
|
-
const comparableRoot = useWindowsSemantics ? resolvedRoot.toLowerCase() : resolvedRoot;
|
|
1400
|
-
const comparablePath = useWindowsSemantics ? resolvedPath.toLowerCase() : resolvedPath;
|
|
1401
|
-
const relativePath = pathRuntime.relative(comparableRoot, comparablePath);
|
|
1402
|
-
const isInRoot =
|
|
1403
|
-
relativePath === "" ||
|
|
1404
|
-
(!relativePath.startsWith("..") && !pathRuntime.isAbsolute(relativePath));
|
|
1405
|
-
if (!isInRoot) {
|
|
1406
|
-
return createErr(
|
|
1407
|
-
ErrorCode.E_PERMISSION,
|
|
1408
|
-
`${context} path is outside configured allowlist root: ${inputPath}`
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
return createOk(resolvedPath);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function applyUploadRoot(files: string[], uploadRoot: string | undefined): ToolResponse<string[]> {
|
|
1416
|
-
if (uploadRoot === undefined) {
|
|
1417
|
-
return createErr(
|
|
1418
|
-
ErrorCode.E_PERMISSION,
|
|
1419
|
-
"Upload root is not configured. Set BROWSERD_UPLOAD_ROOT to enable browser.upload.arm."
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const rootedFiles: string[] = [];
|
|
1424
|
-
for (const file of files) {
|
|
1425
|
-
const rootedPath = resolvePathWithinRoot(file, uploadRoot, "upload");
|
|
1426
|
-
if (!rootedPath.ok) {
|
|
1427
|
-
return rootedPath;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
rootedFiles.push(rootedPath.data);
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
return createOk(rootedFiles);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function normalizeDownloadPayload(
|
|
1437
|
-
rawDownload: unknown,
|
|
1438
|
-
requestedPath: string | undefined,
|
|
1439
|
-
downloadRoot: string | undefined
|
|
1440
|
-
): ToolResponse<Record<string, unknown>> {
|
|
1441
|
-
if (downloadRoot === undefined) {
|
|
1442
|
-
return createErr(
|
|
1443
|
-
ErrorCode.E_PERMISSION,
|
|
1444
|
-
"Download root is not configured. Set BROWSERD_DOWNLOAD_ROOT to enable browser.download.wait."
|
|
1445
|
-
);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
if (!isObjectRecord(rawDownload)) {
|
|
1449
|
-
return createErr(ErrorCode.E_INTERNAL, "Invalid driver download payload.");
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
const download = { ...rawDownload };
|
|
1453
|
-
const actualPath =
|
|
1454
|
-
typeof download.path === "string" && download.path.trim().length > 0
|
|
1455
|
-
? download.path.trim()
|
|
1456
|
-
: undefined;
|
|
1457
|
-
if (actualPath === undefined) {
|
|
1458
|
-
return createErr(
|
|
1459
|
-
ErrorCode.E_INTERNAL,
|
|
1460
|
-
"Driver download payload is missing path required for allowlist enforcement."
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const rootedActualPath = resolvePathWithinRoot(actualPath, downloadRoot, "download");
|
|
1465
|
-
if (!rootedActualPath.ok) {
|
|
1466
|
-
return rootedActualPath;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (requestedPath !== undefined) {
|
|
1470
|
-
const rootedRequestedPath = resolvePathWithinRoot(requestedPath, downloadRoot, "download");
|
|
1471
|
-
if (!rootedRequestedPath.ok) {
|
|
1472
|
-
return rootedRequestedPath;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
const useWindowsSemantics = isWindowsPathStyle(downloadRoot);
|
|
1476
|
-
const comparableActualPath = useWindowsSemantics
|
|
1477
|
-
? rootedActualPath.data.toLowerCase()
|
|
1478
|
-
: rootedActualPath.data;
|
|
1479
|
-
const comparableRequestedPath = useWindowsSemantics
|
|
1480
|
-
? rootedRequestedPath.data.toLowerCase()
|
|
1481
|
-
: rootedRequestedPath.data;
|
|
1482
|
-
if (comparableActualPath !== comparableRequestedPath) {
|
|
1483
|
-
return createErr(
|
|
1484
|
-
ErrorCode.E_INTERNAL,
|
|
1485
|
-
`Driver failed to persist download to requested path: ${rootedRequestedPath.data}`
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
return createOk({
|
|
1491
|
-
...download,
|
|
1492
|
-
path: rootedActualPath.data
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function createBrowserdToolMap(
|
|
1497
|
-
drivers: Map<string, BrowserDriver>,
|
|
1498
|
-
sessions: SessionStore,
|
|
1499
|
-
defaultDriverKey: string,
|
|
1500
|
-
config: BrowserdConfig
|
|
1501
|
-
): ToolMap {
|
|
1502
|
-
type TraceStep = {
|
|
1503
|
-
index: number;
|
|
1504
|
-
timestamp: string;
|
|
1505
|
-
tool: string;
|
|
1506
|
-
ok: boolean;
|
|
1507
|
-
durationMs: number;
|
|
1508
|
-
targetId?: string;
|
|
1509
|
-
profile?: string;
|
|
1510
|
-
errorCode?: string;
|
|
1511
|
-
};
|
|
1512
|
-
type TraceKeyResponse = {
|
|
1513
|
-
stepIndex: number;
|
|
1514
|
-
kind: string;
|
|
1515
|
-
timestamp: string;
|
|
1516
|
-
data: Record<string, unknown>;
|
|
1517
|
-
};
|
|
1518
|
-
type TraceScreenshot = {
|
|
1519
|
-
stepIndex: number;
|
|
1520
|
-
timestamp: string;
|
|
1521
|
-
tool: string;
|
|
1522
|
-
targetId?: string;
|
|
1523
|
-
};
|
|
1524
|
-
type SessionTraceState = {
|
|
1525
|
-
nextStepIndex: number;
|
|
1526
|
-
steps: TraceStep[];
|
|
1527
|
-
keyResponses: TraceKeyResponse[];
|
|
1528
|
-
screenshots: TraceScreenshot[];
|
|
1529
|
-
};
|
|
1530
|
-
const traceStateBySession = new Map<string, SessionTraceState>();
|
|
1531
|
-
const TRACE_EVENT_LIMIT = 500;
|
|
1532
|
-
|
|
1533
|
-
const getOrCreateSessionTraceState = (sessionId: string): SessionTraceState => {
|
|
1534
|
-
const existing = traceStateBySession.get(sessionId);
|
|
1535
|
-
if (existing !== undefined) {
|
|
1536
|
-
return existing;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
const created: SessionTraceState = {
|
|
1540
|
-
nextStepIndex: 1,
|
|
1541
|
-
steps: [],
|
|
1542
|
-
keyResponses: [],
|
|
1543
|
-
screenshots: []
|
|
1544
|
-
};
|
|
1545
|
-
traceStateBySession.set(sessionId, created);
|
|
1546
|
-
return created;
|
|
1547
|
-
};
|
|
1548
|
-
|
|
1549
|
-
const trimTraceState = (state: SessionTraceState): void => {
|
|
1550
|
-
if (state.steps.length > TRACE_EVENT_LIMIT) {
|
|
1551
|
-
state.steps.splice(0, state.steps.length - TRACE_EVENT_LIMIT);
|
|
1552
|
-
}
|
|
1553
|
-
if (state.keyResponses.length > TRACE_EVENT_LIMIT) {
|
|
1554
|
-
state.keyResponses.splice(0, state.keyResponses.length - TRACE_EVENT_LIMIT);
|
|
1555
|
-
}
|
|
1556
|
-
if (state.screenshots.length > TRACE_EVENT_LIMIT) {
|
|
1557
|
-
state.screenshots.splice(0, state.screenshots.length - TRACE_EVENT_LIMIT);
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
|
|
1561
|
-
const recordTrace = (
|
|
1562
|
-
sessionId: string,
|
|
1563
|
-
toolName: string,
|
|
1564
|
-
args: ToolCallArgs,
|
|
1565
|
-
response: ToolResponse<Record<string, unknown>> | ToolResponse<unknown>,
|
|
1566
|
-
startedAtMs: number,
|
|
1567
|
-
finishedAtMs: number
|
|
1568
|
-
): void => {
|
|
1569
|
-
const state = getOrCreateSessionTraceState(sessionId);
|
|
1570
|
-
const stepIndex = state.nextStepIndex;
|
|
1571
|
-
state.nextStepIndex += 1;
|
|
1572
|
-
const timestamp = new Date(startedAtMs).toISOString();
|
|
1573
|
-
const targetId = typeof args.targetId === "string" ? args.targetId : undefined;
|
|
1574
|
-
const profile = typeof args.profile === "string" ? args.profile : undefined;
|
|
1575
|
-
const errorCode =
|
|
1576
|
-
!response.ok && isObjectRecord(response.error) && typeof response.error.code === "string"
|
|
1577
|
-
? response.error.code
|
|
1578
|
-
: undefined;
|
|
1579
|
-
|
|
1580
|
-
state.steps.push({
|
|
1581
|
-
index: stepIndex,
|
|
1582
|
-
timestamp,
|
|
1583
|
-
tool: toolName,
|
|
1584
|
-
ok: response.ok,
|
|
1585
|
-
durationMs: Math.max(0, finishedAtMs - startedAtMs),
|
|
1586
|
-
...(targetId !== undefined ? { targetId } : {}),
|
|
1587
|
-
...(profile !== undefined ? { profile } : {}),
|
|
1588
|
-
...(errorCode !== undefined ? { errorCode } : {})
|
|
1589
|
-
});
|
|
1590
|
-
|
|
1591
|
-
if (
|
|
1592
|
-
response.ok &&
|
|
1593
|
-
(toolName === "browser.screenshot" || toolName === "browser.element.screenshot")
|
|
1594
|
-
) {
|
|
1595
|
-
state.screenshots.push({
|
|
1596
|
-
stepIndex,
|
|
1597
|
-
timestamp,
|
|
1598
|
-
tool: toolName,
|
|
1599
|
-
...(targetId !== undefined ? { targetId } : {})
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
if (response.ok && isObjectRecord(response.data)) {
|
|
1604
|
-
if (toolName === "browser.network.waitFor" && isObjectRecord(response.data.request)) {
|
|
1605
|
-
state.keyResponses.push({
|
|
1606
|
-
stepIndex,
|
|
1607
|
-
kind: "network.waitFor",
|
|
1608
|
-
timestamp,
|
|
1609
|
-
data: {
|
|
1610
|
-
...response.data.request
|
|
1611
|
-
}
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (toolName === "browser.network.responseBody") {
|
|
1616
|
-
const requestId = response.data.requestId;
|
|
1617
|
-
const encoding = response.data.encoding;
|
|
1618
|
-
const body = response.data.body;
|
|
1619
|
-
state.keyResponses.push({
|
|
1620
|
-
stepIndex,
|
|
1621
|
-
kind: "network.responseBody",
|
|
1622
|
-
timestamp,
|
|
1623
|
-
data: {
|
|
1624
|
-
...(typeof requestId === "string" ? { requestId } : {}),
|
|
1625
|
-
...(typeof encoding === "string" ? { encoding } : {}),
|
|
1626
|
-
...(typeof body === "string" ? { size: body.length } : {})
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
if (toolName === "browser.network.list") {
|
|
1632
|
-
const requests = response.data.requests;
|
|
1633
|
-
state.keyResponses.push({
|
|
1634
|
-
stepIndex,
|
|
1635
|
-
kind: "network.list",
|
|
1636
|
-
timestamp,
|
|
1637
|
-
data: {
|
|
1638
|
-
count: Array.isArray(requests) ? requests.length : 0
|
|
1639
|
-
}
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (toolName === "browser.network.harExport") {
|
|
1644
|
-
const har = response.data.har;
|
|
1645
|
-
const entriesCount =
|
|
1646
|
-
isObjectRecord(har) &&
|
|
1647
|
-
isObjectRecord(har.log) &&
|
|
1648
|
-
Array.isArray(har.log.entries)
|
|
1649
|
-
? har.log.entries.length
|
|
1650
|
-
: 0;
|
|
1651
|
-
state.keyResponses.push({
|
|
1652
|
-
stepIndex,
|
|
1653
|
-
kind: "network.harExport",
|
|
1654
|
-
timestamp,
|
|
1655
|
-
data: {
|
|
1656
|
-
entries: entriesCount
|
|
1657
|
-
}
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
if (toolName === "browser.memory.resolve") {
|
|
1662
|
-
state.keyResponses.push({
|
|
1663
|
-
stepIndex,
|
|
1664
|
-
kind: "memory.resolve",
|
|
1665
|
-
timestamp,
|
|
1666
|
-
data: {
|
|
1667
|
-
memoryHit: response.data.hit === true
|
|
1668
|
-
}
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
trimTraceState(state);
|
|
1674
|
-
};
|
|
1675
|
-
|
|
1676
|
-
const toolMap = buildToolMap();
|
|
1677
|
-
const navigationMemoryStore = createNavigationMemoryStore({
|
|
1678
|
-
path: config.memoryPath,
|
|
1679
|
-
mode: config.memoryEnabled ? config.memoryMode : "off",
|
|
1680
|
-
ttlDays: config.memoryTtlDays
|
|
1681
|
-
});
|
|
1682
|
-
const allowedScopes = new Set<CapabilityScope>(config.authScopes);
|
|
1683
|
-
const runWithDefaultDriver = <TData>(
|
|
1684
|
-
args: ToolCallArgs,
|
|
1685
|
-
operation: (
|
|
1686
|
-
driver: BrowserDriver,
|
|
1687
|
-
driverKey: string
|
|
1688
|
-
) => Promise<ToolResponse<TData>> | ToolResponse<TData>
|
|
1689
|
-
): Promise<ToolResponse<TData>> =>
|
|
1690
|
-
runWithDriver(args, drivers, sessions, defaultDriverKey, operation);
|
|
1691
|
-
|
|
1692
|
-
const runStructuredAction = async (
|
|
1693
|
-
args: ToolCallArgs,
|
|
1694
|
-
actionType: string,
|
|
1695
|
-
payload: Record<string, unknown>
|
|
1696
|
-
): Promise<
|
|
1697
|
-
ToolResponse<{
|
|
1698
|
-
driver: string;
|
|
1699
|
-
targetId: string;
|
|
1700
|
-
data: Record<string, unknown>;
|
|
1701
|
-
}>
|
|
1702
|
-
> =>
|
|
1703
|
-
await runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1704
|
-
const targetId = resolveTargetId(args, sessions);
|
|
1705
|
-
if (!targetId.ok) {
|
|
1706
|
-
return targetId;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
const actionResult = await driver.act(
|
|
1710
|
-
{
|
|
1711
|
-
type: actionType,
|
|
1712
|
-
payload
|
|
1713
|
-
},
|
|
1714
|
-
targetId.data
|
|
1715
|
-
);
|
|
1716
|
-
const resolvedData = resolveDriverActionData(actionResult, actionType);
|
|
1717
|
-
if (!resolvedData.ok) {
|
|
1718
|
-
return resolvedData;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
return createOk({
|
|
1722
|
-
driver: driverKey,
|
|
1723
|
-
targetId: targetId.data,
|
|
1724
|
-
data: resolvedData.data
|
|
1725
|
-
});
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
toolMap.set("browser.status", async (args) => {
|
|
1729
|
-
const resolvedDriver = resolveDriver(args, drivers, sessions, defaultDriverKey);
|
|
1730
|
-
if (!resolvedDriver.ok) {
|
|
1731
|
-
return resolvedDriver;
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
try {
|
|
1735
|
-
const status = await resolvedDriver.data.driver.status();
|
|
1736
|
-
return createOk({
|
|
1737
|
-
kind: "browserd",
|
|
1738
|
-
ready: true,
|
|
1739
|
-
driver: resolvedDriver.data.driverKey,
|
|
1740
|
-
drivers: [...drivers.keys()].sort(),
|
|
1741
|
-
status
|
|
1742
|
-
});
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
return mapDriverError(error);
|
|
1745
|
-
}
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
toolMap.set("browser.profile.use", async (args) => {
|
|
1749
|
-
const requestedProfile = requireStringArg(args, "profile");
|
|
1750
|
-
if (!requestedProfile.ok) {
|
|
1751
|
-
return requestedProfile;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
if (!drivers.has(requestedProfile.data)) {
|
|
1755
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown driver profile: ${requestedProfile.data}`);
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
sessions.useProfile(args.sessionId, requestedProfile.data);
|
|
1759
|
-
return createOk({
|
|
1760
|
-
profile: requestedProfile.data
|
|
1761
|
-
});
|
|
1762
|
-
});
|
|
1763
|
-
|
|
1764
|
-
toolMap.set("browser.profile.list", async (args) =>
|
|
1765
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1766
|
-
const profiles = await driver.listProfiles();
|
|
1767
|
-
return createOk({
|
|
1768
|
-
driver: driverKey,
|
|
1769
|
-
profiles
|
|
1770
|
-
});
|
|
1771
|
-
})
|
|
1772
|
-
);
|
|
1773
|
-
|
|
1774
|
-
toolMap.set("browser.session.list", async (args) => {
|
|
1775
|
-
const tenant = resolveOptionalStringArg(args, "tenant");
|
|
1776
|
-
if (!tenant.ok) {
|
|
1777
|
-
return tenant;
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
const limit = resolveOptionalIntegerArg(args, "limit", 1);
|
|
1781
|
-
if (!limit.ok) {
|
|
1782
|
-
return limit;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const normalizedTenant = tenant.data?.toLowerCase();
|
|
1786
|
-
const sessionsPayload = sessions
|
|
1787
|
-
.listSnapshots()
|
|
1788
|
-
.map((snapshot) => ({
|
|
1789
|
-
sessionId: snapshot.state.sessionId,
|
|
1790
|
-
tenant: resolveSessionTenant(snapshot.state.sessionId),
|
|
1791
|
-
profile: snapshot.state.profile,
|
|
1792
|
-
targetId: snapshot.state.targetId,
|
|
1793
|
-
touchedAt: new Date(snapshot.touchedAt).toISOString()
|
|
1794
|
-
}))
|
|
1795
|
-
.filter((entry) =>
|
|
1796
|
-
normalizedTenant === undefined ? true : entry.tenant === normalizedTenant
|
|
1797
|
-
)
|
|
1798
|
-
.sort((left, right) => right.touchedAt.localeCompare(left.touchedAt));
|
|
1799
|
-
|
|
1800
|
-
const limitedSessions =
|
|
1801
|
-
limit.data === undefined ? sessionsPayload : sessionsPayload.slice(0, limit.data);
|
|
1802
|
-
return createOk({
|
|
1803
|
-
sessions: limitedSessions,
|
|
1804
|
-
totalActive: sessions.size()
|
|
1805
|
-
});
|
|
1806
|
-
});
|
|
1807
|
-
|
|
1808
|
-
toolMap.set("browser.session.drop", async (args) => {
|
|
1809
|
-
const sessionIdToDelete = requireStringArg(args, "sessionIdToDelete");
|
|
1810
|
-
if (!sessionIdToDelete.ok) {
|
|
1811
|
-
return sessionIdToDelete;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
const dropAuthorization = authorizeSessionDrop(args.sessionId, sessionIdToDelete.data);
|
|
1815
|
-
if (!dropAuthorization.ok) {
|
|
1816
|
-
return dropAuthorization;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
return createOk({
|
|
1820
|
-
sessionIdToDelete: sessionIdToDelete.data,
|
|
1821
|
-
dropped: sessions.delete(sessionIdToDelete.data)
|
|
1822
|
-
});
|
|
1823
|
-
});
|
|
1824
|
-
|
|
1825
|
-
toolMap.set("browser.memory.status", async () => createOk(navigationMemoryStore.status()));
|
|
1826
|
-
|
|
1827
|
-
toolMap.set("browser.memory.resolve", async (args) => {
|
|
1828
|
-
const scope = resolveNavigationMemoryScope(args);
|
|
1829
|
-
if (!scope.ok) {
|
|
1830
|
-
return scope;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
try {
|
|
1834
|
-
return createOk(await navigationMemoryStore.resolve(scope.data));
|
|
1835
|
-
} catch (error) {
|
|
1836
|
-
return mapNavigationMemoryError(error);
|
|
1837
|
-
}
|
|
1838
|
-
});
|
|
1839
|
-
|
|
1840
|
-
toolMap.set("browser.memory.upsert", async (args) => {
|
|
1841
|
-
const scope = resolveNavigationMemoryScope(args);
|
|
1842
|
-
if (!scope.ok) {
|
|
1843
|
-
return scope;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
const signals = resolveNavigationSignals(args);
|
|
1847
|
-
if (!signals.ok) {
|
|
1848
|
-
return signals;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
const confidence = resolveOptionalNumberArg(args, "confidence");
|
|
1852
|
-
if (!confidence.ok) {
|
|
1853
|
-
return confidence;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
const confirmed = args.confirmed;
|
|
1857
|
-
if (confirmed !== undefined && typeof confirmed !== "boolean") {
|
|
1858
|
-
return createErr(ErrorCode.E_INVALID_ARG, "confirmed must be a boolean when provided.");
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
try {
|
|
1862
|
-
return createOk(
|
|
1863
|
-
await navigationMemoryStore.upsert({
|
|
1864
|
-
...scope.data,
|
|
1865
|
-
signals: signals.data,
|
|
1866
|
-
...(confidence.data !== undefined ? { confidence: confidence.data } : {}),
|
|
1867
|
-
...(confirmed !== undefined ? { confirmed } : {})
|
|
1868
|
-
})
|
|
1869
|
-
);
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
return mapNavigationMemoryError(error);
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
toolMap.set("browser.memory.list", async (args) => {
|
|
1876
|
-
const domain = resolveOptionalStringArg(args, "domain");
|
|
1877
|
-
if (!domain.ok) {
|
|
1878
|
-
return domain;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
const profileId = resolveOptionalStringArg(args, "profileId");
|
|
1882
|
-
if (!profileId.ok) {
|
|
1883
|
-
return profileId;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
const intentKey = resolveOptionalStringArg(args, "intentKey");
|
|
1887
|
-
if (!intentKey.ok) {
|
|
1888
|
-
return intentKey;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
try {
|
|
1892
|
-
const entries = await navigationMemoryStore.list({
|
|
1893
|
-
...(domain.data !== undefined ? { domain: domain.data } : {}),
|
|
1894
|
-
...(profileId.data !== undefined ? { profileId: profileId.data } : {}),
|
|
1895
|
-
...(intentKey.data !== undefined ? { intentKey: intentKey.data } : {})
|
|
1896
|
-
});
|
|
1897
|
-
return createOk({ entries });
|
|
1898
|
-
} catch (error) {
|
|
1899
|
-
return mapNavigationMemoryError(error);
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
toolMap.set("browser.memory.inspect", async (args) => {
|
|
1904
|
-
const id = requireStringArg(args, "id");
|
|
1905
|
-
if (!id.ok) {
|
|
1906
|
-
return id;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
try {
|
|
1910
|
-
return createOk({
|
|
1911
|
-
entry: await navigationMemoryStore.inspect(id.data)
|
|
1912
|
-
});
|
|
1913
|
-
} catch (error) {
|
|
1914
|
-
return mapNavigationMemoryError(error);
|
|
1915
|
-
}
|
|
1916
|
-
});
|
|
1917
|
-
|
|
1918
|
-
toolMap.set("browser.memory.delete", async (args) => {
|
|
1919
|
-
const id = requireStringArg(args, "id");
|
|
1920
|
-
if (!id.ok) {
|
|
1921
|
-
return id;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
try {
|
|
1925
|
-
return createOk({
|
|
1926
|
-
deleted: await navigationMemoryStore.delete(id.data)
|
|
1927
|
-
});
|
|
1928
|
-
} catch (error) {
|
|
1929
|
-
return mapNavigationMemoryError(error);
|
|
1930
|
-
}
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
toolMap.set("browser.memory.purge", async () => {
|
|
1934
|
-
try {
|
|
1935
|
-
return createOk({
|
|
1936
|
-
purged: await navigationMemoryStore.purgeExpired()
|
|
1937
|
-
});
|
|
1938
|
-
} catch (error) {
|
|
1939
|
-
return mapNavigationMemoryError(error);
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
|
-
toolMap.set("browser.memory.mode.set", async (args) => {
|
|
1944
|
-
if (!config.memoryEnabled) {
|
|
1945
|
-
return createErr(
|
|
1946
|
-
ErrorCode.E_PERMISSION,
|
|
1947
|
-
"Navigation memory is disabled by BROWSERD_MEMORY_ENABLED=false."
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
const mode = resolveNavigationMemoryMode(args);
|
|
1952
|
-
if (!mode.ok) {
|
|
1953
|
-
return mode;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
try {
|
|
1957
|
-
return createOk(await navigationMemoryStore.setMode(mode.data));
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
return mapNavigationMemoryError(error);
|
|
1960
|
-
}
|
|
1961
|
-
});
|
|
1962
|
-
|
|
1963
|
-
toolMap.set("browser.memory.ttl.set", async (args) => {
|
|
1964
|
-
const ttlDays = resolveOptionalIntegerArg(args, "ttlDays", 0);
|
|
1965
|
-
if (!ttlDays.ok) {
|
|
1966
|
-
return ttlDays;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
if (ttlDays.data === undefined) {
|
|
1970
|
-
return createErr(ErrorCode.E_INVALID_ARG, "ttlDays is required and must be an integer greater than or equal to 0.");
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
try {
|
|
1974
|
-
return createOk(await navigationMemoryStore.setTtlDays(ttlDays.data));
|
|
1975
|
-
} catch (error) {
|
|
1976
|
-
return mapNavigationMemoryError(error);
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
toolMap.set("browser.tab.list", async (args) =>
|
|
1981
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1982
|
-
const tabs = await driver.listTabs();
|
|
1983
|
-
return createOk({
|
|
1984
|
-
driver: driverKey,
|
|
1985
|
-
tabs
|
|
1986
|
-
});
|
|
1987
|
-
})
|
|
1988
|
-
);
|
|
1989
|
-
|
|
1990
|
-
toolMap.set("browser.tab.open", async (args) =>
|
|
1991
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1992
|
-
const url = requireStringArg(args, "url");
|
|
1993
|
-
if (!url.ok) {
|
|
1994
|
-
return url;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
const targetId = await driver.openTab(url.data);
|
|
1998
|
-
sessions.useTarget(args.sessionId, targetId);
|
|
1999
|
-
return createOk({
|
|
2000
|
-
driver: driverKey,
|
|
2001
|
-
targetId
|
|
2002
|
-
});
|
|
2003
|
-
})
|
|
2004
|
-
);
|
|
2005
|
-
|
|
2006
|
-
toolMap.set("browser.tab.focus", async (args) =>
|
|
2007
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2008
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2009
|
-
if (!targetId.ok) {
|
|
2010
|
-
return targetId;
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
await driver.focusTab(targetId.data);
|
|
2014
|
-
sessions.useTarget(args.sessionId, targetId.data);
|
|
2015
|
-
return createOk({
|
|
2016
|
-
driver: driverKey,
|
|
2017
|
-
targetId: targetId.data,
|
|
2018
|
-
focused: true
|
|
2019
|
-
});
|
|
2020
|
-
})
|
|
2021
|
-
);
|
|
2022
|
-
|
|
2023
|
-
toolMap.set("browser.tab.close", async (args) =>
|
|
2024
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2025
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2026
|
-
if (!targetId.ok) {
|
|
2027
|
-
return targetId;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
await driver.closeTab(targetId.data);
|
|
2031
|
-
return createOk({
|
|
2032
|
-
driver: driverKey,
|
|
2033
|
-
targetId: targetId.data,
|
|
2034
|
-
closed: true
|
|
2035
|
-
});
|
|
2036
|
-
})
|
|
2037
|
-
);
|
|
2038
|
-
|
|
2039
|
-
toolMap.set("browser.snapshot", async (args) =>
|
|
2040
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2041
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2042
|
-
if (!targetId.ok) {
|
|
2043
|
-
return targetId;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2047
|
-
return createOk({
|
|
2048
|
-
driver: driverKey,
|
|
2049
|
-
targetId: targetId.data,
|
|
2050
|
-
snapshot
|
|
2051
|
-
});
|
|
2052
|
-
})
|
|
2053
|
-
);
|
|
2054
|
-
|
|
2055
|
-
toolMap.set("browser.screenshot", async (args) =>
|
|
2056
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2057
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2058
|
-
if (!targetId.ok) {
|
|
2059
|
-
return targetId;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
if (!hasScreenshotGetter(driver)) {
|
|
2063
|
-
return createErr(
|
|
2064
|
-
ErrorCode.E_DRIVER_UNAVAILABLE,
|
|
2065
|
-
`Driver does not support screenshots: ${driverKey}`
|
|
2066
|
-
);
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
const screenshot = await driver.screenshot(targetId.data);
|
|
2070
|
-
return createOk({
|
|
2071
|
-
driver: driverKey,
|
|
2072
|
-
targetId: targetId.data,
|
|
2073
|
-
screenshot
|
|
2074
|
-
});
|
|
2075
|
-
})
|
|
2076
|
-
);
|
|
2077
|
-
|
|
2078
|
-
toolMap.set("browser.dom.query", async (args) => {
|
|
2079
|
-
const selector = requireStringArg(args, "selector");
|
|
2080
|
-
if (!selector.ok) {
|
|
2081
|
-
return selector;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
const result = await runStructuredAction(args, "domQuery", {
|
|
2085
|
-
selector: selector.data
|
|
2086
|
-
});
|
|
2087
|
-
if (!result.ok) {
|
|
2088
|
-
return result;
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
return createOk({
|
|
2092
|
-
driver: result.data.driver,
|
|
2093
|
-
targetId: result.data.targetId,
|
|
2094
|
-
query: result.data.data
|
|
2095
|
-
});
|
|
2096
|
-
});
|
|
2097
|
-
|
|
2098
|
-
toolMap.set("browser.dom.queryAll", async (args) => {
|
|
2099
|
-
const selector = requireStringArg(args, "selector");
|
|
2100
|
-
if (!selector.ok) {
|
|
2101
|
-
return selector;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
const result = await runStructuredAction(args, "domQueryAll", {
|
|
2105
|
-
selector: selector.data
|
|
2106
|
-
});
|
|
2107
|
-
if (!result.ok) {
|
|
2108
|
-
return result;
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
return createOk({
|
|
2112
|
-
driver: result.data.driver,
|
|
2113
|
-
targetId: result.data.targetId,
|
|
2114
|
-
query: result.data.data
|
|
2115
|
-
});
|
|
2116
|
-
});
|
|
2117
|
-
|
|
2118
|
-
toolMap.set("browser.element.screenshot", async (args) => {
|
|
2119
|
-
const selector = requireStringArg(args, "selector");
|
|
2120
|
-
if (!selector.ok) {
|
|
2121
|
-
return selector;
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
const result = await runStructuredAction(args, "elementScreenshot", {
|
|
2125
|
-
selector: selector.data
|
|
2126
|
-
});
|
|
2127
|
-
if (!result.ok) {
|
|
2128
|
-
return result;
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
return createOk({
|
|
2132
|
-
driver: result.data.driver,
|
|
2133
|
-
targetId: result.data.targetId,
|
|
2134
|
-
screenshot: result.data.data
|
|
2135
|
-
});
|
|
2136
|
-
});
|
|
2137
|
-
|
|
2138
|
-
toolMap.set("browser.a11y.snapshot", async (args) => {
|
|
2139
|
-
const selector = resolveOptionalStringArg(args, "selector");
|
|
2140
|
-
if (!selector.ok) {
|
|
2141
|
-
return selector;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
const result = await runStructuredAction(args, "a11ySnapshot", {
|
|
2145
|
-
...(selector.data !== undefined ? { selector: selector.data } : {})
|
|
2146
|
-
});
|
|
2147
|
-
if (!result.ok) {
|
|
2148
|
-
return result;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
return createOk({
|
|
2152
|
-
driver: result.data.driver,
|
|
2153
|
-
targetId: result.data.targetId,
|
|
2154
|
-
snapshot: result.data.data
|
|
2155
|
-
});
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
toolMap.set("browser.cookie.get", async (args) => {
|
|
2159
|
-
const name = resolveOptionalStringArg(args, "name");
|
|
2160
|
-
if (!name.ok) {
|
|
2161
|
-
return name;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
const result = await runStructuredAction(args, "cookieGet", {
|
|
2165
|
-
...(name.data !== undefined ? { name: name.data } : {})
|
|
2166
|
-
});
|
|
2167
|
-
if (!result.ok) {
|
|
2168
|
-
return result;
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
return createOk({
|
|
2172
|
-
driver: result.data.driver,
|
|
2173
|
-
targetId: result.data.targetId,
|
|
2174
|
-
...result.data.data
|
|
2175
|
-
});
|
|
2176
|
-
});
|
|
2177
|
-
|
|
2178
|
-
toolMap.set("browser.cookie.set", async (args) => {
|
|
2179
|
-
const name = requireStringArg(args, "name");
|
|
2180
|
-
if (!name.ok) {
|
|
2181
|
-
return name;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
const value = requireStringArg(args, "value");
|
|
2185
|
-
if (!value.ok) {
|
|
2186
|
-
return value;
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
const url = resolveOptionalStringArg(args, "url");
|
|
2190
|
-
if (!url.ok) {
|
|
2191
|
-
return url;
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
const result = await runStructuredAction(args, "cookieSet", {
|
|
2195
|
-
name: name.data,
|
|
2196
|
-
value: value.data,
|
|
2197
|
-
...(url.data !== undefined ? { url: url.data } : {})
|
|
2198
|
-
});
|
|
2199
|
-
if (!result.ok) {
|
|
2200
|
-
return result;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
return createOk({
|
|
2204
|
-
driver: result.data.driver,
|
|
2205
|
-
targetId: result.data.targetId,
|
|
2206
|
-
...result.data.data
|
|
2207
|
-
});
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
toolMap.set("browser.cookie.clear", async (args) => {
|
|
2211
|
-
const name = resolveOptionalStringArg(args, "name");
|
|
2212
|
-
if (!name.ok) {
|
|
2213
|
-
return name;
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
const result = await runStructuredAction(args, "cookieClear", {
|
|
2217
|
-
...(name.data !== undefined ? { name: name.data } : {})
|
|
2218
|
-
});
|
|
2219
|
-
if (!result.ok) {
|
|
2220
|
-
return result;
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
return createOk({
|
|
2224
|
-
driver: result.data.driver,
|
|
2225
|
-
targetId: result.data.targetId,
|
|
2226
|
-
...result.data.data
|
|
2227
|
-
});
|
|
2228
|
-
});
|
|
2229
|
-
|
|
2230
|
-
toolMap.set("browser.storage.get", async (args) => {
|
|
2231
|
-
const scope = requireStringArg(args, "scope");
|
|
2232
|
-
if (!scope.ok) {
|
|
2233
|
-
return scope;
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
if (scope.data !== "local" && scope.data !== "session") {
|
|
2237
|
-
return createErr(ErrorCode.E_INVALID_ARG, "scope must be local or session.");
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
const key = requireStringArg(args, "key");
|
|
2241
|
-
if (!key.ok) {
|
|
2242
|
-
return key;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
const result = await runStructuredAction(args, "storageGet", {
|
|
2246
|
-
scope: scope.data,
|
|
2247
|
-
key: key.data
|
|
2248
|
-
});
|
|
2249
|
-
if (!result.ok) {
|
|
2250
|
-
return result;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
return createOk({
|
|
2254
|
-
driver: result.data.driver,
|
|
2255
|
-
targetId: result.data.targetId,
|
|
2256
|
-
...result.data.data
|
|
2257
|
-
});
|
|
2258
|
-
});
|
|
2259
|
-
|
|
2260
|
-
toolMap.set("browser.storage.set", async (args) => {
|
|
2261
|
-
const scope = requireStringArg(args, "scope");
|
|
2262
|
-
if (!scope.ok) {
|
|
2263
|
-
return scope;
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
if (scope.data !== "local" && scope.data !== "session") {
|
|
2267
|
-
return createErr(ErrorCode.E_INVALID_ARG, "scope must be local or session.");
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const key = requireStringArg(args, "key");
|
|
2271
|
-
if (!key.ok) {
|
|
2272
|
-
return key;
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
const value = requireStringArg(args, "value");
|
|
2276
|
-
if (!value.ok) {
|
|
2277
|
-
return value;
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
const result = await runStructuredAction(args, "storageSet", {
|
|
2281
|
-
scope: scope.data,
|
|
2282
|
-
key: key.data,
|
|
2283
|
-
value: value.data
|
|
2284
|
-
});
|
|
2285
|
-
if (!result.ok) {
|
|
2286
|
-
return result;
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
return createOk({
|
|
2290
|
-
driver: result.data.driver,
|
|
2291
|
-
targetId: result.data.targetId,
|
|
2292
|
-
...result.data.data
|
|
2293
|
-
});
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
toolMap.set("browser.frame.list", async (args) => {
|
|
2297
|
-
const result = await runStructuredAction(args, "frameList", {});
|
|
2298
|
-
if (!result.ok) {
|
|
2299
|
-
return result;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
return createOk({
|
|
2303
|
-
driver: result.data.driver,
|
|
2304
|
-
targetId: result.data.targetId,
|
|
2305
|
-
...result.data.data
|
|
2306
|
-
});
|
|
2307
|
-
});
|
|
2308
|
-
|
|
2309
|
-
toolMap.set("browser.frame.snapshot", async (args) => {
|
|
2310
|
-
const frameId = requireStringArg(args, "frameId");
|
|
2311
|
-
if (!frameId.ok) {
|
|
2312
|
-
return frameId;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
const result = await runStructuredAction(args, "frameSnapshot", {
|
|
2316
|
-
frameId: frameId.data
|
|
2317
|
-
});
|
|
2318
|
-
if (!result.ok) {
|
|
2319
|
-
return result;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
return createOk({
|
|
2323
|
-
driver: result.data.driver,
|
|
2324
|
-
targetId: result.data.targetId,
|
|
2325
|
-
...result.data.data
|
|
2326
|
-
});
|
|
2327
|
-
});
|
|
2328
|
-
|
|
2329
|
-
toolMap.set("browser.act", async (args) =>
|
|
2330
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2331
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2332
|
-
if (!targetId.ok) {
|
|
2333
|
-
return targetId;
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
const action = resolveAction(args);
|
|
2337
|
-
if (!action.ok) {
|
|
2338
|
-
return action;
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
const result = await driver.act(action.data, targetId.data);
|
|
2342
|
-
return createOk({
|
|
2343
|
-
driver: driverKey,
|
|
2344
|
-
targetId: targetId.data,
|
|
2345
|
-
result
|
|
2346
|
-
});
|
|
2347
|
-
})
|
|
2348
|
-
);
|
|
2349
|
-
|
|
2350
|
-
toolMap.set("browser.upload.arm", async (args) =>
|
|
2351
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2352
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2353
|
-
if (!targetId.ok) {
|
|
2354
|
-
return targetId;
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
const files = resolveFiles(args);
|
|
2358
|
-
if (!files.ok) {
|
|
2359
|
-
return files;
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
const rootedFiles = applyUploadRoot(files.data, config.uploadRoot);
|
|
2363
|
-
if (!rootedFiles.ok) {
|
|
2364
|
-
return rootedFiles;
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
await driver.armUpload(targetId.data, rootedFiles.data);
|
|
2368
|
-
return createOk({
|
|
2369
|
-
driver: driverKey,
|
|
2370
|
-
targetId: targetId.data,
|
|
2371
|
-
armed: true,
|
|
2372
|
-
files: rootedFiles.data
|
|
2373
|
-
});
|
|
2374
|
-
})
|
|
2375
|
-
);
|
|
2376
|
-
|
|
2377
|
-
toolMap.set("browser.dialog.arm", async (args) =>
|
|
2378
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2379
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2380
|
-
if (!targetId.ok) {
|
|
2381
|
-
return targetId;
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
await driver.armDialog(targetId.data);
|
|
2385
|
-
return createOk({
|
|
2386
|
-
driver: driverKey,
|
|
2387
|
-
targetId: targetId.data,
|
|
2388
|
-
armed: true
|
|
2389
|
-
});
|
|
2390
|
-
})
|
|
2391
|
-
);
|
|
2392
|
-
|
|
2393
|
-
toolMap.set("browser.download.trigger", async (args) =>
|
|
2394
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2395
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2396
|
-
if (!targetId.ok) {
|
|
2397
|
-
return targetId;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
await driver.triggerDownload(targetId.data);
|
|
2401
|
-
return createOk({
|
|
2402
|
-
driver: driverKey,
|
|
2403
|
-
targetId: targetId.data,
|
|
2404
|
-
triggered: true
|
|
2405
|
-
});
|
|
2406
|
-
})
|
|
2407
|
-
);
|
|
2408
|
-
|
|
2409
|
-
toolMap.set("browser.download.wait", async (args) =>
|
|
2410
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2411
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2412
|
-
if (!targetId.ok) {
|
|
2413
|
-
return targetId;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
const requestedPath = resolveOptionalPathArg(args, "path");
|
|
2417
|
-
if (!requestedPath.ok) {
|
|
2418
|
-
return requestedPath;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
const download = await driver.waitDownload(targetId.data, undefined, requestedPath.data);
|
|
2422
|
-
const normalizedDownload = normalizeDownloadPayload(
|
|
2423
|
-
download,
|
|
2424
|
-
requestedPath.data,
|
|
2425
|
-
config.downloadRoot
|
|
2426
|
-
);
|
|
2427
|
-
if (!normalizedDownload.ok) {
|
|
2428
|
-
return normalizedDownload;
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
return createOk({
|
|
2432
|
-
driver: driverKey,
|
|
2433
|
-
targetId: targetId.data,
|
|
2434
|
-
download: normalizedDownload.data
|
|
2435
|
-
});
|
|
2436
|
-
})
|
|
2437
|
-
);
|
|
2438
|
-
|
|
2439
|
-
toolMap.set("browser.wait.element", async (args) =>
|
|
2440
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2441
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2442
|
-
if (!targetId.ok) {
|
|
2443
|
-
return targetId;
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
const selector = requireStringArg(args, "selector");
|
|
2447
|
-
if (!selector.ok) {
|
|
2448
|
-
return selector;
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
const timeoutMs = resolveOptionalIntegerArg(args, "timeoutMs", 1);
|
|
2452
|
-
if (!timeoutMs.ok) {
|
|
2453
|
-
return timeoutMs;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
const pollMs = resolveOptionalIntegerArg(args, "pollMs", 1);
|
|
2457
|
-
if (!pollMs.ok) {
|
|
2458
|
-
return pollMs;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
const maxWaitMs = timeoutMs.data ?? DEFAULT_NETWORK_WAIT_TIMEOUT_MS;
|
|
2462
|
-
const intervalMs = pollMs.data ?? DEFAULT_NETWORK_WAIT_POLL_MS;
|
|
2463
|
-
const startTime = Date.now();
|
|
2464
|
-
|
|
2465
|
-
while (Date.now() - startTime <= maxWaitMs) {
|
|
2466
|
-
const actionResult = await driver.act(
|
|
2467
|
-
{
|
|
2468
|
-
type: "domQuery",
|
|
2469
|
-
payload: {
|
|
2470
|
-
selector: selector.data
|
|
2471
|
-
}
|
|
2472
|
-
},
|
|
2473
|
-
targetId.data
|
|
2474
|
-
);
|
|
2475
|
-
const data = resolveDriverActionData(actionResult, "domQuery");
|
|
2476
|
-
if (!data.ok) {
|
|
2477
|
-
return data;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
if (data.data.found === true) {
|
|
2481
|
-
return createOk({
|
|
2482
|
-
driver: driverKey,
|
|
2483
|
-
targetId: targetId.data,
|
|
2484
|
-
selector: selector.data,
|
|
2485
|
-
element: data.data,
|
|
2486
|
-
elapsedMs: Date.now() - startTime
|
|
2487
|
-
});
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
await delay(intervalMs);
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
return createErr(
|
|
2494
|
-
ErrorCode.E_TIMEOUT,
|
|
2495
|
-
`Timed out waiting for element: ${selector.data}`
|
|
2496
|
-
);
|
|
2497
|
-
})
|
|
2498
|
-
);
|
|
2499
|
-
|
|
2500
|
-
toolMap.set("browser.wait.text", async (args) =>
|
|
2501
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2502
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2503
|
-
if (!targetId.ok) {
|
|
2504
|
-
return targetId;
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
const text = requireStringArg(args, "text");
|
|
2508
|
-
if (!text.ok) {
|
|
2509
|
-
return text;
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
const selector = resolveOptionalStringArg(args, "selector");
|
|
2513
|
-
if (!selector.ok) {
|
|
2514
|
-
return selector;
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
const timeoutMs = resolveOptionalIntegerArg(args, "timeoutMs", 1);
|
|
2518
|
-
if (!timeoutMs.ok) {
|
|
2519
|
-
return timeoutMs;
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
const pollMs = resolveOptionalIntegerArg(args, "pollMs", 1);
|
|
2523
|
-
if (!pollMs.ok) {
|
|
2524
|
-
return pollMs;
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
const maxWaitMs = timeoutMs.data ?? DEFAULT_NETWORK_WAIT_TIMEOUT_MS;
|
|
2528
|
-
const intervalMs = pollMs.data ?? DEFAULT_NETWORK_WAIT_POLL_MS;
|
|
2529
|
-
const startTime = Date.now();
|
|
2530
|
-
|
|
2531
|
-
while (Date.now() - startTime <= maxWaitMs) {
|
|
2532
|
-
if (selector.data !== undefined) {
|
|
2533
|
-
const actionResult = await driver.act(
|
|
2534
|
-
{
|
|
2535
|
-
type: "domQueryAll",
|
|
2536
|
-
payload: {
|
|
2537
|
-
selector: selector.data
|
|
2538
|
-
}
|
|
2539
|
-
},
|
|
2540
|
-
targetId.data
|
|
2541
|
-
);
|
|
2542
|
-
const data = resolveDriverActionData(actionResult, "domQueryAll");
|
|
2543
|
-
if (!data.ok) {
|
|
2544
|
-
return data;
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
const elements = readElementList(data.data.elements);
|
|
2548
|
-
const matchingElement = elements.find(
|
|
2549
|
-
(element) =>
|
|
2550
|
-
typeof element.text === "string" &&
|
|
2551
|
-
element.text.includes(text.data)
|
|
2552
|
-
);
|
|
2553
|
-
if (matchingElement !== undefined) {
|
|
2554
|
-
return createOk({
|
|
2555
|
-
driver: driverKey,
|
|
2556
|
-
targetId: targetId.data,
|
|
2557
|
-
text: text.data,
|
|
2558
|
-
selector: selector.data,
|
|
2559
|
-
element: matchingElement,
|
|
2560
|
-
elapsedMs: Date.now() - startTime
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
} else {
|
|
2564
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2565
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2566
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
const html = readSnapshotHtml(snapshot);
|
|
2570
|
-
if (html !== undefined && html.includes(text.data)) {
|
|
2571
|
-
return createOk({
|
|
2572
|
-
driver: driverKey,
|
|
2573
|
-
targetId: targetId.data,
|
|
2574
|
-
text: text.data,
|
|
2575
|
-
elapsedMs: Date.now() - startTime,
|
|
2576
|
-
matchedIn: "snapshot.html"
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
await delay(intervalMs);
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
return createErr(
|
|
2585
|
-
ErrorCode.E_TIMEOUT,
|
|
2586
|
-
`Timed out waiting for text: ${text.data}`
|
|
2587
|
-
);
|
|
2588
|
-
})
|
|
2589
|
-
);
|
|
2590
|
-
|
|
2591
|
-
toolMap.set("browser.wait.url", async (args) =>
|
|
2592
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2593
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2594
|
-
if (!targetId.ok) {
|
|
2595
|
-
return targetId;
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
const urlPattern = requireStringArg(args, "urlPattern");
|
|
2599
|
-
if (!urlPattern.ok) {
|
|
2600
|
-
return urlPattern;
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
const timeoutMs = resolveOptionalIntegerArg(args, "timeoutMs", 1);
|
|
2604
|
-
if (!timeoutMs.ok) {
|
|
2605
|
-
return timeoutMs;
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
const pollMs = resolveOptionalIntegerArg(args, "pollMs", 1);
|
|
2609
|
-
if (!pollMs.ok) {
|
|
2610
|
-
return pollMs;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
const maxWaitMs = timeoutMs.data ?? DEFAULT_NETWORK_WAIT_TIMEOUT_MS;
|
|
2614
|
-
const intervalMs = pollMs.data ?? DEFAULT_NETWORK_WAIT_POLL_MS;
|
|
2615
|
-
const startTime = Date.now();
|
|
2616
|
-
|
|
2617
|
-
while (Date.now() - startTime <= maxWaitMs) {
|
|
2618
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2619
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2620
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
const currentUrl = readSnapshotUrl(snapshot);
|
|
2624
|
-
if (currentUrl !== undefined && currentUrl.includes(urlPattern.data)) {
|
|
2625
|
-
return createOk({
|
|
2626
|
-
driver: driverKey,
|
|
2627
|
-
targetId: targetId.data,
|
|
2628
|
-
urlPattern: urlPattern.data,
|
|
2629
|
-
currentUrl,
|
|
2630
|
-
elapsedMs: Date.now() - startTime
|
|
2631
|
-
});
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
await delay(intervalMs);
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
return createErr(
|
|
2638
|
-
ErrorCode.E_TIMEOUT,
|
|
2639
|
-
`Timed out waiting for URL: ${urlPattern.data}`
|
|
2640
|
-
);
|
|
2641
|
-
})
|
|
2642
|
-
);
|
|
2643
|
-
|
|
2644
|
-
toolMap.set("browser.console.list", async (args) =>
|
|
2645
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2646
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2647
|
-
if (!targetId.ok) {
|
|
2648
|
-
return targetId;
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
const consoleType = resolveOptionalStringArg(args, "type");
|
|
2652
|
-
if (!consoleType.ok) {
|
|
2653
|
-
return consoleType;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
const contains = resolveOptionalStringArg(args, "contains");
|
|
2657
|
-
if (!contains.ok) {
|
|
2658
|
-
return contains;
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
const since = resolveOptionalTimestampArg(args, "since");
|
|
2662
|
-
if (!since.ok) {
|
|
2663
|
-
return since;
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
const limit = resolveOptionalIntegerArg(args, "limit", 1);
|
|
2667
|
-
if (!limit.ok) {
|
|
2668
|
-
return limit;
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2672
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2673
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
if (hasConsoleEntriesGetter(driver)) {
|
|
2677
|
-
const entries = filterConsoleEntries(
|
|
2678
|
-
readConsoleEntries(driver.getConsoleEntries(targetId.data)),
|
|
2679
|
-
{
|
|
2680
|
-
type: consoleType.data,
|
|
2681
|
-
contains: contains.data,
|
|
2682
|
-
sinceMs: since.data,
|
|
2683
|
-
limit: limit.data
|
|
2684
|
-
}
|
|
2685
|
-
);
|
|
2686
|
-
return createOk({
|
|
2687
|
-
driver: driverKey,
|
|
2688
|
-
targetId: targetId.data,
|
|
2689
|
-
entries
|
|
2690
|
-
});
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
return createOk({
|
|
2694
|
-
driver: driverKey,
|
|
2695
|
-
targetId: targetId.data,
|
|
2696
|
-
entries: []
|
|
2697
|
-
});
|
|
2698
|
-
})
|
|
2699
|
-
);
|
|
2700
|
-
|
|
2701
|
-
toolMap.set("browser.network.list", async (args) =>
|
|
2702
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2703
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2704
|
-
if (!targetId.ok) {
|
|
2705
|
-
return targetId;
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
const urlContains = resolveOptionalStringArg(args, "urlContains");
|
|
2709
|
-
if (!urlContains.ok) {
|
|
2710
|
-
return urlContains;
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
const method = resolveOptionalStringArg(args, "method");
|
|
2714
|
-
if (!method.ok) {
|
|
2715
|
-
return method;
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
const status = resolveOptionalIntegerArg(args, "status", 100);
|
|
2719
|
-
if (!status.ok) {
|
|
2720
|
-
return status;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
const since = resolveOptionalTimestampArg(args, "since");
|
|
2724
|
-
if (!since.ok) {
|
|
2725
|
-
return since;
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
const limit = resolveOptionalIntegerArg(args, "limit", 1);
|
|
2729
|
-
if (!limit.ok) {
|
|
2730
|
-
return limit;
|
|
2731
|
-
}
|
|
2732
|
-
|
|
2733
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2734
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2735
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
const requests = filterNetworkSummaries(readNetworkRequestSummaries(snapshot), {
|
|
2739
|
-
urlContains: urlContains.data,
|
|
2740
|
-
method: method.data,
|
|
2741
|
-
status: status.data,
|
|
2742
|
-
sinceMs: since.data,
|
|
2743
|
-
limit: limit.data
|
|
2744
|
-
});
|
|
2745
|
-
|
|
2746
|
-
return createOk({
|
|
2747
|
-
driver: driverKey,
|
|
2748
|
-
targetId: targetId.data,
|
|
2749
|
-
requests
|
|
2750
|
-
});
|
|
2751
|
-
})
|
|
2752
|
-
);
|
|
2753
|
-
|
|
2754
|
-
toolMap.set("browser.network.harExport", async (args) =>
|
|
2755
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2756
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2757
|
-
if (!targetId.ok) {
|
|
2758
|
-
return targetId;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
const includeBodiesRaw = args.includeBodies;
|
|
2762
|
-
if (includeBodiesRaw !== undefined && typeof includeBodiesRaw !== "boolean") {
|
|
2763
|
-
return createErr(
|
|
2764
|
-
ErrorCode.E_INVALID_ARG,
|
|
2765
|
-
"includeBodies must be a boolean when provided."
|
|
2766
|
-
);
|
|
2767
|
-
}
|
|
2768
|
-
const includeBodies = includeBodiesRaw === true;
|
|
2769
|
-
|
|
2770
|
-
const urlContains = resolveOptionalStringArg(args, "urlContains");
|
|
2771
|
-
if (!urlContains.ok) {
|
|
2772
|
-
return urlContains;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
const method = resolveOptionalStringArg(args, "method");
|
|
2776
|
-
if (!method.ok) {
|
|
2777
|
-
return method;
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
const status = resolveOptionalIntegerArg(args, "status", 100);
|
|
2781
|
-
if (!status.ok) {
|
|
2782
|
-
return status;
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
const since = resolveOptionalTimestampArg(args, "since");
|
|
2786
|
-
if (!since.ok) {
|
|
2787
|
-
return since;
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
const limit = resolveOptionalIntegerArg(args, "limit", 1);
|
|
2791
|
-
if (!limit.ok) {
|
|
2792
|
-
return limit;
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2796
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2797
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
const filteredSummaries = filterNetworkSummaries(readNetworkRequestSummaries(snapshot), {
|
|
2801
|
-
urlContains: urlContains.data,
|
|
2802
|
-
method: method.data,
|
|
2803
|
-
status: status.data,
|
|
2804
|
-
sinceMs: since.data,
|
|
2805
|
-
limit: limit.data
|
|
2806
|
-
});
|
|
2807
|
-
const har = createHarLog(filteredSummaries, includeBodies, (requestId) => {
|
|
2808
|
-
if (!hasNetworkResponseBodyGetter(driver)) {
|
|
2809
|
-
return undefined;
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
return driver.getNetworkResponseBody(requestId, targetId.data);
|
|
2813
|
-
});
|
|
2814
|
-
|
|
2815
|
-
return createOk({
|
|
2816
|
-
driver: driverKey,
|
|
2817
|
-
targetId: targetId.data,
|
|
2818
|
-
har
|
|
2819
|
-
});
|
|
2820
|
-
})
|
|
2821
|
-
);
|
|
2822
|
-
|
|
2823
|
-
toolMap.set("browser.network.waitFor", async (args) =>
|
|
2824
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2825
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2826
|
-
if (!targetId.ok) {
|
|
2827
|
-
return targetId;
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
const urlPattern = requireStringArg(args, "urlPattern");
|
|
2831
|
-
if (!urlPattern.ok) {
|
|
2832
|
-
return urlPattern;
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
const method = resolveOptionalStringArg(args, "method");
|
|
2836
|
-
if (!method.ok) {
|
|
2837
|
-
return method;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
const status = resolveOptionalIntegerArg(args, "status", 100);
|
|
2841
|
-
if (!status.ok) {
|
|
2842
|
-
return status;
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
const timeoutMs = resolveOptionalIntegerArg(args, "timeoutMs", 1);
|
|
2846
|
-
if (!timeoutMs.ok) {
|
|
2847
|
-
return timeoutMs;
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
const pollMs = resolveOptionalIntegerArg(args, "pollMs", 1);
|
|
2851
|
-
if (!pollMs.ok) {
|
|
2852
|
-
return pollMs;
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
const methodPattern = method.data?.toUpperCase();
|
|
2856
|
-
const expectedStatus = status.data;
|
|
2857
|
-
const maxWaitMs = timeoutMs.data ?? DEFAULT_NETWORK_WAIT_TIMEOUT_MS;
|
|
2858
|
-
const intervalMs = pollMs.data ?? DEFAULT_NETWORK_WAIT_POLL_MS;
|
|
2859
|
-
const startTime = Date.now();
|
|
2860
|
-
|
|
2861
|
-
while (Date.now() - startTime <= maxWaitMs) {
|
|
2862
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2863
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2864
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
const summaries = readNetworkRequestSummaries(snapshot);
|
|
2868
|
-
const matchedSummary = summaries
|
|
2869
|
-
.slice()
|
|
2870
|
-
.reverse()
|
|
2871
|
-
.find((summary) => {
|
|
2872
|
-
if (!summary.url.includes(urlPattern.data)) {
|
|
2873
|
-
return false;
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
if (
|
|
2877
|
-
methodPattern !== undefined &&
|
|
2878
|
-
(summary.method === undefined || summary.method.toUpperCase() !== methodPattern)
|
|
2879
|
-
) {
|
|
2880
|
-
return false;
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
if (expectedStatus !== undefined && summary.status !== expectedStatus) {
|
|
2884
|
-
return false;
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
return true;
|
|
2888
|
-
});
|
|
2889
|
-
|
|
2890
|
-
if (matchedSummary !== undefined) {
|
|
2891
|
-
return createOk({
|
|
2892
|
-
driver: driverKey,
|
|
2893
|
-
targetId: targetId.data,
|
|
2894
|
-
request: matchedSummary,
|
|
2895
|
-
elapsedMs: Date.now() - startTime
|
|
2896
|
-
});
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
await delay(intervalMs);
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
return createErr(
|
|
2903
|
-
ErrorCode.E_TIMEOUT,
|
|
2904
|
-
`Timed out waiting for network response: ${urlPattern.data}`
|
|
2905
|
-
);
|
|
2906
|
-
})
|
|
2907
|
-
);
|
|
2908
|
-
|
|
2909
|
-
toolMap.set("browser.network.responseBody", async (args) =>
|
|
2910
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
2911
|
-
const targetId = resolveTargetId(args, sessions);
|
|
2912
|
-
if (!targetId.ok) {
|
|
2913
|
-
return targetId;
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
const requestId = resolveRequestId(args);
|
|
2917
|
-
if (!requestId.ok) {
|
|
2918
|
-
return requestId;
|
|
2919
|
-
}
|
|
2920
|
-
|
|
2921
|
-
const snapshot = await driver.snapshot(targetId.data);
|
|
2922
|
-
if (!isTargetKnownSnapshot(snapshot)) {
|
|
2923
|
-
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
if (hasNetworkResponseBodyGetter(driver)) {
|
|
2927
|
-
const responseBody = driver.getNetworkResponseBody(requestId.data, targetId.data);
|
|
2928
|
-
if (responseBody === undefined) {
|
|
2929
|
-
return createErr(
|
|
2930
|
-
ErrorCode.E_NOT_FOUND,
|
|
2931
|
-
`Unknown requestId: ${requestId.data} (target: ${targetId.data})`
|
|
2932
|
-
);
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
return createOk({
|
|
2936
|
-
driver: driverKey,
|
|
2937
|
-
targetId: targetId.data,
|
|
2938
|
-
requestId: requestId.data,
|
|
2939
|
-
body: responseBody.body,
|
|
2940
|
-
encoding: responseBody.encoding
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
return createOk({
|
|
2945
|
-
driver: driverKey,
|
|
2946
|
-
targetId: targetId.data,
|
|
2947
|
-
requestId: requestId.data,
|
|
2948
|
-
body: "",
|
|
2949
|
-
encoding: "utf8"
|
|
2950
|
-
});
|
|
2951
|
-
})
|
|
2952
|
-
);
|
|
2953
|
-
|
|
2954
|
-
toolMap.set("browser.trace.get", async (args) => {
|
|
2955
|
-
const limit = resolveOptionalIntegerArg(args, "limit", 1);
|
|
2956
|
-
if (!limit.ok) {
|
|
2957
|
-
return limit;
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
const traceState = traceStateBySession.get(args.sessionId);
|
|
2961
|
-
const steps = applyLimitToTail(traceState?.steps ?? [], limit.data);
|
|
2962
|
-
const keyResponses = applyLimitToTail(traceState?.keyResponses ?? [], limit.data);
|
|
2963
|
-
const screenshots = applyLimitToTail(traceState?.screenshots ?? [], limit.data);
|
|
2964
|
-
|
|
2965
|
-
return createOk({
|
|
2966
|
-
sessionId: args.sessionId,
|
|
2967
|
-
steps,
|
|
2968
|
-
keyResponses,
|
|
2969
|
-
screenshots
|
|
2970
|
-
});
|
|
2971
|
-
});
|
|
2972
|
-
|
|
2973
|
-
for (const [toolName, handler] of [...toolMap.entries()]) {
|
|
2974
|
-
toolMap.set(toolName, async (args) => {
|
|
2975
|
-
const startedAtMs = Date.now();
|
|
2976
|
-
const authorization = authorizeToolCall(args, toolName, config.authToken, allowedScopes);
|
|
2977
|
-
if (!authorization.ok) {
|
|
2978
|
-
const finishedAtMs = Date.now();
|
|
2979
|
-
recordTrace(args.sessionId, toolName, args, authorization, startedAtMs, finishedAtMs);
|
|
2980
|
-
return authorization;
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
const sessionAuthorization = authorizeSessionGovernance(args, sessions, config);
|
|
2984
|
-
if (!sessionAuthorization.ok) {
|
|
2985
|
-
const finishedAtMs = Date.now();
|
|
2986
|
-
recordTrace(args.sessionId, toolName, args, sessionAuthorization, startedAtMs, finishedAtMs);
|
|
2987
|
-
return sessionAuthorization;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
const domainAuthorization = authorizeDomainAllowlist(toolName, args, config);
|
|
2991
|
-
if (!domainAuthorization.ok) {
|
|
2992
|
-
const finishedAtMs = Date.now();
|
|
2993
|
-
recordTrace(args.sessionId, toolName, args, domainAuthorization, startedAtMs, finishedAtMs);
|
|
2994
|
-
return domainAuthorization;
|
|
2995
|
-
}
|
|
2996
|
-
|
|
2997
|
-
const response = await handler(args);
|
|
2998
|
-
const finishedAtMs = Date.now();
|
|
2999
|
-
recordTrace(args.sessionId, toolName, args, response, startedAtMs, finishedAtMs);
|
|
3000
|
-
return response;
|
|
3001
|
-
});
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
return toolMap;
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
export function createContainer(config: BrowserdConfig = loadBrowserdConfig()): BrowserdContainer {
|
|
3008
|
-
const sessions = new SessionStore({
|
|
3009
|
-
ttlMs: config.sessionTtlMs
|
|
3010
|
-
});
|
|
3011
|
-
const driverRegistry = new DriverRegistry();
|
|
3012
|
-
const drivers = new Map<string, BrowserDriver>();
|
|
3013
|
-
const cleanupHandlers: Array<() => void> = [];
|
|
3014
|
-
|
|
3015
|
-
if (config.sessionTtlMs > 0 && config.sessionCleanupIntervalMs > 0) {
|
|
3016
|
-
const cleanupTimer = setInterval(() => {
|
|
3017
|
-
sessions.cleanupExpired();
|
|
3018
|
-
}, config.sessionCleanupIntervalMs);
|
|
3019
|
-
cleanupTimer.unref?.();
|
|
3020
|
-
cleanupHandlers.push(() => {
|
|
3021
|
-
clearInterval(cleanupTimer);
|
|
3022
|
-
});
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
function registerDriver(driverKey: string, driver: BrowserDriver): void {
|
|
3026
|
-
driverRegistry.register(driverKey, driver);
|
|
3027
|
-
drivers.set(driverKey, driver);
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
registerDriver(DEFAULT_DRIVER_KEY, createManagedDriver());
|
|
3031
|
-
if (config.managedLocalEnabled) {
|
|
3032
|
-
registerDriver(
|
|
3033
|
-
MANAGED_LOCAL_DRIVER_KEY,
|
|
3034
|
-
createManagedLocalDriver({
|
|
3035
|
-
browserName: config.managedLocalLaunch.browserName,
|
|
3036
|
-
headless: config.managedLocalLaunch.headless,
|
|
3037
|
-
channel: config.managedLocalLaunch.channel,
|
|
3038
|
-
executablePath: config.managedLocalLaunch.executablePath,
|
|
3039
|
-
launchTimeoutMs: config.managedLocalLaunch.launchTimeoutMs,
|
|
3040
|
-
args: [...config.managedLocalLaunch.args]
|
|
3041
|
-
})
|
|
3042
|
-
);
|
|
3043
|
-
}
|
|
3044
|
-
const chromeRelayDriver =
|
|
3045
|
-
config.chromeRelayMode === "extension"
|
|
3046
|
-
? (() => {
|
|
3047
|
-
const extensionBridge = createChromeRelayExtensionBridge({
|
|
3048
|
-
relayUrl: config.chromeRelayUrl,
|
|
3049
|
-
token: config.chromeRelayExtensionToken,
|
|
3050
|
-
requestTimeoutMs: config.chromeRelayExtensionRequestTimeoutMs
|
|
3051
|
-
});
|
|
3052
|
-
cleanupHandlers.push(() => {
|
|
3053
|
-
void extensionBridge.close();
|
|
3054
|
-
});
|
|
3055
|
-
|
|
3056
|
-
return createChromeRelayDriver({
|
|
3057
|
-
relayUrl: config.chromeRelayUrl,
|
|
3058
|
-
runtime: createChromeRelayExtensionRuntime({
|
|
3059
|
-
transport: {
|
|
3060
|
-
invoke: async (method, params) => await extensionBridge.invoke(method, params),
|
|
3061
|
-
isConnected: () => extensionBridge.isConnected(),
|
|
3062
|
-
onEvent: (listener) => extensionBridge.onEvent(listener)
|
|
3063
|
-
}
|
|
3064
|
-
})
|
|
3065
|
-
});
|
|
3066
|
-
})()
|
|
3067
|
-
: createChromeRelayDriver({ relayUrl: config.chromeRelayUrl });
|
|
3068
|
-
registerDriver("chrome-relay", chromeRelayDriver);
|
|
3069
|
-
registerDriver("remote-cdp", createRemoteCdpDriver({ cdpUrl: config.remoteCdpUrl }));
|
|
3070
|
-
const defaultDriverKey = drivers.has(config.defaultDriver)
|
|
3071
|
-
? config.defaultDriver
|
|
3072
|
-
: DEFAULT_DRIVER_KEY;
|
|
3073
|
-
|
|
3074
|
-
return {
|
|
3075
|
-
config,
|
|
3076
|
-
drivers,
|
|
3077
|
-
driverRegistry,
|
|
3078
|
-
sessions,
|
|
3079
|
-
mcpServer: createMcpStdioServer(
|
|
3080
|
-
createBrowserdToolMap(drivers, sessions, defaultDriverKey, config)
|
|
3081
|
-
),
|
|
3082
|
-
close() {
|
|
3083
|
-
for (const cleanupHandler of [...cleanupHandlers].reverse()) {
|
|
3084
|
-
cleanupHandler();
|
|
3085
|
-
}
|
|
3086
|
-
}
|
|
3087
|
-
};
|
|
3088
|
-
}
|