@agenticmail/enterprise 0.5.422 → 0.5.424

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.
@@ -0,0 +1,1136 @@
1
+ import {
2
+ createBrowserRouteContext
3
+ } from "./chunk-HHP5PRXW.js";
4
+ import {
5
+ registerBrowserRoutes
6
+ } from "./chunk-JGTRHXPL.js";
7
+ import {
8
+ resolveBrowserConfig,
9
+ resolveProfile
10
+ } from "./chunk-GKJU25QA.js";
11
+ import {
12
+ DEFAULT_AI_SNAPSHOT_MAX_CHARS,
13
+ ensureChromeExtensionRelayServer
14
+ } from "./chunk-PB4RGLMS.js";
15
+ import {
16
+ DEFAULT_UPLOAD_DIR,
17
+ createSubsystemLogger,
18
+ ensureGatewayStartupAuth,
19
+ escapeRegExp,
20
+ formatCliCommand,
21
+ loadConfig,
22
+ resolveGatewayAuth,
23
+ resolvePathsWithinRoot,
24
+ wrapExternalContent
25
+ } from "./chunk-A3PUJDNH.js";
26
+ import {
27
+ imageResultFromFile,
28
+ jsonResult,
29
+ readStringParam
30
+ } from "./chunk-ZB3VC2MR.js";
31
+ import "./chunk-KFQGP6VL.js";
32
+
33
+ // src/browser/client-actions-url.ts
34
+ function buildProfileQuery(profile) {
35
+ return profile ? `?profile=${encodeURIComponent(profile)}` : "";
36
+ }
37
+ function withBaseUrl(baseUrl, path) {
38
+ const trimmed = baseUrl?.trim();
39
+ if (!trimmed) {
40
+ return path;
41
+ }
42
+ return `${trimmed.replace(/\/$/, "")}${path}`;
43
+ }
44
+
45
+ // src/browser/bridge-auth-registry.ts
46
+ var authByPort = /* @__PURE__ */ new Map();
47
+ function getBridgeAuthForPort(port) {
48
+ if (!Number.isFinite(port) || port <= 0) {
49
+ return void 0;
50
+ }
51
+ return authByPort.get(port);
52
+ }
53
+
54
+ // src/browser/control-auth.ts
55
+ function resolveBrowserControlAuth(cfg, env = process.env) {
56
+ const auth = resolveGatewayAuth({
57
+ authConfig: cfg?.gateway?.auth,
58
+ env,
59
+ tailscaleMode: cfg?.gateway?.tailscale?.mode
60
+ });
61
+ const token = typeof auth?.token === "string" ? auth.token.trim() : "";
62
+ const password = typeof auth?.password === "string" ? auth.password.trim() : "";
63
+ return {
64
+ token: token || void 0,
65
+ password: password || void 0
66
+ };
67
+ }
68
+ function shouldAutoGenerateBrowserAuth(env) {
69
+ const nodeEnv = (env.NODE_ENV ?? "").trim().toLowerCase();
70
+ if (nodeEnv === "test") {
71
+ return false;
72
+ }
73
+ const vitest = (env.VITEST ?? "").trim().toLowerCase();
74
+ if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "off") {
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+ async function ensureBrowserControlAuth(params) {
80
+ const env = params.env ?? process.env;
81
+ const auth = resolveBrowserControlAuth(params.cfg, env);
82
+ if (auth.token || auth.password) {
83
+ return { auth };
84
+ }
85
+ if (!shouldAutoGenerateBrowserAuth(env)) {
86
+ return { auth };
87
+ }
88
+ if (params.cfg.gateway?.auth?.mode === "password") {
89
+ return { auth };
90
+ }
91
+ if (params.cfg.gateway?.auth?.mode === "none") {
92
+ return { auth };
93
+ }
94
+ if (params.cfg.gateway?.auth?.mode === "trusted-proxy") {
95
+ return { auth };
96
+ }
97
+ const latestCfg = loadConfig();
98
+ const latestAuth = resolveBrowserControlAuth(latestCfg, env);
99
+ if (latestAuth.token || latestAuth.password) {
100
+ return { auth: latestAuth };
101
+ }
102
+ if (latestCfg.gateway?.auth?.mode === "password") {
103
+ return { auth: latestAuth };
104
+ }
105
+ if (latestCfg.gateway?.auth?.mode === "none") {
106
+ return { auth: latestAuth };
107
+ }
108
+ if (latestCfg.gateway?.auth?.mode === "trusted-proxy") {
109
+ return { auth: latestAuth };
110
+ }
111
+ const ensured = await ensureGatewayStartupAuth({
112
+ cfg: latestCfg,
113
+ env,
114
+ persist: true
115
+ });
116
+ const ensuredAuth = resolveBrowserControlAuth(ensured.cfg, env);
117
+ return {
118
+ auth: ensuredAuth,
119
+ generatedToken: ensured.generatedToken
120
+ };
121
+ }
122
+
123
+ // src/browser/server-lifecycle.ts
124
+ async function ensureExtensionRelayForProfiles(params) {
125
+ for (const name of Object.keys(params.resolved.profiles)) {
126
+ const profile = resolveProfile(params.resolved, name);
127
+ if (!profile || profile.driver !== "extension") {
128
+ continue;
129
+ }
130
+ await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
131
+ params.onWarn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
132
+ });
133
+ }
134
+ }
135
+
136
+ // src/browser/control-service.ts
137
+ var state = null;
138
+ var log = createSubsystemLogger("browser");
139
+ var logService = log.child("service");
140
+ function createBrowserControlContext() {
141
+ return createBrowserRouteContext({
142
+ getState: () => state,
143
+ refreshConfigFromDisk: true
144
+ });
145
+ }
146
+ async function startBrowserControlServiceFromConfig() {
147
+ if (state) {
148
+ return state;
149
+ }
150
+ const cfg = loadConfig();
151
+ const resolved = resolveBrowserConfig(cfg.browser, cfg);
152
+ if (!resolved.enabled) {
153
+ return null;
154
+ }
155
+ try {
156
+ const ensured = await ensureBrowserControlAuth({ cfg });
157
+ if (ensured.generatedToken) {
158
+ logService.info("No browser auth configured; generated gateway.auth.token automatically.");
159
+ }
160
+ } catch (err) {
161
+ logService.warn(`failed to auto-configure browser auth: ${String(err)}`);
162
+ }
163
+ state = {
164
+ server: null,
165
+ port: resolved.controlPort,
166
+ resolved,
167
+ profiles: /* @__PURE__ */ new Map()
168
+ };
169
+ await ensureExtensionRelayForProfiles({
170
+ resolved,
171
+ onWarn: (message) => logService.warn(message)
172
+ });
173
+ logService.info(
174
+ `Browser control service ready (profiles=${Object.keys(resolved.profiles).length})`
175
+ );
176
+ return state;
177
+ }
178
+
179
+ // src/browser/routes/dispatcher.ts
180
+ function compileRoute(path) {
181
+ const paramNames = [];
182
+ const parts = path.split("/").map((part) => {
183
+ if (part.startsWith(":")) {
184
+ const name = part.slice(1);
185
+ paramNames.push(name);
186
+ return "([^/]+)";
187
+ }
188
+ return escapeRegExp(part);
189
+ });
190
+ return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
191
+ }
192
+ function createRegistry() {
193
+ const routes = [];
194
+ const register = (method) => (path, handler) => {
195
+ const { regex, paramNames } = compileRoute(path);
196
+ routes.push({ method, path, regex, paramNames, handler });
197
+ };
198
+ const router = {
199
+ get: register("GET"),
200
+ post: register("POST"),
201
+ delete: register("DELETE")
202
+ };
203
+ return { routes, router };
204
+ }
205
+ function normalizePath(path) {
206
+ if (!path) {
207
+ return "/";
208
+ }
209
+ return path.startsWith("/") ? path : `/${path}`;
210
+ }
211
+ function createBrowserRouteDispatcher(ctx) {
212
+ const registry = createRegistry();
213
+ registerBrowserRoutes(registry.router, ctx);
214
+ return {
215
+ dispatch: async (req) => {
216
+ const method = req.method;
217
+ const path = normalizePath(req.path);
218
+ const query = req.query ?? {};
219
+ const body = req.body;
220
+ const signal = req.signal;
221
+ const match = registry.routes.find((route) => {
222
+ if (route.method !== method) {
223
+ return false;
224
+ }
225
+ return route.regex.test(path);
226
+ });
227
+ if (!match) {
228
+ return { status: 404, body: { error: "Not Found" } };
229
+ }
230
+ const exec = match.regex.exec(path);
231
+ const params = {};
232
+ if (exec) {
233
+ for (const [idx, name] of match.paramNames.entries()) {
234
+ const value = exec[idx + 1];
235
+ if (typeof value === "string") {
236
+ params[name] = decodeURIComponent(value);
237
+ }
238
+ }
239
+ }
240
+ let status = 200;
241
+ let payload = void 0;
242
+ const res = {
243
+ status(code) {
244
+ status = code;
245
+ return res;
246
+ },
247
+ json(bodyValue) {
248
+ payload = bodyValue;
249
+ }
250
+ };
251
+ try {
252
+ await match.handler(
253
+ {
254
+ params,
255
+ query,
256
+ body,
257
+ signal
258
+ },
259
+ res
260
+ );
261
+ } catch (err) {
262
+ return { status: 500, body: { error: String(err) } };
263
+ }
264
+ return { status, body: payload };
265
+ }
266
+ };
267
+ }
268
+
269
+ // src/browser/client-fetch.ts
270
+ function isAbsoluteHttp(url) {
271
+ return /^https?:\/\//i.test(url.trim());
272
+ }
273
+ function isLoopbackHttpUrl(url) {
274
+ try {
275
+ const host = new URL(url).hostname.trim().toLowerCase();
276
+ const normalizedHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
277
+ return normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1";
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+ function withLoopbackBrowserAuthImpl(url, init, deps) {
283
+ const headers = new Headers(init?.headers ?? {});
284
+ if (headers.has("authorization") || headers.has("x-agenticmail-password")) {
285
+ return { ...init, headers };
286
+ }
287
+ if (!isLoopbackHttpUrl(url)) {
288
+ return { ...init, headers };
289
+ }
290
+ try {
291
+ const cfg = deps.loadConfig();
292
+ const auth = deps.resolveBrowserControlAuth(cfg);
293
+ if (auth.token) {
294
+ headers.set("Authorization", `Bearer ${auth.token}`);
295
+ return { ...init, headers };
296
+ }
297
+ if (auth.password) {
298
+ headers.set("x-agenticmail-password", auth.password);
299
+ return { ...init, headers };
300
+ }
301
+ } catch {
302
+ }
303
+ try {
304
+ const parsed = new URL(url);
305
+ const port = parsed.port && Number.parseInt(parsed.port, 10) > 0 ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80;
306
+ const bridgeAuth = deps.getBridgeAuthForPort(port);
307
+ if (bridgeAuth?.token) {
308
+ headers.set("Authorization", `Bearer ${bridgeAuth.token}`);
309
+ } else if (bridgeAuth?.password) {
310
+ headers.set("x-agenticmail-password", bridgeAuth.password);
311
+ }
312
+ } catch {
313
+ }
314
+ return { ...init, headers };
315
+ }
316
+ function withLoopbackBrowserAuth(url, init) {
317
+ return withLoopbackBrowserAuthImpl(url, init, {
318
+ loadConfig,
319
+ resolveBrowserControlAuth,
320
+ getBridgeAuthForPort
321
+ });
322
+ }
323
+ var consecutiveBrowserErrors = 0;
324
+ var MAX_TRANSIENT_ERRORS = 2;
325
+ function resetBrowserErrorCount() {
326
+ consecutiveBrowserErrors = 0;
327
+ }
328
+ function enhanceBrowserFetchError(url, err, timeoutMs) {
329
+ consecutiveBrowserErrors += 1;
330
+ const isLocal = !isAbsoluteHttp(url);
331
+ const msg = String(err);
332
+ const msgLower = msg.toLowerCase();
333
+ const looksLikeTimeout = msgLower.includes("timed out") || msgLower.includes("timeout") || msgLower.includes("aborted") || msgLower.includes("abort") || msgLower.includes("aborterror");
334
+ if (consecutiveBrowserErrors <= MAX_TRANSIENT_ERRORS) {
335
+ const retryHint = "This may be a transient browser connection issue. You can retry once \u2014 if it fails again, stop and inform the user.";
336
+ if (looksLikeTimeout) {
337
+ return new Error(
338
+ `Browser timed out after ${timeoutMs}ms. ${retryHint}`
339
+ );
340
+ }
341
+ return new Error(
342
+ `Browser connection error: ${msg}. ${retryHint}`
343
+ );
344
+ }
345
+ const operatorHint = isLocal ? `Restart the AgenticMail gateway (AgenticMail.app menubar, or \`${formatCliCommand("agenticmail gateway")}\`).` : "If this is a sandboxed session, ensure the sandbox browser is running.";
346
+ const modelHint = "Do NOT retry the browser tool \u2014 it will keep failing. Use an alternative approach or inform the user that the browser is currently unavailable.";
347
+ if (looksLikeTimeout) {
348
+ return new Error(
349
+ `Can't reach the AgenticMail browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`
350
+ );
351
+ }
352
+ return new Error(
353
+ `Can't reach the AgenticMail browser control service. ${operatorHint} ${modelHint} (${msg})`
354
+ );
355
+ }
356
+ async function fetchHttpJson(url, init) {
357
+ const timeoutMs = init.timeoutMs ?? 5e3;
358
+ const ctrl = new AbortController();
359
+ const upstreamSignal = init.signal;
360
+ let upstreamAbortListener;
361
+ if (upstreamSignal) {
362
+ if (upstreamSignal.aborted) {
363
+ ctrl.abort(upstreamSignal.reason);
364
+ } else {
365
+ upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
366
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
367
+ }
368
+ }
369
+ const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
370
+ try {
371
+ const res = await fetch(url, { ...init, signal: ctrl.signal });
372
+ if (!res.ok) {
373
+ const text = await res.text().catch(() => "");
374
+ throw new Error(text || `HTTP ${res.status}`);
375
+ }
376
+ return await res.json();
377
+ } finally {
378
+ clearTimeout(t);
379
+ if (upstreamSignal && upstreamAbortListener) {
380
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
381
+ }
382
+ }
383
+ }
384
+ function isOwnBrowserServer(url) {
385
+ if (!isAbsoluteHttp(url)) return false;
386
+ const ownPort = globalThis.__agenticmail_browser_port;
387
+ if (!ownPort) return false;
388
+ try {
389
+ const parsed = new URL(url);
390
+ const host = parsed.hostname.toLowerCase();
391
+ const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
392
+ return (host === "127.0.0.1" || host === "localhost" || host === "::1") && port === ownPort;
393
+ } catch {
394
+ return false;
395
+ }
396
+ }
397
+ async function fetchBrowserJson(url, init) {
398
+ const timeoutMs = init?.timeoutMs ?? 5e3;
399
+ try {
400
+ if (isAbsoluteHttp(url) && !isOwnBrowserServer(url)) {
401
+ const httpInit = withLoopbackBrowserAuth(url, init);
402
+ const result2 = await fetchHttpJson(url, { ...httpInit, timeoutMs });
403
+ resetBrowserErrorCount();
404
+ return result2;
405
+ }
406
+ const existingCtx = globalThis.__agenticmail_browser_ctx;
407
+ let dispatcher = globalThis.__agenticmail_browser_dispatcher;
408
+ if (!dispatcher) {
409
+ if (existingCtx) {
410
+ dispatcher = createBrowserRouteDispatcher(existingCtx);
411
+ } else {
412
+ const started = await startBrowserControlServiceFromConfig();
413
+ if (!started) {
414
+ throw new Error("browser control disabled");
415
+ }
416
+ dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
417
+ }
418
+ globalThis.__agenticmail_browser_dispatcher = dispatcher;
419
+ }
420
+ const parsed = new URL(url, "http://localhost");
421
+ const query = {};
422
+ for (const [key, value] of parsed.searchParams.entries()) {
423
+ query[key] = value;
424
+ }
425
+ let body = init?.body;
426
+ if (typeof body === "string") {
427
+ try {
428
+ body = JSON.parse(body);
429
+ } catch {
430
+ }
431
+ }
432
+ const abortCtrl = new AbortController();
433
+ const upstreamSignal = init?.signal;
434
+ let upstreamAbortListener;
435
+ if (upstreamSignal) {
436
+ if (upstreamSignal.aborted) {
437
+ abortCtrl.abort(upstreamSignal.reason);
438
+ } else {
439
+ upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
440
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
441
+ }
442
+ }
443
+ let abortListener;
444
+ const abortPromise = abortCtrl.signal.aborted ? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted")) : new Promise((_, reject) => {
445
+ abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
446
+ abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
447
+ });
448
+ let timer;
449
+ if (timeoutMs) {
450
+ timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
451
+ }
452
+ const dispatchPromise = dispatcher.dispatch({
453
+ method: init?.method?.toUpperCase() === "DELETE" ? "DELETE" : init?.method?.toUpperCase() === "POST" ? "POST" : "GET",
454
+ path: parsed.pathname,
455
+ query,
456
+ body,
457
+ signal: abortCtrl.signal
458
+ });
459
+ const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
460
+ if (timer) {
461
+ clearTimeout(timer);
462
+ }
463
+ if (abortListener) {
464
+ abortCtrl.signal.removeEventListener("abort", abortListener);
465
+ }
466
+ if (upstreamSignal && upstreamAbortListener) {
467
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
468
+ }
469
+ });
470
+ if (result.status >= 400) {
471
+ const message = result.body && typeof result.body === "object" && "error" in result.body ? String(result.body.error) : `HTTP ${result.status}`;
472
+ const err = new Error(message);
473
+ if (result.status < 500) {
474
+ err._browserValidation = true;
475
+ resetBrowserErrorCount();
476
+ }
477
+ throw err;
478
+ }
479
+ resetBrowserErrorCount();
480
+ return result.body;
481
+ } catch (err) {
482
+ if (err && typeof err === "object" && "_browserValidation" in err) {
483
+ throw err;
484
+ }
485
+ throw enhanceBrowserFetchError(url, err, timeoutMs);
486
+ }
487
+ }
488
+
489
+ // src/browser/client-actions-core.ts
490
+ async function browserNavigate(baseUrl, opts) {
491
+ const q = buildProfileQuery(opts.profile);
492
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/navigate${q}`), {
493
+ method: "POST",
494
+ headers: { "Content-Type": "application/json" },
495
+ body: JSON.stringify({ url: opts.url, targetId: opts.targetId }),
496
+ timeoutMs: 5e4
497
+ });
498
+ }
499
+ async function browserArmDialog(baseUrl, opts) {
500
+ const q = buildProfileQuery(opts.profile);
501
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/dialog${q}`), {
502
+ method: "POST",
503
+ headers: { "Content-Type": "application/json" },
504
+ body: JSON.stringify({
505
+ accept: opts.accept,
506
+ promptText: opts.promptText,
507
+ targetId: opts.targetId,
508
+ timeoutMs: opts.timeoutMs
509
+ }),
510
+ timeoutMs: 5e4
511
+ });
512
+ }
513
+ async function browserArmFileChooser(baseUrl, opts) {
514
+ const q = buildProfileQuery(opts.profile);
515
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/file-chooser${q}`), {
516
+ method: "POST",
517
+ headers: { "Content-Type": "application/json" },
518
+ body: JSON.stringify({
519
+ paths: opts.paths,
520
+ ref: opts.ref,
521
+ inputRef: opts.inputRef,
522
+ element: opts.element,
523
+ targetId: opts.targetId,
524
+ timeoutMs: opts.timeoutMs
525
+ }),
526
+ timeoutMs: 5e4
527
+ });
528
+ }
529
+ async function browserAct(baseUrl, req, opts) {
530
+ const q = buildProfileQuery(opts?.profile);
531
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/act${q}`), {
532
+ method: "POST",
533
+ headers: { "Content-Type": "application/json" },
534
+ body: JSON.stringify(req),
535
+ timeoutMs: 5e4
536
+ });
537
+ }
538
+ async function browserScreenshotAction(baseUrl, opts) {
539
+ const q = buildProfileQuery(opts.profile);
540
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/screenshot${q}`), {
541
+ method: "POST",
542
+ headers: { "Content-Type": "application/json" },
543
+ body: JSON.stringify({
544
+ targetId: opts.targetId,
545
+ fullPage: opts.fullPage,
546
+ ref: opts.ref,
547
+ element: opts.element,
548
+ type: opts.type
549
+ }),
550
+ timeoutMs: 5e4
551
+ });
552
+ }
553
+
554
+ // src/browser/client-actions-observe.ts
555
+ function buildQuerySuffix(params) {
556
+ const query = new URLSearchParams();
557
+ for (const [key, value] of params) {
558
+ if (typeof value === "boolean") {
559
+ query.set(key, String(value));
560
+ continue;
561
+ }
562
+ if (typeof value === "string" && value.length > 0) {
563
+ query.set(key, value);
564
+ }
565
+ }
566
+ const encoded = query.toString();
567
+ return encoded.length > 0 ? `?${encoded}` : "";
568
+ }
569
+ async function browserConsoleMessages(baseUrl, opts = {}) {
570
+ const suffix = buildQuerySuffix([
571
+ ["level", opts.level],
572
+ ["targetId", opts.targetId],
573
+ ["profile", opts.profile]
574
+ ]);
575
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/console${suffix}`), { timeoutMs: 2e4 });
576
+ }
577
+ async function browserPdfSave(baseUrl, opts = {}) {
578
+ const q = buildProfileQuery(opts.profile);
579
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/pdf${q}`), {
580
+ method: "POST",
581
+ headers: { "Content-Type": "application/json" },
582
+ body: JSON.stringify({ targetId: opts.targetId }),
583
+ timeoutMs: 2e4
584
+ });
585
+ }
586
+
587
+ // src/browser/client.ts
588
+ function buildProfileQuery2(profile) {
589
+ return profile ? `?profile=${encodeURIComponent(profile)}` : "";
590
+ }
591
+ function withBaseUrl2(baseUrl, path) {
592
+ const trimmed = baseUrl?.trim();
593
+ if (!trimmed) {
594
+ return path;
595
+ }
596
+ return `${trimmed.replace(/\/$/, "")}${path}`;
597
+ }
598
+ async function browserStatus(baseUrl, opts) {
599
+ const q = buildProfileQuery2(opts?.profile);
600
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/${q}`), {
601
+ timeoutMs: 1500
602
+ });
603
+ }
604
+ async function browserProfiles(baseUrl) {
605
+ const res = await fetchBrowserJson(
606
+ withBaseUrl2(baseUrl, `/profiles`),
607
+ {
608
+ timeoutMs: 3e3
609
+ }
610
+ );
611
+ return res.profiles ?? [];
612
+ }
613
+ async function browserStart(baseUrl, opts) {
614
+ const q = buildProfileQuery2(opts?.profile);
615
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/start${q}`), {
616
+ method: "POST",
617
+ timeoutMs: 15e3
618
+ });
619
+ }
620
+ async function browserStop(baseUrl, opts) {
621
+ const q = buildProfileQuery2(opts?.profile);
622
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/stop${q}`), {
623
+ method: "POST",
624
+ timeoutMs: 15e3
625
+ });
626
+ }
627
+ async function browserTabs(baseUrl, opts) {
628
+ const q = buildProfileQuery2(opts?.profile);
629
+ const res = await fetchBrowserJson(
630
+ withBaseUrl2(baseUrl, `/tabs${q}`),
631
+ { timeoutMs: 3e3 }
632
+ );
633
+ return res.tabs ?? [];
634
+ }
635
+ async function browserOpenTab(baseUrl, url, opts) {
636
+ const q = buildProfileQuery2(opts?.profile);
637
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/open${q}`), {
638
+ method: "POST",
639
+ headers: { "Content-Type": "application/json" },
640
+ body: JSON.stringify({ url }),
641
+ timeoutMs: 15e3
642
+ });
643
+ }
644
+ async function browserFocusTab(baseUrl, targetId, opts) {
645
+ const q = buildProfileQuery2(opts?.profile);
646
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/focus${q}`), {
647
+ method: "POST",
648
+ headers: { "Content-Type": "application/json" },
649
+ body: JSON.stringify({ targetId }),
650
+ timeoutMs: 5e3
651
+ });
652
+ }
653
+ async function browserCloseTab(baseUrl, targetId, opts) {
654
+ const q = buildProfileQuery2(opts?.profile);
655
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
656
+ method: "DELETE",
657
+ timeoutMs: 5e3
658
+ });
659
+ }
660
+ async function browserSnapshot(baseUrl, opts) {
661
+ const q = new URLSearchParams();
662
+ q.set("format", opts.format);
663
+ if (opts.targetId) {
664
+ q.set("targetId", opts.targetId);
665
+ }
666
+ if (typeof opts.limit === "number") {
667
+ q.set("limit", String(opts.limit));
668
+ }
669
+ if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
670
+ q.set("maxChars", String(opts.maxChars));
671
+ }
672
+ if (opts.refs === "aria" || opts.refs === "role") {
673
+ q.set("refs", opts.refs);
674
+ }
675
+ if (typeof opts.interactive === "boolean") {
676
+ q.set("interactive", String(opts.interactive));
677
+ }
678
+ if (typeof opts.compact === "boolean") {
679
+ q.set("compact", String(opts.compact));
680
+ }
681
+ if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) {
682
+ q.set("depth", String(opts.depth));
683
+ }
684
+ if (opts.selector?.trim()) {
685
+ q.set("selector", opts.selector.trim());
686
+ }
687
+ if (opts.frame?.trim()) {
688
+ q.set("frame", opts.frame.trim());
689
+ }
690
+ if (opts.labels === true) {
691
+ q.set("labels", "1");
692
+ }
693
+ if (opts.mode) {
694
+ q.set("mode", opts.mode);
695
+ }
696
+ if (opts.profile) {
697
+ q.set("profile", opts.profile);
698
+ }
699
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/snapshot?${q.toString()}`), {
700
+ timeoutMs: 5e4
701
+ });
702
+ }
703
+
704
+ // src/agent-tools/tools/browser-tool.schema.ts
705
+ import { Type as Type2 } from "@sinclair/typebox";
706
+
707
+ // src/agent-tools/schema/typebox.ts
708
+ import { Type } from "@sinclair/typebox";
709
+ function stringEnum(values, options = {}) {
710
+ return Type.Unsafe({
711
+ type: "string",
712
+ enum: [...values],
713
+ ...options
714
+ });
715
+ }
716
+ function optionalStringEnum(values, options = {}) {
717
+ return Type.Optional(stringEnum(values, options));
718
+ }
719
+
720
+ // src/agent-tools/tools/browser-tool.schema.ts
721
+ var BROWSER_ACT_KINDS = [
722
+ "click",
723
+ "type",
724
+ "press",
725
+ "hover",
726
+ "drag",
727
+ "select",
728
+ "fill",
729
+ "resize",
730
+ "wait",
731
+ "evaluate",
732
+ "close",
733
+ "mouse_click",
734
+ "scroll"
735
+ ];
736
+ var BROWSER_TOOL_ACTIONS = [
737
+ "status",
738
+ "start",
739
+ "stop",
740
+ "profiles",
741
+ "tabs",
742
+ "open",
743
+ "focus",
744
+ "close",
745
+ "snapshot",
746
+ "screenshot",
747
+ "navigate",
748
+ "console",
749
+ "pdf",
750
+ "upload",
751
+ "dialog",
752
+ "act"
753
+ ];
754
+ var BROWSER_TARGETS = ["sandbox", "host", "node"];
755
+ var BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"];
756
+ var BROWSER_SNAPSHOT_MODES = ["efficient"];
757
+ var BROWSER_SNAPSHOT_REFS = ["role", "aria"];
758
+ var BROWSER_IMAGE_TYPES = ["png", "jpeg"];
759
+ var BrowserActSchema = Type2.Object({
760
+ kind: stringEnum(BROWSER_ACT_KINDS),
761
+ // Common fields
762
+ targetId: Type2.Optional(Type2.String()),
763
+ ref: Type2.Optional(Type2.String()),
764
+ // click
765
+ doubleClick: Type2.Optional(Type2.Boolean()),
766
+ button: Type2.Optional(Type2.String()),
767
+ modifiers: Type2.Optional(Type2.Array(Type2.String())),
768
+ // type
769
+ text: Type2.Optional(Type2.String()),
770
+ submit: Type2.Optional(Type2.Boolean()),
771
+ slowly: Type2.Optional(Type2.Boolean()),
772
+ // press
773
+ key: Type2.Optional(Type2.String()),
774
+ // drag
775
+ startRef: Type2.Optional(Type2.String()),
776
+ endRef: Type2.Optional(Type2.String()),
777
+ // select
778
+ values: Type2.Optional(Type2.Array(Type2.String())),
779
+ // fill - use permissive array of objects
780
+ fields: Type2.Optional(Type2.Array(Type2.Object({}, { additionalProperties: true }))),
781
+ // resize
782
+ width: Type2.Optional(Type2.Number()),
783
+ height: Type2.Optional(Type2.Number()),
784
+ // wait
785
+ timeMs: Type2.Optional(Type2.Number()),
786
+ textGone: Type2.Optional(Type2.String()),
787
+ // evaluate
788
+ fn: Type2.Optional(Type2.String()),
789
+ // mouse_click (coordinate-based clicking — fallback for Shadow DOM)
790
+ x: Type2.Optional(Type2.Number()),
791
+ y: Type2.Optional(Type2.Number()),
792
+ // scroll
793
+ deltaX: Type2.Optional(Type2.Number()),
794
+ deltaY: Type2.Optional(Type2.Number())
795
+ });
796
+ var BrowserToolSchema = Type2.Object({
797
+ action: stringEnum(BROWSER_TOOL_ACTIONS),
798
+ target: optionalStringEnum(BROWSER_TARGETS),
799
+ node: Type2.Optional(Type2.String()),
800
+ profile: Type2.Optional(Type2.String()),
801
+ targetUrl: Type2.Optional(Type2.String()),
802
+ targetId: Type2.Optional(Type2.String()),
803
+ limit: Type2.Optional(Type2.Number()),
804
+ maxChars: Type2.Optional(Type2.Number()),
805
+ mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
806
+ snapshotFormat: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
807
+ refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
808
+ interactive: Type2.Optional(Type2.Boolean()),
809
+ compact: Type2.Optional(Type2.Boolean()),
810
+ depth: Type2.Optional(Type2.Number()),
811
+ selector: Type2.Optional(Type2.String()),
812
+ frame: Type2.Optional(Type2.String()),
813
+ labels: Type2.Optional(Type2.Boolean()),
814
+ fullPage: Type2.Optional(Type2.Boolean()),
815
+ ref: Type2.Optional(Type2.String()),
816
+ element: Type2.Optional(Type2.String()),
817
+ type: optionalStringEnum(BROWSER_IMAGE_TYPES),
818
+ level: Type2.Optional(Type2.String()),
819
+ paths: Type2.Optional(Type2.Array(Type2.String())),
820
+ inputRef: Type2.Optional(Type2.String()),
821
+ timeoutMs: Type2.Optional(Type2.Number()),
822
+ accept: Type2.Optional(Type2.Boolean()),
823
+ promptText: Type2.Optional(Type2.String()),
824
+ request: Type2.Optional(BrowserActSchema)
825
+ });
826
+
827
+ // src/agent-tools/tools/browser-tool.ts
828
+ function wrapBrowserExternalJson(params) {
829
+ const extractedText = JSON.stringify(params.payload, null, 2);
830
+ const wrappedText = wrapExternalContent(extractedText, {
831
+ source: "browser",
832
+ includeWarning: params.includeWarning ?? true
833
+ });
834
+ return {
835
+ wrappedText,
836
+ safeDetails: {
837
+ ok: true,
838
+ externalContent: {
839
+ untrusted: true,
840
+ source: "browser",
841
+ kind: params.kind,
842
+ wrapped: true
843
+ }
844
+ }
845
+ };
846
+ }
847
+ function createEnterpriseBrowserTool(config) {
848
+ const baseUrl = config?.baseUrl;
849
+ const defaultProfile = config?.defaultProfile;
850
+ return {
851
+ label: "Browser",
852
+ name: "browser",
853
+ description: [
854
+ "Control the browser for web automation \u2014 navigate, screenshot, snapshot (accessibility tree), click, type, hover, drag, fill forms, manage tabs, capture console logs, save PDFs, upload files, and handle dialogs.",
855
+ "Actions: status, start, stop, profiles, tabs, open, focus, close, snapshot, screenshot, navigate, console, pdf, upload, dialog, act.",
856
+ "Use snapshot+act for UI automation. snapshot returns the page accessibility tree; use refs from it with act to interact.",
857
+ 'snapshot format="ai" returns a text description; format="aria" returns structured nodes.',
858
+ "act supports: click, type, press, hover, drag, select, fill, resize, wait, evaluate, close, mouse_click, scroll.",
859
+ "mouse_click: coordinate-based clicking (x, y) \u2014 use when ref-based click fails on Shadow DOM/custom components. Take a screenshot first to identify coordinates.",
860
+ "scroll: scroll the page (deltaY positive=down, negative=up). Use to navigate long pages before taking snapshots.",
861
+ "IMPORTANT: Use open(targetUrl) to create NEW tabs for each different site/URL. Do NOT reuse the same tab for different sites \u2014 open a new tab, get its targetId, then use that targetId for all actions on that site.",
862
+ "Reddit URLs are auto-rewritten to old.reddit.com (avoids Shadow DOM issues).",
863
+ 'TWITTER/X RULES: (1) Non-Premium accounts have a 280 character limit per post/reply. ALWAYS keep tweets under 280 chars. Count carefully before posting. If your message is too long, shorten it \u2014 the Post/Reply button will be DISABLED if over the limit. (2) ALWAYS verify your post went through: after clicking Reply/Post, check for a "Your post was sent" confirmation alert or see your reply appear in the thread. If the button was disabled or no confirmation appeared, the post FAILED \u2014 shorten and retry. Never assume a post succeeded without verification. (3) For replies, navigate directly to the post URL (e.g. x.com/user/status/ID) instead of searching \u2014 this avoids loading the massive search results DOM.',
864
+ "TOKEN EFFICIENCY: Snapshots can be large. Use compact=true to reduce size. Use maxChars (e.g. 5000) to limit output. Use selector to snapshot only a specific part of the page (e.g. the reply box or a single tweet). Avoid taking full-page snapshots repeatedly \u2014 each one adds thousands of tokens to your context.",
865
+ "FALLBACK STRATEGY: If snapshot refs fail \u2192 try evaluate with document.querySelector(). If clicks fail \u2192 take screenshot, identify coordinates, use mouse_click(x, y). If page is too long \u2192 use scroll to navigate, then snapshot again."
866
+ ].join(" "),
867
+ parameters: BrowserToolSchema,
868
+ execute: async (_toolCallId, args) => {
869
+ const executeWithRetry = async () => {
870
+ try {
871
+ return await executeInner(args);
872
+ } catch (err) {
873
+ const msg = String(err?.message || err || "");
874
+ if (msg.includes("Can't reach") && !msg.includes("timed out")) {
875
+ await new Promise((r) => setTimeout(r, 1500));
876
+ return await executeInner(args);
877
+ }
878
+ throw err;
879
+ }
880
+ };
881
+ return await executeWithRetry();
882
+ }
883
+ };
884
+ async function executeInner(args) {
885
+ const params = args;
886
+ const action = readStringParam(params, "action", { required: true });
887
+ const profile = readStringParam(params, "profile") || defaultProfile;
888
+ switch (action) {
889
+ case "status":
890
+ return jsonResult(await browserStatus(baseUrl, { profile }));
891
+ case "start":
892
+ await browserStart(baseUrl, { profile });
893
+ return jsonResult(await browserStatus(baseUrl, { profile }));
894
+ case "stop":
895
+ await browserStop(baseUrl, { profile });
896
+ return jsonResult(await browserStatus(baseUrl, { profile }));
897
+ case "profiles":
898
+ return jsonResult({ profiles: await browserProfiles(baseUrl) });
899
+ case "tabs": {
900
+ const tabs = await browserTabs(baseUrl, { profile });
901
+ const wrapped = wrapBrowserExternalJson({
902
+ kind: "tabs",
903
+ payload: { tabs },
904
+ includeWarning: false
905
+ });
906
+ return {
907
+ content: [{ type: "text", text: wrapped.wrappedText }],
908
+ details: { ...wrapped.safeDetails, tabCount: tabs.length }
909
+ };
910
+ }
911
+ case "open": {
912
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
913
+ return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
914
+ }
915
+ case "focus": {
916
+ const targetId = readStringParam(params, "targetId", { required: true });
917
+ await browserFocusTab(baseUrl, targetId, { profile });
918
+ return jsonResult({ ok: true });
919
+ }
920
+ case "close": {
921
+ const targetId = readStringParam(params, "targetId");
922
+ if (targetId) {
923
+ await browserCloseTab(baseUrl, targetId, { profile });
924
+ } else {
925
+ await browserAct(baseUrl, { kind: "close" }, { profile });
926
+ }
927
+ return jsonResult({ ok: true });
928
+ }
929
+ case "snapshot": {
930
+ const format = params.snapshotFormat === "ai" || params.snapshotFormat === "aria" ? params.snapshotFormat : "ai";
931
+ const mode = params.mode === "efficient" ? "efficient" : void 0;
932
+ const labels = typeof params.labels === "boolean" ? params.labels : void 0;
933
+ const refs = params.refs === "aria" || params.refs === "role" ? params.refs : void 0;
934
+ const hasMaxChars = Object.hasOwn(params, "maxChars");
935
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
936
+ const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : void 0;
937
+ const maxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0 ? Math.floor(params.maxChars) : void 0;
938
+ const resolvedMaxChars = hasMaxChars ? maxChars : mode === "efficient" ? void 0 : DEFAULT_AI_SNAPSHOT_MAX_CHARS;
939
+ const interactive = typeof params.interactive === "boolean" ? params.interactive : void 0;
940
+ const compact = typeof params.compact === "boolean" ? params.compact : void 0;
941
+ const depth = typeof params.depth === "number" && Number.isFinite(params.depth) ? params.depth : void 0;
942
+ const selector = typeof params.selector === "string" ? params.selector.trim() : void 0;
943
+ const frame = typeof params.frame === "string" ? params.frame.trim() : void 0;
944
+ const snapshot = await browserSnapshot(baseUrl, {
945
+ format,
946
+ targetId,
947
+ limit,
948
+ ...typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {},
949
+ refs,
950
+ interactive,
951
+ compact,
952
+ depth,
953
+ selector,
954
+ frame,
955
+ labels,
956
+ mode,
957
+ profile
958
+ });
959
+ if (snapshot.format === "ai") {
960
+ const extractedText = snapshot.snapshot ?? "";
961
+ const wrappedSnapshot = wrapExternalContent(extractedText, {
962
+ source: "browser",
963
+ includeWarning: true
964
+ });
965
+ const safeDetails = {
966
+ ok: true,
967
+ format: snapshot.format,
968
+ targetId: snapshot.targetId,
969
+ url: snapshot.url,
970
+ truncated: snapshot.truncated,
971
+ stats: snapshot.stats,
972
+ refs: snapshot.refs ? Object.keys(snapshot.refs).length : void 0,
973
+ labels: snapshot.labels,
974
+ labelsCount: snapshot.labelsCount,
975
+ labelsSkipped: snapshot.labelsSkipped,
976
+ imagePath: snapshot.imagePath,
977
+ imageType: snapshot.imageType,
978
+ externalContent: {
979
+ untrusted: true,
980
+ source: "browser",
981
+ kind: "snapshot",
982
+ format: "ai",
983
+ wrapped: true
984
+ }
985
+ };
986
+ if (labels && snapshot.imagePath) {
987
+ return await imageResultFromFile({
988
+ label: "browser:snapshot",
989
+ path: snapshot.imagePath,
990
+ extraText: wrappedSnapshot,
991
+ details: safeDetails
992
+ });
993
+ }
994
+ return {
995
+ content: [{ type: "text", text: wrappedSnapshot }],
996
+ details: safeDetails
997
+ };
998
+ }
999
+ const snapshotAny = snapshot;
1000
+ const ariaTree = typeof snapshotAny.snapshot === "string" ? snapshotAny.snapshot : snapshot.nodes ? JSON.stringify(snapshot.nodes, null, 2) : JSON.stringify(snapshot, null, 2);
1001
+ const wrappedAria = wrapExternalContent(ariaTree, {
1002
+ source: "browser",
1003
+ includeWarning: true
1004
+ });
1005
+ return {
1006
+ content: [{ type: "text", text: wrappedAria }],
1007
+ details: {
1008
+ ok: true,
1009
+ format: "aria",
1010
+ targetId: snapshot.targetId,
1011
+ url: snapshot.url,
1012
+ nodeCount: snapshot.nodes?.length ?? 0,
1013
+ truncated: snapshotAny.truncated,
1014
+ externalContent: {
1015
+ untrusted: true,
1016
+ source: "browser",
1017
+ kind: "snapshot",
1018
+ format: "aria",
1019
+ wrapped: true
1020
+ }
1021
+ }
1022
+ };
1023
+ }
1024
+ case "screenshot": {
1025
+ const targetId = readStringParam(params, "targetId");
1026
+ const fullPage = Boolean(params.fullPage);
1027
+ const ref = readStringParam(params, "ref");
1028
+ const element = readStringParam(params, "element");
1029
+ const type = params.type === "jpeg" ? "jpeg" : "png";
1030
+ const result = await browserScreenshotAction(baseUrl, {
1031
+ targetId,
1032
+ fullPage,
1033
+ ref,
1034
+ element,
1035
+ type,
1036
+ profile
1037
+ });
1038
+ return await imageResultFromFile({
1039
+ label: "browser:screenshot",
1040
+ path: result.path,
1041
+ details: result
1042
+ });
1043
+ }
1044
+ case "navigate": {
1045
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
1046
+ const targetId = readStringParam(params, "targetId");
1047
+ return jsonResult(
1048
+ await browserNavigate(baseUrl, {
1049
+ url: targetUrl,
1050
+ targetId,
1051
+ profile
1052
+ })
1053
+ );
1054
+ }
1055
+ case "console": {
1056
+ const level = typeof params.level === "string" ? params.level.trim() : void 0;
1057
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1058
+ const result = await browserConsoleMessages(baseUrl, { level, targetId, profile });
1059
+ const wrapped = wrapBrowserExternalJson({
1060
+ kind: "console",
1061
+ payload: result,
1062
+ includeWarning: false
1063
+ });
1064
+ return {
1065
+ content: [{ type: "text", text: wrapped.wrappedText }],
1066
+ details: {
1067
+ ...wrapped.safeDetails,
1068
+ targetId: result.targetId,
1069
+ messageCount: result.messages.length
1070
+ }
1071
+ };
1072
+ }
1073
+ case "pdf": {
1074
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1075
+ const result = await browserPdfSave(baseUrl, { targetId, profile });
1076
+ return {
1077
+ content: [{ type: "text", text: `FILE:${result.path}` }],
1078
+ details: result
1079
+ };
1080
+ }
1081
+ case "upload": {
1082
+ const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
1083
+ if (paths.length === 0) throw new Error("paths required");
1084
+ const uploadDir = config?.uploadDir || DEFAULT_UPLOAD_DIR;
1085
+ const normalizedPaths = resolvePathsWithinRoot(uploadDir, ...paths);
1086
+ const ref = readStringParam(params, "ref");
1087
+ const inputRef = readStringParam(params, "inputRef");
1088
+ const element = readStringParam(params, "element");
1089
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1090
+ const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
1091
+ return jsonResult(
1092
+ await browserArmFileChooser(baseUrl, {
1093
+ paths: normalizedPaths,
1094
+ ref,
1095
+ inputRef,
1096
+ element,
1097
+ targetId,
1098
+ timeoutMs,
1099
+ profile
1100
+ })
1101
+ );
1102
+ }
1103
+ case "dialog": {
1104
+ const accept = Boolean(params.accept);
1105
+ const promptText = typeof params.promptText === "string" ? params.promptText : void 0;
1106
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1107
+ const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
1108
+ return jsonResult(
1109
+ await browserArmDialog(baseUrl, {
1110
+ accept,
1111
+ promptText,
1112
+ targetId,
1113
+ timeoutMs,
1114
+ profile
1115
+ })
1116
+ );
1117
+ }
1118
+ case "act": {
1119
+ const request = params.request;
1120
+ if (!request || typeof request !== "object") throw new Error("request required");
1121
+ if (request.kind === "evaluate" && config?.allowEvaluate === false) {
1122
+ throw new Error("JavaScript evaluation is disabled for this agent. Enable it in agent config.");
1123
+ }
1124
+ const result = await browserAct(baseUrl, request, {
1125
+ profile
1126
+ });
1127
+ return jsonResult(result);
1128
+ }
1129
+ default:
1130
+ throw new Error(`Unknown browser action: ${action}`);
1131
+ }
1132
+ }
1133
+ }
1134
+ export {
1135
+ createEnterpriseBrowserTool
1136
+ };