@agenticmail/enterprise 0.5.405 → 0.5.406

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