@flrande/browserctl 0.4.0-dev.15.1 → 0.5.0-dev.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/browserctl/src/commands/act.test.ts +71 -0
- package/apps/browserctl/src/commands/act.ts +45 -1
- package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
- package/apps/browserctl/src/commands/console-list.test.ts +102 -0
- package/apps/browserctl/src/commands/console-list.ts +89 -1
- package/apps/browserctl/src/commands/har-export.test.ts +112 -0
- package/apps/browserctl/src/commands/har-export.ts +120 -0
- package/apps/browserctl/src/commands/memory-delete.ts +20 -0
- package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
- package/apps/browserctl/src/commands/memory-list.ts +90 -0
- package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
- package/apps/browserctl/src/commands/memory-purge.ts +16 -0
- package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
- package/apps/browserctl/src/commands/memory-status.ts +16 -0
- package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
- package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
- package/apps/browserctl/src/commands/network-list.test.ts +110 -0
- package/apps/browserctl/src/commands/network-list.ts +112 -0
- package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
- package/apps/browserctl/src/commands/session-drop.ts +16 -0
- package/apps/browserctl/src/commands/session-list.test.ts +81 -0
- package/apps/browserctl/src/commands/session-list.ts +70 -0
- package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
- package/apps/browserctl/src/commands/trace-get.ts +62 -0
- package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-element.ts +76 -0
- package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
- package/apps/browserctl/src/commands/wait-text.ts +93 -0
- package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-url.ts +76 -0
- package/apps/browserctl/src/main.dispatch.test.ts +206 -1
- package/apps/browserctl/src/main.test.ts +30 -0
- package/apps/browserctl/src/main.ts +246 -4
- package/apps/browserd/src/container.ts +1603 -48
- package/apps/browserd/src/main.test.ts +538 -1
- package/apps/browserd/src/tool-matrix.test.ts +492 -3
- package/package.json +5 -1
- package/packages/core/src/driver.ts +1 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/navigation-memory.test.ts +259 -0
- package/packages/core/src/navigation-memory.ts +360 -0
- package/packages/core/src/session-store.test.ts +33 -0
- package/packages/core/src/session-store.ts +111 -6
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
- package/packages/driver-managed/src/managed-driver.test.ts +124 -0
- package/packages/driver-managed/src/managed-driver.ts +233 -17
- package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
- package/packages/driver-managed/src/managed-local-driver.ts +232 -10
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
- package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DriverRegistry,
|
|
3
3
|
SessionStore,
|
|
4
|
+
createNavigationMemoryStore,
|
|
4
5
|
type BrowserDriver,
|
|
5
|
-
type BrowserDriverScreenshot
|
|
6
|
+
type BrowserDriverScreenshot,
|
|
7
|
+
type NavigationMemoryMode,
|
|
8
|
+
type NavigationSignal
|
|
6
9
|
} from "../../../packages/core/src";
|
|
7
10
|
import {
|
|
8
11
|
createChromeRelayDriver,
|
|
@@ -23,11 +26,12 @@ import {
|
|
|
23
26
|
type ToolCallArgs,
|
|
24
27
|
type ToolMap
|
|
25
28
|
} from "../../../packages/transport-mcp-stdio/src";
|
|
26
|
-
import { win32 as windowsPath } from "node:path";
|
|
29
|
+
import { posix as posixPath, win32 as windowsPath } from "node:path";
|
|
27
30
|
import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
|
|
28
31
|
|
|
29
32
|
type CapabilityScope = "read" | "act" | "upload" | "download";
|
|
30
33
|
type ChromeRelayMode = "cdp" | "extension";
|
|
34
|
+
type DomainAllowlistMode = "off" | "enforce";
|
|
31
35
|
|
|
32
36
|
export type BrowserdConfig = {
|
|
33
37
|
chromeRelayUrl: string;
|
|
@@ -41,6 +45,18 @@ export type BrowserdConfig = {
|
|
|
41
45
|
downloadRoot?: string;
|
|
42
46
|
authToken?: string;
|
|
43
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;
|
|
44
60
|
managedLocalLaunch: {
|
|
45
61
|
browserName: ManagedLocalBrowserName;
|
|
46
62
|
headless: boolean;
|
|
@@ -61,6 +77,14 @@ const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
|
|
|
61
77
|
const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
|
|
62
78
|
const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
|
|
63
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";
|
|
64
88
|
const ALL_CAPABILITY_SCOPES: readonly CapabilityScope[] = ["read", "act", "upload", "download"];
|
|
65
89
|
const TOOL_SCOPE_BY_NAME: Readonly<Record<string, CapabilityScope>> = {
|
|
66
90
|
"browser.status": "read",
|
|
@@ -76,6 +100,11 @@ const TOOL_SCOPE_BY_NAME: Readonly<Record<string, CapabilityScope>> = {
|
|
|
76
100
|
"browser.dom.queryAll": "read",
|
|
77
101
|
"browser.element.screenshot": "read",
|
|
78
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",
|
|
79
108
|
"browser.act": "act",
|
|
80
109
|
"browser.upload.arm": "upload",
|
|
81
110
|
"browser.dialog.arm": "act",
|
|
@@ -90,7 +119,19 @@ const TOOL_SCOPE_BY_NAME: Readonly<Record<string, CapabilityScope>> = {
|
|
|
90
119
|
"browser.frame.list": "read",
|
|
91
120
|
"browser.frame.snapshot": "read",
|
|
92
121
|
"browser.console.list": "read",
|
|
93
|
-
"browser.network.responseBody": "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"
|
|
94
135
|
};
|
|
95
136
|
|
|
96
137
|
function resolveNonEmptyString(value: string | undefined): string | undefined {
|
|
@@ -133,6 +174,15 @@ function parseOptionalNumber(value: string | undefined): number | undefined {
|
|
|
133
174
|
return parsedNumber;
|
|
134
175
|
}
|
|
135
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
|
+
|
|
136
186
|
function parseCsv(value: string | undefined): string[] {
|
|
137
187
|
const rawValue = resolveNonEmptyString(value);
|
|
138
188
|
if (rawValue === undefined) {
|
|
@@ -190,6 +240,24 @@ function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
|
|
|
190
240
|
return DEFAULT_CHROME_RELAY_MODE;
|
|
191
241
|
}
|
|
192
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
|
+
|
|
193
261
|
export type BrowserdContainer = {
|
|
194
262
|
config: BrowserdConfig;
|
|
195
263
|
drivers: Map<string, BrowserDriver>;
|
|
@@ -210,6 +278,9 @@ export function loadBrowserdConfig(
|
|
|
210
278
|
|
|
211
279
|
const defaultDriver =
|
|
212
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);
|
|
213
284
|
|
|
214
285
|
return {
|
|
215
286
|
chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
|
|
@@ -226,6 +297,22 @@ export function loadBrowserdConfig(
|
|
|
226
297
|
downloadRoot: resolveNonEmptyString(env.BROWSERD_DOWNLOAD_ROOT),
|
|
227
298
|
authToken: resolveNonEmptyString(env.BROWSERD_AUTH_TOKEN),
|
|
228
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,
|
|
229
316
|
managedLocalLaunch: {
|
|
230
317
|
browserName: parseManagedLocalBrowserName(env.BROWSERD_MANAGED_LOCAL_BROWSER),
|
|
231
318
|
headless: parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_HEADLESS, DEFAULT_MANAGED_LOCAL_HEADLESS),
|
|
@@ -493,6 +580,229 @@ function resolveOptionalIntegerArg(
|
|
|
493
580
|
return resolved;
|
|
494
581
|
}
|
|
495
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
|
+
|
|
496
806
|
function resolveDriverActionData(
|
|
497
807
|
actionResult: unknown,
|
|
498
808
|
actionType: string
|
|
@@ -599,6 +909,214 @@ function readNetworkRequestSummaries(snapshot: unknown): NetworkRequestSummary[]
|
|
|
599
909
|
return summaries;
|
|
600
910
|
}
|
|
601
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
|
+
|
|
602
1120
|
function delay(ms: number): Promise<void> {
|
|
603
1121
|
return new Promise((resolve) => {
|
|
604
1122
|
setTimeout(resolve, ms);
|
|
@@ -643,22 +1161,247 @@ function authorizeToolCall(
|
|
|
643
1161
|
return createOk(null);
|
|
644
1162
|
}
|
|
645
1163
|
|
|
646
|
-
function
|
|
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(
|
|
647
1388
|
inputPath: string,
|
|
648
1389
|
rootPath: string,
|
|
649
1390
|
context: "upload" | "download"
|
|
650
1391
|
): ToolResponse<string> {
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
);
|
|
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);
|
|
659
1402
|
const isInRoot =
|
|
660
1403
|
relativePath === "" ||
|
|
661
|
-
(!relativePath.startsWith("..") && !
|
|
1404
|
+
(!relativePath.startsWith("..") && !pathRuntime.isAbsolute(relativePath));
|
|
662
1405
|
if (!isInRoot) {
|
|
663
1406
|
return createErr(
|
|
664
1407
|
ErrorCode.E_PERMISSION,
|
|
@@ -679,7 +1422,7 @@ function applyUploadRoot(files: string[], uploadRoot: string | undefined): ToolR
|
|
|
679
1422
|
|
|
680
1423
|
const rootedFiles: string[] = [];
|
|
681
1424
|
for (const file of files) {
|
|
682
|
-
const rootedPath =
|
|
1425
|
+
const rootedPath = resolvePathWithinRoot(file, uploadRoot, "upload");
|
|
683
1426
|
if (!rootedPath.ok) {
|
|
684
1427
|
return rootedPath;
|
|
685
1428
|
}
|
|
@@ -707,26 +1450,46 @@ function normalizeDownloadPayload(
|
|
|
707
1450
|
}
|
|
708
1451
|
|
|
709
1452
|
const download = { ...rawDownload };
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
(typeof download.path === "string" && download.path.trim().length > 0
|
|
1453
|
+
const actualPath =
|
|
1454
|
+
typeof download.path === "string" && download.path.trim().length > 0
|
|
713
1455
|
? download.path.trim()
|
|
714
|
-
: undefined
|
|
715
|
-
if (
|
|
1456
|
+
: undefined;
|
|
1457
|
+
if (actualPath === undefined) {
|
|
716
1458
|
return createErr(
|
|
717
1459
|
ErrorCode.E_INTERNAL,
|
|
718
1460
|
"Driver download payload is missing path required for allowlist enforcement."
|
|
719
1461
|
);
|
|
720
1462
|
}
|
|
721
1463
|
|
|
722
|
-
const
|
|
723
|
-
if (!
|
|
724
|
-
return
|
|
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
|
+
}
|
|
725
1488
|
}
|
|
726
1489
|
|
|
727
1490
|
return createOk({
|
|
728
1491
|
...download,
|
|
729
|
-
path:
|
|
1492
|
+
path: rootedActualPath.data
|
|
730
1493
|
});
|
|
731
1494
|
}
|
|
732
1495
|
|
|
@@ -736,7 +1499,186 @@ function createBrowserdToolMap(
|
|
|
736
1499
|
defaultDriverKey: string,
|
|
737
1500
|
config: BrowserdConfig
|
|
738
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
|
+
|
|
739
1676
|
const toolMap = buildToolMap();
|
|
1677
|
+
const navigationMemoryStore = createNavigationMemoryStore({
|
|
1678
|
+
path: config.memoryPath,
|
|
1679
|
+
mode: config.memoryEnabled ? config.memoryMode : "off",
|
|
1680
|
+
ttlDays: config.memoryTtlDays
|
|
1681
|
+
});
|
|
740
1682
|
const allowedScopes = new Set<CapabilityScope>(config.authScopes);
|
|
741
1683
|
const runWithDefaultDriver = <TData>(
|
|
742
1684
|
args: ToolCallArgs,
|
|
@@ -799,36 +1741,242 @@ function createBrowserdToolMap(
|
|
|
799
1741
|
status
|
|
800
1742
|
});
|
|
801
1743
|
} catch (error) {
|
|
802
|
-
return mapDriverError(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);
|
|
803
1960
|
}
|
|
804
1961
|
});
|
|
805
1962
|
|
|
806
|
-
toolMap.set("browser.
|
|
807
|
-
const
|
|
808
|
-
if (!
|
|
809
|
-
return
|
|
1963
|
+
toolMap.set("browser.memory.ttl.set", async (args) => {
|
|
1964
|
+
const ttlDays = resolveOptionalIntegerArg(args, "ttlDays", 0);
|
|
1965
|
+
if (!ttlDays.ok) {
|
|
1966
|
+
return ttlDays;
|
|
810
1967
|
}
|
|
811
1968
|
|
|
812
|
-
if (
|
|
813
|
-
return createErr(ErrorCode.
|
|
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.");
|
|
814
1971
|
}
|
|
815
1972
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
return createOk(await navigationMemoryStore.setTtlDays(ttlDays.data));
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return mapNavigationMemoryError(error);
|
|
1977
|
+
}
|
|
820
1978
|
});
|
|
821
1979
|
|
|
822
|
-
toolMap.set("browser.profile.list", async (args) =>
|
|
823
|
-
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
824
|
-
const profiles = await driver.listProfiles();
|
|
825
|
-
return createOk({
|
|
826
|
-
driver: driverKey,
|
|
827
|
-
profiles
|
|
828
|
-
});
|
|
829
|
-
})
|
|
830
|
-
);
|
|
831
|
-
|
|
832
1980
|
toolMap.set("browser.tab.list", async (args) =>
|
|
833
1981
|
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
834
1982
|
const tabs = await driver.listTabs();
|
|
@@ -1270,7 +2418,7 @@ function createBrowserdToolMap(
|
|
|
1270
2418
|
return requestedPath;
|
|
1271
2419
|
}
|
|
1272
2420
|
|
|
1273
|
-
const download = await driver.waitDownload(targetId.data);
|
|
2421
|
+
const download = await driver.waitDownload(targetId.data, undefined, requestedPath.data);
|
|
1274
2422
|
const normalizedDownload = normalizeDownloadPayload(
|
|
1275
2423
|
download,
|
|
1276
2424
|
requestedPath.data,
|
|
@@ -1288,6 +2436,211 @@ function createBrowserdToolMap(
|
|
|
1288
2436
|
})
|
|
1289
2437
|
);
|
|
1290
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
|
+
|
|
1291
2644
|
toolMap.set("browser.console.list", async (args) =>
|
|
1292
2645
|
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1293
2646
|
const targetId = resolveTargetId(args, sessions);
|
|
@@ -1295,16 +2648,45 @@ function createBrowserdToolMap(
|
|
|
1295
2648
|
return targetId;
|
|
1296
2649
|
}
|
|
1297
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
|
+
|
|
1298
2671
|
const snapshot = await driver.snapshot(targetId.data);
|
|
1299
2672
|
if (!isTargetKnownSnapshot(snapshot)) {
|
|
1300
2673
|
return createErr(ErrorCode.E_NOT_FOUND, `Unknown targetId: ${targetId.data}`);
|
|
1301
2674
|
}
|
|
1302
2675
|
|
|
1303
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
|
+
);
|
|
1304
2686
|
return createOk({
|
|
1305
2687
|
driver: driverKey,
|
|
1306
2688
|
targetId: targetId.data,
|
|
1307
|
-
entries
|
|
2689
|
+
entries
|
|
1308
2690
|
});
|
|
1309
2691
|
}
|
|
1310
2692
|
|
|
@@ -1316,6 +2698,128 @@ function createBrowserdToolMap(
|
|
|
1316
2698
|
})
|
|
1317
2699
|
);
|
|
1318
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
|
+
|
|
1319
2823
|
toolMap.set("browser.network.waitFor", async (args) =>
|
|
1320
2824
|
runWithDefaultDriver(args, async (driver, driverKey) => {
|
|
1321
2825
|
const targetId = resolveTargetId(args, sessions);
|
|
@@ -1447,14 +2951,53 @@ function createBrowserdToolMap(
|
|
|
1447
2951
|
})
|
|
1448
2952
|
);
|
|
1449
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
|
+
|
|
1450
2973
|
for (const [toolName, handler] of [...toolMap.entries()]) {
|
|
1451
2974
|
toolMap.set(toolName, async (args) => {
|
|
2975
|
+
const startedAtMs = Date.now();
|
|
1452
2976
|
const authorization = authorizeToolCall(args, toolName, config.authToken, allowedScopes);
|
|
1453
2977
|
if (!authorization.ok) {
|
|
2978
|
+
const finishedAtMs = Date.now();
|
|
2979
|
+
recordTrace(args.sessionId, toolName, args, authorization, startedAtMs, finishedAtMs);
|
|
1454
2980
|
return authorization;
|
|
1455
2981
|
}
|
|
1456
2982
|
|
|
1457
|
-
|
|
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;
|
|
1458
3001
|
});
|
|
1459
3002
|
}
|
|
1460
3003
|
|
|
@@ -1462,11 +3005,23 @@ function createBrowserdToolMap(
|
|
|
1462
3005
|
}
|
|
1463
3006
|
|
|
1464
3007
|
export function createContainer(config: BrowserdConfig = loadBrowserdConfig()): BrowserdContainer {
|
|
1465
|
-
const sessions = new SessionStore(
|
|
3008
|
+
const sessions = new SessionStore({
|
|
3009
|
+
ttlMs: config.sessionTtlMs
|
|
3010
|
+
});
|
|
1466
3011
|
const driverRegistry = new DriverRegistry();
|
|
1467
3012
|
const drivers = new Map<string, BrowserDriver>();
|
|
1468
3013
|
const cleanupHandlers: Array<() => void> = [];
|
|
1469
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
|
+
|
|
1470
3025
|
function registerDriver(driverKey: string, driver: BrowserDriver): void {
|
|
1471
3026
|
driverRegistry.register(driverKey, driver);
|
|
1472
3027
|
drivers.set(driverKey, driver);
|