@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.
- package/dist/agent-tools-N6XO67X6.js +14587 -0
- package/dist/agent-tools-NKRUJDD7.js +14587 -0
- package/dist/browser-tool-SADIVYCE.js +1136 -0
- package/dist/browser-tool-UZJKWIEO.js +1150 -0
- package/dist/chunk-7C2NO3QT.js +1728 -0
- package/dist/chunk-GKJU25QA.js +359 -0
- package/dist/chunk-HHP5PRXW.js +615 -0
- package/dist/chunk-I4KM2MRX.js +4993 -0
- package/dist/chunk-JGTRHXPL.js +2130 -0
- package/dist/chunk-KNLR52QC.js +1728 -0
- package/dist/chunk-NX5KMT4B.js +5174 -0
- package/dist/chunk-SLBSXEFM.js +4993 -0
- package/dist/chunk-W4TFQFKJ.js +5174 -0
- package/dist/cli-agent-FHKAYMA7.js +2715 -0
- package/dist/cli-agent-WTYFGC4V.js +2715 -0
- package/dist/cli-serve-45Q43DXO.js +286 -0
- package/dist/cli-serve-H4GRLNLV.js +286 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/pw-ai-CLBYYYBF.js +2321 -0
- package/dist/routes-ZCLWGNGV.js +10 -0
- package/dist/runtime-GBAB4Q2B.js +46 -0
- package/dist/runtime-SBXULSHE.js +46 -0
- package/dist/server-5ZLWHQHN.js +28 -0
- package/dist/server-EYRZRQ64.js +28 -0
- package/dist/server-context-QUU2BRWM.js +12 -0
- package/dist/setup-6TJAGUE2.js +20 -0
- package/dist/setup-UA5SW7WE.js +20 -0
- package/logs/cloudflared-error.log +20 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|