@flrande/browserctl 0.5.0 → 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.
Files changed (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -57
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -253
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1436
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. 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
- }