@agenticmail/enterprise 0.5.415 → 0.5.417

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,1094 @@
1
+ import {
2
+ createBrowserRouteContext
3
+ } from "./chunk-O367THP2.js";
4
+ import {
5
+ registerBrowserRoutes
6
+ } from "./chunk-QPUJ74AP.js";
7
+ import {
8
+ resolveBrowserConfig,
9
+ resolveProfile
10
+ } from "./chunk-BXAA7F6D.js";
11
+ import {
12
+ DEFAULT_AI_SNAPSHOT_MAX_CHARS,
13
+ ensureChromeExtensionRelayServer
14
+ } from "./chunk-2KUGKVK3.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
+ function enhanceBrowserFetchError(url, err, timeoutMs) {
324
+ const isLocal = !isAbsoluteHttp(url);
325
+ 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.";
326
+ 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.";
327
+ const msg = String(err);
328
+ const msgLower = msg.toLowerCase();
329
+ const looksLikeTimeout = msgLower.includes("timed out") || msgLower.includes("timeout") || msgLower.includes("aborted") || msgLower.includes("abort") || msgLower.includes("aborterror");
330
+ if (looksLikeTimeout) {
331
+ return new Error(
332
+ `Can't reach the AgenticMail browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`
333
+ );
334
+ }
335
+ return new Error(
336
+ `Can't reach the AgenticMail browser control service. ${operatorHint} ${modelHint} (${msg})`
337
+ );
338
+ }
339
+ async function fetchHttpJson(url, init) {
340
+ const timeoutMs = init.timeoutMs ?? 5e3;
341
+ const ctrl = new AbortController();
342
+ const upstreamSignal = init.signal;
343
+ let upstreamAbortListener;
344
+ if (upstreamSignal) {
345
+ if (upstreamSignal.aborted) {
346
+ ctrl.abort(upstreamSignal.reason);
347
+ } else {
348
+ upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
349
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
350
+ }
351
+ }
352
+ const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
353
+ try {
354
+ const res = await fetch(url, { ...init, signal: ctrl.signal });
355
+ if (!res.ok) {
356
+ const text = await res.text().catch(() => "");
357
+ throw new Error(text || `HTTP ${res.status}`);
358
+ }
359
+ return await res.json();
360
+ } finally {
361
+ clearTimeout(t);
362
+ if (upstreamSignal && upstreamAbortListener) {
363
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
364
+ }
365
+ }
366
+ }
367
+ function isOwnBrowserServer(url) {
368
+ if (!isAbsoluteHttp(url)) return false;
369
+ const ownPort = globalThis.__agenticmail_browser_port;
370
+ if (!ownPort) return false;
371
+ try {
372
+ const parsed = new URL(url);
373
+ const host = parsed.hostname.toLowerCase();
374
+ const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
375
+ return (host === "127.0.0.1" || host === "localhost" || host === "::1") && port === ownPort;
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+ async function fetchBrowserJson(url, init) {
381
+ const timeoutMs = init?.timeoutMs ?? 5e3;
382
+ try {
383
+ if (isAbsoluteHttp(url) && !isOwnBrowserServer(url)) {
384
+ const httpInit = withLoopbackBrowserAuth(url, init);
385
+ return await fetchHttpJson(url, { ...httpInit, timeoutMs });
386
+ }
387
+ const existingCtx = globalThis.__agenticmail_browser_ctx;
388
+ let dispatcher;
389
+ if (existingCtx) {
390
+ dispatcher = createBrowserRouteDispatcher(existingCtx);
391
+ } else {
392
+ const started = await startBrowserControlServiceFromConfig();
393
+ if (!started) {
394
+ throw new Error("browser control disabled");
395
+ }
396
+ dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
397
+ }
398
+ const parsed = new URL(url, "http://localhost");
399
+ const query = {};
400
+ for (const [key, value] of parsed.searchParams.entries()) {
401
+ query[key] = value;
402
+ }
403
+ let body = init?.body;
404
+ if (typeof body === "string") {
405
+ try {
406
+ body = JSON.parse(body);
407
+ } catch {
408
+ }
409
+ }
410
+ const abortCtrl = new AbortController();
411
+ const upstreamSignal = init?.signal;
412
+ let upstreamAbortListener;
413
+ if (upstreamSignal) {
414
+ if (upstreamSignal.aborted) {
415
+ abortCtrl.abort(upstreamSignal.reason);
416
+ } else {
417
+ upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
418
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
419
+ }
420
+ }
421
+ let abortListener;
422
+ const abortPromise = abortCtrl.signal.aborted ? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted")) : new Promise((_, reject) => {
423
+ abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
424
+ abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
425
+ });
426
+ let timer;
427
+ if (timeoutMs) {
428
+ timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
429
+ }
430
+ const dispatchPromise = dispatcher.dispatch({
431
+ method: init?.method?.toUpperCase() === "DELETE" ? "DELETE" : init?.method?.toUpperCase() === "POST" ? "POST" : "GET",
432
+ path: parsed.pathname,
433
+ query,
434
+ body,
435
+ signal: abortCtrl.signal
436
+ });
437
+ const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
438
+ if (timer) {
439
+ clearTimeout(timer);
440
+ }
441
+ if (abortListener) {
442
+ abortCtrl.signal.removeEventListener("abort", abortListener);
443
+ }
444
+ if (upstreamSignal && upstreamAbortListener) {
445
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
446
+ }
447
+ });
448
+ if (result.status >= 400) {
449
+ const message = result.body && typeof result.body === "object" && "error" in result.body ? String(result.body.error) : `HTTP ${result.status}`;
450
+ throw new Error(message);
451
+ }
452
+ return result.body;
453
+ } catch (err) {
454
+ throw enhanceBrowserFetchError(url, err, timeoutMs);
455
+ }
456
+ }
457
+
458
+ // src/browser/client-actions-core.ts
459
+ async function browserNavigate(baseUrl, opts) {
460
+ const q = buildProfileQuery(opts.profile);
461
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/navigate${q}`), {
462
+ method: "POST",
463
+ headers: { "Content-Type": "application/json" },
464
+ body: JSON.stringify({ url: opts.url, targetId: opts.targetId }),
465
+ timeoutMs: 45e3
466
+ });
467
+ }
468
+ async function browserArmDialog(baseUrl, opts) {
469
+ const q = buildProfileQuery(opts.profile);
470
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/dialog${q}`), {
471
+ method: "POST",
472
+ headers: { "Content-Type": "application/json" },
473
+ body: JSON.stringify({
474
+ accept: opts.accept,
475
+ promptText: opts.promptText,
476
+ targetId: opts.targetId,
477
+ timeoutMs: opts.timeoutMs
478
+ }),
479
+ timeoutMs: 45e3
480
+ });
481
+ }
482
+ async function browserArmFileChooser(baseUrl, opts) {
483
+ const q = buildProfileQuery(opts.profile);
484
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/file-chooser${q}`), {
485
+ method: "POST",
486
+ headers: { "Content-Type": "application/json" },
487
+ body: JSON.stringify({
488
+ paths: opts.paths,
489
+ ref: opts.ref,
490
+ inputRef: opts.inputRef,
491
+ element: opts.element,
492
+ targetId: opts.targetId,
493
+ timeoutMs: opts.timeoutMs
494
+ }),
495
+ timeoutMs: 45e3
496
+ });
497
+ }
498
+ async function browserAct(baseUrl, req, opts) {
499
+ const q = buildProfileQuery(opts?.profile);
500
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/act${q}`), {
501
+ method: "POST",
502
+ headers: { "Content-Type": "application/json" },
503
+ body: JSON.stringify(req),
504
+ timeoutMs: 45e3
505
+ });
506
+ }
507
+ async function browserScreenshotAction(baseUrl, opts) {
508
+ const q = buildProfileQuery(opts.profile);
509
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/screenshot${q}`), {
510
+ method: "POST",
511
+ headers: { "Content-Type": "application/json" },
512
+ body: JSON.stringify({
513
+ targetId: opts.targetId,
514
+ fullPage: opts.fullPage,
515
+ ref: opts.ref,
516
+ element: opts.element,
517
+ type: opts.type
518
+ }),
519
+ timeoutMs: 45e3
520
+ });
521
+ }
522
+
523
+ // src/browser/client-actions-observe.ts
524
+ function buildQuerySuffix(params) {
525
+ const query = new URLSearchParams();
526
+ for (const [key, value] of params) {
527
+ if (typeof value === "boolean") {
528
+ query.set(key, String(value));
529
+ continue;
530
+ }
531
+ if (typeof value === "string" && value.length > 0) {
532
+ query.set(key, value);
533
+ }
534
+ }
535
+ const encoded = query.toString();
536
+ return encoded.length > 0 ? `?${encoded}` : "";
537
+ }
538
+ async function browserConsoleMessages(baseUrl, opts = {}) {
539
+ const suffix = buildQuerySuffix([
540
+ ["level", opts.level],
541
+ ["targetId", opts.targetId],
542
+ ["profile", opts.profile]
543
+ ]);
544
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/console${suffix}`), { timeoutMs: 2e4 });
545
+ }
546
+ async function browserPdfSave(baseUrl, opts = {}) {
547
+ const q = buildProfileQuery(opts.profile);
548
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/pdf${q}`), {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({ targetId: opts.targetId }),
552
+ timeoutMs: 2e4
553
+ });
554
+ }
555
+
556
+ // src/browser/client.ts
557
+ function buildProfileQuery2(profile) {
558
+ return profile ? `?profile=${encodeURIComponent(profile)}` : "";
559
+ }
560
+ function withBaseUrl2(baseUrl, path) {
561
+ const trimmed = baseUrl?.trim();
562
+ if (!trimmed) {
563
+ return path;
564
+ }
565
+ return `${trimmed.replace(/\/$/, "")}${path}`;
566
+ }
567
+ async function browserStatus(baseUrl, opts) {
568
+ const q = buildProfileQuery2(opts?.profile);
569
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/${q}`), {
570
+ timeoutMs: 1500
571
+ });
572
+ }
573
+ async function browserProfiles(baseUrl) {
574
+ const res = await fetchBrowserJson(
575
+ withBaseUrl2(baseUrl, `/profiles`),
576
+ {
577
+ timeoutMs: 3e3
578
+ }
579
+ );
580
+ return res.profiles ?? [];
581
+ }
582
+ async function browserStart(baseUrl, opts) {
583
+ const q = buildProfileQuery2(opts?.profile);
584
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/start${q}`), {
585
+ method: "POST",
586
+ timeoutMs: 15e3
587
+ });
588
+ }
589
+ async function browserStop(baseUrl, opts) {
590
+ const q = buildProfileQuery2(opts?.profile);
591
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/stop${q}`), {
592
+ method: "POST",
593
+ timeoutMs: 15e3
594
+ });
595
+ }
596
+ async function browserTabs(baseUrl, opts) {
597
+ const q = buildProfileQuery2(opts?.profile);
598
+ const res = await fetchBrowserJson(
599
+ withBaseUrl2(baseUrl, `/tabs${q}`),
600
+ { timeoutMs: 3e3 }
601
+ );
602
+ return res.tabs ?? [];
603
+ }
604
+ async function browserOpenTab(baseUrl, url, opts) {
605
+ const q = buildProfileQuery2(opts?.profile);
606
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/open${q}`), {
607
+ method: "POST",
608
+ headers: { "Content-Type": "application/json" },
609
+ body: JSON.stringify({ url }),
610
+ timeoutMs: 15e3
611
+ });
612
+ }
613
+ async function browserFocusTab(baseUrl, targetId, opts) {
614
+ const q = buildProfileQuery2(opts?.profile);
615
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/focus${q}`), {
616
+ method: "POST",
617
+ headers: { "Content-Type": "application/json" },
618
+ body: JSON.stringify({ targetId }),
619
+ timeoutMs: 5e3
620
+ });
621
+ }
622
+ async function browserCloseTab(baseUrl, targetId, opts) {
623
+ const q = buildProfileQuery2(opts?.profile);
624
+ await fetchBrowserJson(withBaseUrl2(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
625
+ method: "DELETE",
626
+ timeoutMs: 5e3
627
+ });
628
+ }
629
+ async function browserSnapshot(baseUrl, opts) {
630
+ const q = new URLSearchParams();
631
+ q.set("format", opts.format);
632
+ if (opts.targetId) {
633
+ q.set("targetId", opts.targetId);
634
+ }
635
+ if (typeof opts.limit === "number") {
636
+ q.set("limit", String(opts.limit));
637
+ }
638
+ if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
639
+ q.set("maxChars", String(opts.maxChars));
640
+ }
641
+ if (opts.refs === "aria" || opts.refs === "role") {
642
+ q.set("refs", opts.refs);
643
+ }
644
+ if (typeof opts.interactive === "boolean") {
645
+ q.set("interactive", String(opts.interactive));
646
+ }
647
+ if (typeof opts.compact === "boolean") {
648
+ q.set("compact", String(opts.compact));
649
+ }
650
+ if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) {
651
+ q.set("depth", String(opts.depth));
652
+ }
653
+ if (opts.selector?.trim()) {
654
+ q.set("selector", opts.selector.trim());
655
+ }
656
+ if (opts.frame?.trim()) {
657
+ q.set("frame", opts.frame.trim());
658
+ }
659
+ if (opts.labels === true) {
660
+ q.set("labels", "1");
661
+ }
662
+ if (opts.mode) {
663
+ q.set("mode", opts.mode);
664
+ }
665
+ if (opts.profile) {
666
+ q.set("profile", opts.profile);
667
+ }
668
+ return await fetchBrowserJson(withBaseUrl2(baseUrl, `/snapshot?${q.toString()}`), {
669
+ timeoutMs: 45e3
670
+ });
671
+ }
672
+
673
+ // src/agent-tools/tools/browser-tool.schema.ts
674
+ import { Type as Type2 } from "@sinclair/typebox";
675
+
676
+ // src/agent-tools/schema/typebox.ts
677
+ import { Type } from "@sinclair/typebox";
678
+ function stringEnum(values, options = {}) {
679
+ return Type.Unsafe({
680
+ type: "string",
681
+ enum: [...values],
682
+ ...options
683
+ });
684
+ }
685
+ function optionalStringEnum(values, options = {}) {
686
+ return Type.Optional(stringEnum(values, options));
687
+ }
688
+
689
+ // src/agent-tools/tools/browser-tool.schema.ts
690
+ var BROWSER_ACT_KINDS = [
691
+ "click",
692
+ "type",
693
+ "press",
694
+ "hover",
695
+ "drag",
696
+ "select",
697
+ "fill",
698
+ "resize",
699
+ "wait",
700
+ "evaluate",
701
+ "close",
702
+ "mouse_click",
703
+ "scroll"
704
+ ];
705
+ var BROWSER_TOOL_ACTIONS = [
706
+ "status",
707
+ "start",
708
+ "stop",
709
+ "profiles",
710
+ "tabs",
711
+ "open",
712
+ "focus",
713
+ "close",
714
+ "snapshot",
715
+ "screenshot",
716
+ "navigate",
717
+ "console",
718
+ "pdf",
719
+ "upload",
720
+ "dialog",
721
+ "act"
722
+ ];
723
+ var BROWSER_TARGETS = ["sandbox", "host", "node"];
724
+ var BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"];
725
+ var BROWSER_SNAPSHOT_MODES = ["efficient"];
726
+ var BROWSER_SNAPSHOT_REFS = ["role", "aria"];
727
+ var BROWSER_IMAGE_TYPES = ["png", "jpeg"];
728
+ var BrowserActSchema = Type2.Object({
729
+ kind: stringEnum(BROWSER_ACT_KINDS),
730
+ // Common fields
731
+ targetId: Type2.Optional(Type2.String()),
732
+ ref: Type2.Optional(Type2.String()),
733
+ // click
734
+ doubleClick: Type2.Optional(Type2.Boolean()),
735
+ button: Type2.Optional(Type2.String()),
736
+ modifiers: Type2.Optional(Type2.Array(Type2.String())),
737
+ // type
738
+ text: Type2.Optional(Type2.String()),
739
+ submit: Type2.Optional(Type2.Boolean()),
740
+ slowly: Type2.Optional(Type2.Boolean()),
741
+ // press
742
+ key: Type2.Optional(Type2.String()),
743
+ // drag
744
+ startRef: Type2.Optional(Type2.String()),
745
+ endRef: Type2.Optional(Type2.String()),
746
+ // select
747
+ values: Type2.Optional(Type2.Array(Type2.String())),
748
+ // fill - use permissive array of objects
749
+ fields: Type2.Optional(Type2.Array(Type2.Object({}, { additionalProperties: true }))),
750
+ // resize
751
+ width: Type2.Optional(Type2.Number()),
752
+ height: Type2.Optional(Type2.Number()),
753
+ // wait
754
+ timeMs: Type2.Optional(Type2.Number()),
755
+ textGone: Type2.Optional(Type2.String()),
756
+ // evaluate
757
+ fn: Type2.Optional(Type2.String()),
758
+ // mouse_click (coordinate-based clicking — fallback for Shadow DOM)
759
+ x: Type2.Optional(Type2.Number()),
760
+ y: Type2.Optional(Type2.Number()),
761
+ // scroll
762
+ deltaX: Type2.Optional(Type2.Number()),
763
+ deltaY: Type2.Optional(Type2.Number())
764
+ });
765
+ var BrowserToolSchema = Type2.Object({
766
+ action: stringEnum(BROWSER_TOOL_ACTIONS),
767
+ target: optionalStringEnum(BROWSER_TARGETS),
768
+ node: Type2.Optional(Type2.String()),
769
+ profile: Type2.Optional(Type2.String()),
770
+ targetUrl: Type2.Optional(Type2.String()),
771
+ targetId: Type2.Optional(Type2.String()),
772
+ limit: Type2.Optional(Type2.Number()),
773
+ maxChars: Type2.Optional(Type2.Number()),
774
+ mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
775
+ snapshotFormat: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
776
+ refs: optionalStringEnum(BROWSER_SNAPSHOT_REFS),
777
+ interactive: Type2.Optional(Type2.Boolean()),
778
+ compact: Type2.Optional(Type2.Boolean()),
779
+ depth: Type2.Optional(Type2.Number()),
780
+ selector: Type2.Optional(Type2.String()),
781
+ frame: Type2.Optional(Type2.String()),
782
+ labels: Type2.Optional(Type2.Boolean()),
783
+ fullPage: Type2.Optional(Type2.Boolean()),
784
+ ref: Type2.Optional(Type2.String()),
785
+ element: Type2.Optional(Type2.String()),
786
+ type: optionalStringEnum(BROWSER_IMAGE_TYPES),
787
+ level: Type2.Optional(Type2.String()),
788
+ paths: Type2.Optional(Type2.Array(Type2.String())),
789
+ inputRef: Type2.Optional(Type2.String()),
790
+ timeoutMs: Type2.Optional(Type2.Number()),
791
+ accept: Type2.Optional(Type2.Boolean()),
792
+ promptText: Type2.Optional(Type2.String()),
793
+ request: Type2.Optional(BrowserActSchema)
794
+ });
795
+
796
+ // src/agent-tools/tools/browser-tool.ts
797
+ function wrapBrowserExternalJson(params) {
798
+ const extractedText = JSON.stringify(params.payload, null, 2);
799
+ const wrappedText = wrapExternalContent(extractedText, {
800
+ source: "browser",
801
+ includeWarning: params.includeWarning ?? true
802
+ });
803
+ return {
804
+ wrappedText,
805
+ safeDetails: {
806
+ ok: true,
807
+ externalContent: {
808
+ untrusted: true,
809
+ source: "browser",
810
+ kind: params.kind,
811
+ wrapped: true
812
+ }
813
+ }
814
+ };
815
+ }
816
+ function createEnterpriseBrowserTool(config) {
817
+ const baseUrl = config?.baseUrl;
818
+ const defaultProfile = config?.defaultProfile;
819
+ return {
820
+ label: "Browser",
821
+ name: "browser",
822
+ description: [
823
+ "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.",
824
+ "Actions: status, start, stop, profiles, tabs, open, focus, close, snapshot, screenshot, navigate, console, pdf, upload, dialog, act.",
825
+ "Use snapshot+act for UI automation. snapshot returns the page accessibility tree; use refs from it with act to interact.",
826
+ 'snapshot format="ai" returns a text description; format="aria" returns structured nodes.',
827
+ "act supports: click, type, press, hover, drag, select, fill, resize, wait, evaluate, close, mouse_click, scroll.",
828
+ "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.",
829
+ "scroll: scroll the page (deltaY positive=down, negative=up). Use to navigate long pages before taking snapshots.",
830
+ "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.",
831
+ "Reddit URLs are auto-rewritten to old.reddit.com (avoids Shadow DOM issues).",
832
+ '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.',
833
+ "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."
834
+ ].join(" "),
835
+ parameters: BrowserToolSchema,
836
+ execute: async (_toolCallId, args) => {
837
+ const executeWithRetry = async () => {
838
+ try {
839
+ return await executeInner(args);
840
+ } catch (err) {
841
+ const msg = String(err?.message || err || "");
842
+ if (msg.includes("Can't reach") && !msg.includes("timed out")) {
843
+ await new Promise((r) => setTimeout(r, 1500));
844
+ return await executeInner(args);
845
+ }
846
+ throw err;
847
+ }
848
+ };
849
+ return await executeWithRetry();
850
+ }
851
+ };
852
+ async function executeInner(args) {
853
+ const params = args;
854
+ const action = readStringParam(params, "action", { required: true });
855
+ const profile = readStringParam(params, "profile") || defaultProfile;
856
+ switch (action) {
857
+ case "status":
858
+ return jsonResult(await browserStatus(baseUrl, { profile }));
859
+ case "start":
860
+ await browserStart(baseUrl, { profile });
861
+ return jsonResult(await browserStatus(baseUrl, { profile }));
862
+ case "stop":
863
+ await browserStop(baseUrl, { profile });
864
+ return jsonResult(await browserStatus(baseUrl, { profile }));
865
+ case "profiles":
866
+ return jsonResult({ profiles: await browserProfiles(baseUrl) });
867
+ case "tabs": {
868
+ const tabs = await browserTabs(baseUrl, { profile });
869
+ const wrapped = wrapBrowserExternalJson({
870
+ kind: "tabs",
871
+ payload: { tabs },
872
+ includeWarning: false
873
+ });
874
+ return {
875
+ content: [{ type: "text", text: wrapped.wrappedText }],
876
+ details: { ...wrapped.safeDetails, tabCount: tabs.length }
877
+ };
878
+ }
879
+ case "open": {
880
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
881
+ return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
882
+ }
883
+ case "focus": {
884
+ const targetId = readStringParam(params, "targetId", { required: true });
885
+ await browserFocusTab(baseUrl, targetId, { profile });
886
+ return jsonResult({ ok: true });
887
+ }
888
+ case "close": {
889
+ const targetId = readStringParam(params, "targetId");
890
+ if (targetId) {
891
+ await browserCloseTab(baseUrl, targetId, { profile });
892
+ } else {
893
+ await browserAct(baseUrl, { kind: "close" }, { profile });
894
+ }
895
+ return jsonResult({ ok: true });
896
+ }
897
+ case "snapshot": {
898
+ const format = params.snapshotFormat === "ai" || params.snapshotFormat === "aria" ? params.snapshotFormat : "ai";
899
+ const mode = params.mode === "efficient" ? "efficient" : void 0;
900
+ const labels = typeof params.labels === "boolean" ? params.labels : void 0;
901
+ const refs = params.refs === "aria" || params.refs === "role" ? params.refs : void 0;
902
+ const hasMaxChars = Object.hasOwn(params, "maxChars");
903
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
904
+ const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : void 0;
905
+ const maxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0 ? Math.floor(params.maxChars) : void 0;
906
+ const resolvedMaxChars = format === "ai" ? hasMaxChars ? maxChars : mode === "efficient" ? void 0 : DEFAULT_AI_SNAPSHOT_MAX_CHARS : void 0;
907
+ const interactive = typeof params.interactive === "boolean" ? params.interactive : void 0;
908
+ const compact = typeof params.compact === "boolean" ? params.compact : void 0;
909
+ const depth = typeof params.depth === "number" && Number.isFinite(params.depth) ? params.depth : void 0;
910
+ const selector = typeof params.selector === "string" ? params.selector.trim() : void 0;
911
+ const frame = typeof params.frame === "string" ? params.frame.trim() : void 0;
912
+ const snapshot = await browserSnapshot(baseUrl, {
913
+ format,
914
+ targetId,
915
+ limit,
916
+ ...typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {},
917
+ refs,
918
+ interactive,
919
+ compact,
920
+ depth,
921
+ selector,
922
+ frame,
923
+ labels,
924
+ mode,
925
+ profile
926
+ });
927
+ if (snapshot.format === "ai") {
928
+ const extractedText = snapshot.snapshot ?? "";
929
+ const wrappedSnapshot = wrapExternalContent(extractedText, {
930
+ source: "browser",
931
+ includeWarning: true
932
+ });
933
+ const safeDetails = {
934
+ ok: true,
935
+ format: snapshot.format,
936
+ targetId: snapshot.targetId,
937
+ url: snapshot.url,
938
+ truncated: snapshot.truncated,
939
+ stats: snapshot.stats,
940
+ refs: snapshot.refs ? Object.keys(snapshot.refs).length : void 0,
941
+ labels: snapshot.labels,
942
+ labelsCount: snapshot.labelsCount,
943
+ labelsSkipped: snapshot.labelsSkipped,
944
+ imagePath: snapshot.imagePath,
945
+ imageType: snapshot.imageType,
946
+ externalContent: {
947
+ untrusted: true,
948
+ source: "browser",
949
+ kind: "snapshot",
950
+ format: "ai",
951
+ wrapped: true
952
+ }
953
+ };
954
+ if (labels && snapshot.imagePath) {
955
+ return await imageResultFromFile({
956
+ label: "browser:snapshot",
957
+ path: snapshot.imagePath,
958
+ extraText: wrappedSnapshot,
959
+ details: safeDetails
960
+ });
961
+ }
962
+ return {
963
+ content: [{ type: "text", text: wrappedSnapshot }],
964
+ details: safeDetails
965
+ };
966
+ }
967
+ const wrapped = wrapBrowserExternalJson({
968
+ kind: "snapshot",
969
+ payload: snapshot
970
+ });
971
+ return {
972
+ content: [{ type: "text", text: wrapped.wrappedText }],
973
+ details: {
974
+ ...wrapped.safeDetails,
975
+ format: "aria",
976
+ targetId: snapshot.targetId,
977
+ url: snapshot.url,
978
+ nodeCount: snapshot.nodes.length
979
+ }
980
+ };
981
+ }
982
+ case "screenshot": {
983
+ const targetId = readStringParam(params, "targetId");
984
+ const fullPage = Boolean(params.fullPage);
985
+ const ref = readStringParam(params, "ref");
986
+ const element = readStringParam(params, "element");
987
+ const type = params.type === "jpeg" ? "jpeg" : "png";
988
+ const result = await browserScreenshotAction(baseUrl, {
989
+ targetId,
990
+ fullPage,
991
+ ref,
992
+ element,
993
+ type,
994
+ profile
995
+ });
996
+ return await imageResultFromFile({
997
+ label: "browser:screenshot",
998
+ path: result.path,
999
+ details: result
1000
+ });
1001
+ }
1002
+ case "navigate": {
1003
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
1004
+ const targetId = readStringParam(params, "targetId");
1005
+ return jsonResult(
1006
+ await browserNavigate(baseUrl, {
1007
+ url: targetUrl,
1008
+ targetId,
1009
+ profile
1010
+ })
1011
+ );
1012
+ }
1013
+ case "console": {
1014
+ const level = typeof params.level === "string" ? params.level.trim() : void 0;
1015
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1016
+ const result = await browserConsoleMessages(baseUrl, { level, targetId, profile });
1017
+ const wrapped = wrapBrowserExternalJson({
1018
+ kind: "console",
1019
+ payload: result,
1020
+ includeWarning: false
1021
+ });
1022
+ return {
1023
+ content: [{ type: "text", text: wrapped.wrappedText }],
1024
+ details: {
1025
+ ...wrapped.safeDetails,
1026
+ targetId: result.targetId,
1027
+ messageCount: result.messages.length
1028
+ }
1029
+ };
1030
+ }
1031
+ case "pdf": {
1032
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1033
+ const result = await browserPdfSave(baseUrl, { targetId, profile });
1034
+ return {
1035
+ content: [{ type: "text", text: `FILE:${result.path}` }],
1036
+ details: result
1037
+ };
1038
+ }
1039
+ case "upload": {
1040
+ const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
1041
+ if (paths.length === 0) throw new Error("paths required");
1042
+ const uploadDir = config?.uploadDir || DEFAULT_UPLOAD_DIR;
1043
+ const normalizedPaths = resolvePathsWithinRoot(uploadDir, ...paths);
1044
+ const ref = readStringParam(params, "ref");
1045
+ const inputRef = readStringParam(params, "inputRef");
1046
+ const element = readStringParam(params, "element");
1047
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1048
+ const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
1049
+ return jsonResult(
1050
+ await browserArmFileChooser(baseUrl, {
1051
+ paths: normalizedPaths,
1052
+ ref,
1053
+ inputRef,
1054
+ element,
1055
+ targetId,
1056
+ timeoutMs,
1057
+ profile
1058
+ })
1059
+ );
1060
+ }
1061
+ case "dialog": {
1062
+ const accept = Boolean(params.accept);
1063
+ const promptText = typeof params.promptText === "string" ? params.promptText : void 0;
1064
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : void 0;
1065
+ const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : void 0;
1066
+ return jsonResult(
1067
+ await browserArmDialog(baseUrl, {
1068
+ accept,
1069
+ promptText,
1070
+ targetId,
1071
+ timeoutMs,
1072
+ profile
1073
+ })
1074
+ );
1075
+ }
1076
+ case "act": {
1077
+ const request = params.request;
1078
+ if (!request || typeof request !== "object") throw new Error("request required");
1079
+ if (request.kind === "evaluate" && config?.allowEvaluate === false) {
1080
+ throw new Error("JavaScript evaluation is disabled for this agent. Enable it in agent config.");
1081
+ }
1082
+ const result = await browserAct(baseUrl, request, {
1083
+ profile
1084
+ });
1085
+ return jsonResult(result);
1086
+ }
1087
+ default:
1088
+ throw new Error(`Unknown browser action: ${action}`);
1089
+ }
1090
+ }
1091
+ }
1092
+ export {
1093
+ createEnterpriseBrowserTool
1094
+ };