@agenticmail/enterprise 0.5.77 → 0.5.79

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.
Files changed (102) hide show
  1. package/dist/chunk-7RNT4O5T.js +15198 -0
  2. package/dist/chunk-AGFOJCSB.js +2191 -0
  3. package/dist/chunk-CYABMD5B.js +2191 -0
  4. package/dist/chunk-F4GSFCM3.js +898 -0
  5. package/dist/chunk-GINZ56GG.js +15035 -0
  6. package/dist/chunk-NRKB2KGD.js +898 -0
  7. package/dist/chunk-PZA7YOJE.js +898 -0
  8. package/dist/chunk-Q3V7VZFQ.js +2191 -0
  9. package/dist/chunk-RRFB6G6M.js +15198 -0
  10. package/dist/chunk-VX3VFMVB.js +409 -0
  11. package/dist/cli.js +1 -1
  12. package/dist/dashboard/pages/agent-detail.js +491 -2
  13. package/dist/index.js +4 -3
  14. package/dist/pw-ai-KPETTB25.js +2212 -0
  15. package/dist/routes-2T2ZNH3D.js +6642 -0
  16. package/dist/routes-PDHMCIXU.js +6676 -0
  17. package/dist/runtime-5ZJYB5PY.js +47 -0
  18. package/dist/runtime-7HW4GX5L.js +48 -0
  19. package/dist/runtime-XXDCZZIK.js +48 -0
  20. package/dist/server-FMP4BFGW.js +12 -0
  21. package/dist/server-JRHDUNII.js +12 -0
  22. package/dist/server-QPIMKFK4.js +12 -0
  23. package/dist/setup-NPFIX7LF.js +20 -0
  24. package/dist/setup-O5FPRLK4.js +20 -0
  25. package/dist/setup-S4Z4PPIJ.js +20 -0
  26. package/package.json +15 -2
  27. package/src/agent-tools/common.ts +25 -0
  28. package/src/agent-tools/index.ts +3 -0
  29. package/src/agent-tools/schema/typebox.ts +25 -0
  30. package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
  31. package/src/agent-tools/tools/browser-tool.ts +388 -0
  32. package/src/agent-tools/tools/gateway.ts +126 -0
  33. package/src/agent-tools/tools/nodes-utils.ts +80 -0
  34. package/src/browser/bridge-auth-registry.ts +34 -0
  35. package/src/browser/bridge-server.ts +93 -0
  36. package/src/browser/cdp.helpers.ts +180 -0
  37. package/src/browser/cdp.ts +466 -0
  38. package/src/browser/chrome.executables.ts +625 -0
  39. package/src/browser/chrome.profile-decoration.ts +198 -0
  40. package/src/browser/chrome.ts +349 -0
  41. package/src/browser/client-actions-core.ts +259 -0
  42. package/src/browser/client-actions-observe.ts +184 -0
  43. package/src/browser/client-actions-state.ts +284 -0
  44. package/src/browser/client-actions-types.ts +16 -0
  45. package/src/browser/client-actions-url.ts +11 -0
  46. package/src/browser/client-actions.ts +4 -0
  47. package/src/browser/client-fetch.ts +253 -0
  48. package/src/browser/client.ts +337 -0
  49. package/src/browser/config.ts +296 -0
  50. package/src/browser/constants.ts +8 -0
  51. package/src/browser/control-auth.ts +94 -0
  52. package/src/browser/control-service.ts +81 -0
  53. package/src/browser/csrf.ts +87 -0
  54. package/src/browser/enterprise-compat.ts +518 -0
  55. package/src/browser/extension-relay.ts +834 -0
  56. package/src/browser/http-auth.ts +63 -0
  57. package/src/browser/navigation-guard.ts +50 -0
  58. package/src/browser/paths.ts +49 -0
  59. package/src/browser/profiles-service.ts +187 -0
  60. package/src/browser/profiles.ts +113 -0
  61. package/src/browser/proxy-files.ts +41 -0
  62. package/src/browser/pw-ai-module.ts +52 -0
  63. package/src/browser/pw-ai-state.ts +9 -0
  64. package/src/browser/pw-ai.ts +65 -0
  65. package/src/browser/pw-role-snapshot.ts +434 -0
  66. package/src/browser/pw-session.ts +810 -0
  67. package/src/browser/pw-tools-core.activity.ts +68 -0
  68. package/src/browser/pw-tools-core.downloads.ts +281 -0
  69. package/src/browser/pw-tools-core.interactions.ts +646 -0
  70. package/src/browser/pw-tools-core.responses.ts +124 -0
  71. package/src/browser/pw-tools-core.shared.ts +70 -0
  72. package/src/browser/pw-tools-core.snapshot.ts +213 -0
  73. package/src/browser/pw-tools-core.state.ts +209 -0
  74. package/src/browser/pw-tools-core.storage.ts +128 -0
  75. package/src/browser/pw-tools-core.trace.ts +37 -0
  76. package/src/browser/pw-tools-core.ts +8 -0
  77. package/src/browser/resolved-config-refresh.ts +59 -0
  78. package/src/browser/routes/agent.act.shared.ts +52 -0
  79. package/src/browser/routes/agent.act.ts +575 -0
  80. package/src/browser/routes/agent.debug.ts +149 -0
  81. package/src/browser/routes/agent.shared.ts +143 -0
  82. package/src/browser/routes/agent.snapshot.ts +333 -0
  83. package/src/browser/routes/agent.storage.ts +451 -0
  84. package/src/browser/routes/agent.ts +13 -0
  85. package/src/browser/routes/basic.ts +202 -0
  86. package/src/browser/routes/dispatcher.ts +126 -0
  87. package/src/browser/routes/index.ts +11 -0
  88. package/src/browser/routes/path-output.ts +1 -0
  89. package/src/browser/routes/tabs.ts +217 -0
  90. package/src/browser/routes/types.ts +26 -0
  91. package/src/browser/routes/utils.ts +73 -0
  92. package/src/browser/screenshot.ts +54 -0
  93. package/src/browser/server-context.ts +688 -0
  94. package/src/browser/server-context.types.ts +65 -0
  95. package/src/browser/server-lifecycle.ts +48 -0
  96. package/src/browser/server-middleware.ts +37 -0
  97. package/src/browser/server.ts +110 -0
  98. package/src/browser/target-id.ts +30 -0
  99. package/src/browser/trash.ts +21 -0
  100. package/src/dashboard/pages/agent-detail.js +491 -2
  101. package/src/engine/agent-routes.ts +246 -0
  102. package/src/security/external-content.ts +299 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Enterprise Browser Tool
3
+ *
4
+ * Full browser automation for enterprise agents using Playwright.
5
+ * Adapted from OpenClaw's browser system — supports all actions:
6
+ * status, start, stop, profiles, tabs, open, focus, close,
7
+ * snapshot, screenshot, navigate, console, pdf, upload, dialog, act.
8
+ *
9
+ * No restrictions by default — restrictions are configurable per-agent
10
+ * via the dashboard Tools tab and agent config.
11
+ */
12
+
13
+ import crypto from "node:crypto";
14
+ import {
15
+ browserAct,
16
+ browserArmDialog,
17
+ browserArmFileChooser,
18
+ browserConsoleMessages,
19
+ browserNavigate,
20
+ browserPdfSave,
21
+ browserScreenshotAction,
22
+ } from "../../browser/client-actions.js";
23
+ import {
24
+ browserCloseTab,
25
+ browserFocusTab,
26
+ browserOpenTab,
27
+ browserProfiles,
28
+ browserSnapshot,
29
+ browserStart,
30
+ browserStatus,
31
+ browserStop,
32
+ browserTabs,
33
+ } from "../../browser/client.js";
34
+ import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
35
+ import { DEFAULT_UPLOAD_DIR, resolvePathsWithinRoot, wrapExternalContent } from "../../browser/enterprise-compat.js";
36
+ import { BrowserToolSchema } from "./browser-tool.schema.js";
37
+ import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "../common.js";
38
+
39
+ function wrapBrowserExternalJson(params: {
40
+ kind: "snapshot" | "console" | "tabs";
41
+ payload: unknown;
42
+ includeWarning?: boolean;
43
+ }): { wrappedText: string; safeDetails: Record<string, unknown> } {
44
+ const extractedText = JSON.stringify(params.payload, null, 2);
45
+ const wrappedText = wrapExternalContent(extractedText, {
46
+ source: "browser",
47
+ includeWarning: params.includeWarning ?? true,
48
+ });
49
+ return {
50
+ wrappedText,
51
+ safeDetails: {
52
+ ok: true,
53
+ externalContent: {
54
+ untrusted: true,
55
+ source: "browser",
56
+ kind: params.kind,
57
+ wrapped: true,
58
+ },
59
+ },
60
+ };
61
+ }
62
+
63
+ /** Enterprise browser tool configuration */
64
+ export interface EnterpriseBrowserToolConfig {
65
+ /** Base URL for browser control server (default: auto-detect) */
66
+ baseUrl?: string;
67
+ /** Default profile to use */
68
+ defaultProfile?: string;
69
+ /** Allow JavaScript evaluation */
70
+ allowEvaluate?: boolean;
71
+ /** Allow file:// URLs */
72
+ allowFileUrls?: boolean;
73
+ /** Allow navigation to any URL (no SSRF protection) */
74
+ allowAllUrls?: boolean;
75
+ /** Blocked URL patterns */
76
+ blockedUrlPatterns?: string[];
77
+ /** Max screenshot size */
78
+ maxScreenshotBytes?: number;
79
+ /** Upload directory root */
80
+ uploadDir?: string;
81
+ }
82
+
83
+ export function createEnterpriseBrowserTool(config?: EnterpriseBrowserToolConfig): AnyAgentTool {
84
+ const baseUrl = config?.baseUrl;
85
+ const defaultProfile = config?.defaultProfile;
86
+
87
+ return {
88
+ label: "Browser",
89
+ name: "browser",
90
+ description: [
91
+ "Control the browser for web automation — navigate, screenshot, snapshot (accessibility tree), click, type, hover, drag, fill forms, manage tabs, capture console logs, save PDFs, upload files, and handle dialogs.",
92
+ "Actions: status, start, stop, profiles, tabs, open, focus, close, snapshot, screenshot, navigate, console, pdf, upload, dialog, act.",
93
+ "Use snapshot+act for UI automation. snapshot returns the page accessibility tree; use refs from it with act to interact.",
94
+ 'snapshot format="ai" returns a text description; format="aria" returns structured nodes.',
95
+ 'act supports: click, type, press, hover, drag, select, fill, resize, wait, evaluate, close.',
96
+ 'For multi-tab workflows, use tabs to list, open to create, focus to switch, close to remove.',
97
+ ].join(" "),
98
+ parameters: BrowserToolSchema,
99
+ execute: async (_toolCallId, args) => {
100
+ const params = args as Record<string, unknown>;
101
+ const action = readStringParam(params, "action", { required: true });
102
+ const profile = readStringParam(params, "profile") || defaultProfile;
103
+
104
+ switch (action) {
105
+ case "status":
106
+ return jsonResult(await browserStatus(baseUrl, { profile }));
107
+
108
+ case "start":
109
+ await browserStart(baseUrl, { profile });
110
+ return jsonResult(await browserStatus(baseUrl, { profile }));
111
+
112
+ case "stop":
113
+ await browserStop(baseUrl, { profile });
114
+ return jsonResult(await browserStatus(baseUrl, { profile }));
115
+
116
+ case "profiles":
117
+ return jsonResult({ profiles: await browserProfiles(baseUrl) });
118
+
119
+ case "tabs": {
120
+ const tabs = await browserTabs(baseUrl, { profile });
121
+ const wrapped = wrapBrowserExternalJson({
122
+ kind: "tabs",
123
+ payload: { tabs },
124
+ includeWarning: false,
125
+ });
126
+ return {
127
+ content: [{ type: "text", text: wrapped.wrappedText }],
128
+ details: { ...wrapped.safeDetails, tabCount: tabs.length },
129
+ };
130
+ }
131
+
132
+ case "open": {
133
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
134
+ return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
135
+ }
136
+
137
+ case "focus": {
138
+ const targetId = readStringParam(params, "targetId", { required: true });
139
+ await browserFocusTab(baseUrl, targetId, { profile });
140
+ return jsonResult({ ok: true });
141
+ }
142
+
143
+ case "close": {
144
+ const targetId = readStringParam(params, "targetId");
145
+ if (targetId) {
146
+ await browserCloseTab(baseUrl, targetId, { profile });
147
+ } else {
148
+ await browserAct(baseUrl, { kind: "close" }, { profile });
149
+ }
150
+ return jsonResult({ ok: true });
151
+ }
152
+
153
+ case "snapshot": {
154
+ const format =
155
+ params.snapshotFormat === "ai" || params.snapshotFormat === "aria"
156
+ ? params.snapshotFormat
157
+ : "ai";
158
+ const mode = params.mode === "efficient" ? "efficient" : undefined;
159
+ const labels = typeof params.labels === "boolean" ? params.labels : undefined;
160
+ const refs = params.refs === "aria" || params.refs === "role" ? params.refs : undefined;
161
+ const hasMaxChars = Object.hasOwn(params, "maxChars");
162
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
163
+ const limit =
164
+ typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined;
165
+ const maxChars =
166
+ typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0
167
+ ? Math.floor(params.maxChars)
168
+ : undefined;
169
+ const resolvedMaxChars =
170
+ format === "ai"
171
+ ? hasMaxChars
172
+ ? maxChars
173
+ : mode === "efficient"
174
+ ? undefined
175
+ : DEFAULT_AI_SNAPSHOT_MAX_CHARS
176
+ : undefined;
177
+ const interactive = typeof params.interactive === "boolean" ? params.interactive : undefined;
178
+ const compact = typeof params.compact === "boolean" ? params.compact : undefined;
179
+ const depth =
180
+ typeof params.depth === "number" && Number.isFinite(params.depth) ? params.depth : undefined;
181
+ const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
182
+ const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
183
+
184
+ const snapshot = await browserSnapshot(baseUrl, {
185
+ format,
186
+ targetId,
187
+ limit,
188
+ ...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
189
+ refs,
190
+ interactive,
191
+ compact,
192
+ depth,
193
+ selector,
194
+ frame,
195
+ labels,
196
+ mode,
197
+ profile,
198
+ });
199
+
200
+ if (snapshot.format === "ai") {
201
+ const extractedText = snapshot.snapshot ?? "";
202
+ const wrappedSnapshot = wrapExternalContent(extractedText, {
203
+ source: "browser",
204
+ includeWarning: true,
205
+ });
206
+ const safeDetails = {
207
+ ok: true,
208
+ format: snapshot.format,
209
+ targetId: snapshot.targetId,
210
+ url: snapshot.url,
211
+ truncated: snapshot.truncated,
212
+ stats: snapshot.stats,
213
+ refs: snapshot.refs ? Object.keys(snapshot.refs).length : undefined,
214
+ labels: snapshot.labels,
215
+ labelsCount: snapshot.labelsCount,
216
+ labelsSkipped: snapshot.labelsSkipped,
217
+ imagePath: snapshot.imagePath,
218
+ imageType: snapshot.imageType,
219
+ externalContent: {
220
+ untrusted: true,
221
+ source: "browser",
222
+ kind: "snapshot",
223
+ format: "ai",
224
+ wrapped: true,
225
+ },
226
+ };
227
+ if (labels && snapshot.imagePath) {
228
+ return await imageResultFromFile({
229
+ label: "browser:snapshot",
230
+ path: snapshot.imagePath,
231
+ extraText: wrappedSnapshot,
232
+ details: safeDetails,
233
+ });
234
+ }
235
+ return {
236
+ content: [{ type: "text", text: wrappedSnapshot }],
237
+ details: safeDetails,
238
+ };
239
+ }
240
+
241
+ // aria format
242
+ const wrapped = wrapBrowserExternalJson({
243
+ kind: "snapshot",
244
+ payload: snapshot,
245
+ });
246
+ return {
247
+ content: [{ type: "text", text: wrapped.wrappedText }],
248
+ details: {
249
+ ...wrapped.safeDetails,
250
+ format: "aria",
251
+ targetId: snapshot.targetId,
252
+ url: snapshot.url,
253
+ nodeCount: snapshot.nodes.length,
254
+ },
255
+ };
256
+ }
257
+
258
+ case "screenshot": {
259
+ const targetId = readStringParam(params, "targetId");
260
+ const fullPage = Boolean(params.fullPage);
261
+ const ref = readStringParam(params, "ref");
262
+ const element = readStringParam(params, "element");
263
+ const type = params.type === "jpeg" ? "jpeg" : "png";
264
+ const result = await browserScreenshotAction(baseUrl, {
265
+ targetId,
266
+ fullPage,
267
+ ref,
268
+ element,
269
+ type,
270
+ profile,
271
+ });
272
+ return await imageResultFromFile({
273
+ label: "browser:screenshot",
274
+ path: result.path,
275
+ details: result,
276
+ });
277
+ }
278
+
279
+ case "navigate": {
280
+ const targetUrl = readStringParam(params, "targetUrl", { required: true });
281
+ const targetId = readStringParam(params, "targetId");
282
+ return jsonResult(
283
+ await browserNavigate(baseUrl, {
284
+ url: targetUrl,
285
+ targetId,
286
+ profile,
287
+ }),
288
+ );
289
+ }
290
+
291
+ case "console": {
292
+ const level = typeof params.level === "string" ? params.level.trim() : undefined;
293
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
294
+ const result = await browserConsoleMessages(baseUrl, { level, targetId, profile });
295
+ const wrapped = wrapBrowserExternalJson({
296
+ kind: "console",
297
+ payload: result,
298
+ includeWarning: false,
299
+ });
300
+ return {
301
+ content: [{ type: "text", text: wrapped.wrappedText }],
302
+ details: {
303
+ ...wrapped.safeDetails,
304
+ targetId: result.targetId,
305
+ messageCount: result.messages.length,
306
+ },
307
+ };
308
+ }
309
+
310
+ case "pdf": {
311
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
312
+ const result = await browserPdfSave(baseUrl, { targetId, profile });
313
+ return {
314
+ content: [{ type: "text", text: `FILE:${result.path}` }],
315
+ details: result,
316
+ };
317
+ }
318
+
319
+ case "upload": {
320
+ const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
321
+ if (paths.length === 0) throw new Error("paths required");
322
+
323
+ const uploadDir = config?.uploadDir || DEFAULT_UPLOAD_DIR;
324
+ const normalizedPaths = resolvePathsWithinRoot(uploadDir, ...paths);
325
+
326
+ const ref = readStringParam(params, "ref");
327
+ const inputRef = readStringParam(params, "inputRef");
328
+ const element = readStringParam(params, "element");
329
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
330
+ const timeoutMs =
331
+ typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
332
+ ? params.timeoutMs
333
+ : undefined;
334
+
335
+ return jsonResult(
336
+ await browserArmFileChooser(baseUrl, {
337
+ paths: normalizedPaths,
338
+ ref,
339
+ inputRef,
340
+ element,
341
+ targetId,
342
+ timeoutMs,
343
+ profile,
344
+ }),
345
+ );
346
+ }
347
+
348
+ case "dialog": {
349
+ const accept = Boolean(params.accept);
350
+ const promptText = typeof params.promptText === "string" ? params.promptText : undefined;
351
+ const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
352
+ const timeoutMs =
353
+ typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
354
+ ? params.timeoutMs
355
+ : undefined;
356
+
357
+ return jsonResult(
358
+ await browserArmDialog(baseUrl, {
359
+ accept,
360
+ promptText,
361
+ targetId,
362
+ timeoutMs,
363
+ profile,
364
+ }),
365
+ );
366
+ }
367
+
368
+ case "act": {
369
+ const request = params.request as Record<string, unknown> | undefined;
370
+ if (!request || typeof request !== "object") throw new Error("request required");
371
+
372
+ // Check evaluate restrictions
373
+ if (request.kind === "evaluate" && config?.allowEvaluate === false) {
374
+ throw new Error("JavaScript evaluation is disabled for this agent. Enable it in agent config.");
375
+ }
376
+
377
+ const result = await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
378
+ profile,
379
+ });
380
+ return jsonResult(result);
381
+ }
382
+
383
+ default:
384
+ throw new Error(`Unknown browser action: ${action}`);
385
+ }
386
+ },
387
+ };
388
+ }
@@ -0,0 +1,126 @@
1
+ import { loadConfig, resolveGatewayPort } from "../../config/config.js";
2
+ import { callGateway } from "../../gateway/call.js";
3
+ import { resolveLeastPrivilegeOperatorScopesForMethod } from "../../gateway/method-scopes.js";
4
+ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
5
+ import { readStringParam } from "./common.js";
6
+
7
+ export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
8
+
9
+ export type GatewayCallOptions = {
10
+ gatewayUrl?: string;
11
+ gatewayToken?: string;
12
+ timeoutMs?: number;
13
+ };
14
+
15
+ export function readGatewayCallOptions(params: Record<string, unknown>): GatewayCallOptions {
16
+ return {
17
+ gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
18
+ gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
19
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
20
+ };
21
+ }
22
+
23
+ function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: string } {
24
+ const input = raw.trim();
25
+ let url: URL;
26
+ try {
27
+ url = new URL(input);
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
31
+ }
32
+
33
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
34
+ throw new Error(`invalid gatewayUrl protocol: ${url.protocol} (expected ws:// or wss://)`);
35
+ }
36
+ if (url.username || url.password) {
37
+ throw new Error("invalid gatewayUrl: credentials are not allowed");
38
+ }
39
+ if (url.search || url.hash) {
40
+ throw new Error("invalid gatewayUrl: query/hash not allowed");
41
+ }
42
+ // Agents/tools expect the gateway websocket on the origin, not arbitrary paths.
43
+ if (url.pathname && url.pathname !== "/") {
44
+ throw new Error("invalid gatewayUrl: path not allowed");
45
+ }
46
+
47
+ const origin = url.origin;
48
+ // Key: protocol + host only, lowercased. (host includes IPv6 brackets + port when present)
49
+ const key = `${url.protocol}//${url.host.toLowerCase()}`;
50
+ return { origin, key };
51
+ }
52
+
53
+ function validateGatewayUrlOverrideForAgentTools(urlOverride: string): string {
54
+ const cfg = loadConfig();
55
+ const port = resolveGatewayPort(cfg);
56
+ const allowed = new Set<string>([
57
+ `ws://127.0.0.1:${port}`,
58
+ `wss://127.0.0.1:${port}`,
59
+ `ws://localhost:${port}`,
60
+ `wss://localhost:${port}`,
61
+ `ws://[::1]:${port}`,
62
+ `wss://[::1]:${port}`,
63
+ ]);
64
+
65
+ const remoteUrl =
66
+ typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
67
+ if (remoteUrl) {
68
+ try {
69
+ const remote = canonicalizeToolGatewayWsUrl(remoteUrl);
70
+ allowed.add(remote.key);
71
+ } catch {
72
+ // ignore: misconfigured remote url; tools should fall back to default resolution.
73
+ }
74
+ }
75
+
76
+ const parsed = canonicalizeToolGatewayWsUrl(urlOverride);
77
+ if (!allowed.has(parsed.key)) {
78
+ throw new Error(
79
+ [
80
+ "gatewayUrl override rejected.",
81
+ `Allowed: ws(s) loopback on port ${port} (127.0.0.1/localhost/[::1])`,
82
+ "Or: configure gateway.remote.url and omit gatewayUrl to use the configured remote gateway.",
83
+ ].join(" "),
84
+ );
85
+ }
86
+ return parsed.origin;
87
+ }
88
+
89
+ export function resolveGatewayOptions(opts?: GatewayCallOptions) {
90
+ // Prefer an explicit override; otherwise let callGateway choose based on config.
91
+ const url =
92
+ typeof opts?.gatewayUrl === "string" && opts.gatewayUrl.trim()
93
+ ? validateGatewayUrlOverrideForAgentTools(opts.gatewayUrl)
94
+ : undefined;
95
+ const token =
96
+ typeof opts?.gatewayToken === "string" && opts.gatewayToken.trim()
97
+ ? opts.gatewayToken.trim()
98
+ : undefined;
99
+ const timeoutMs =
100
+ typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
101
+ ? Math.max(1, Math.floor(opts.timeoutMs))
102
+ : 30_000;
103
+ return { url, token, timeoutMs };
104
+ }
105
+
106
+ export async function callGatewayTool<T = Record<string, unknown>>(
107
+ method: string,
108
+ opts: GatewayCallOptions,
109
+ params?: unknown,
110
+ extra?: { expectFinal?: boolean },
111
+ ) {
112
+ const gateway = resolveGatewayOptions(opts);
113
+ const scopes = resolveLeastPrivilegeOperatorScopesForMethod(method);
114
+ return await callGateway<T>({
115
+ url: gateway.url,
116
+ token: gateway.token,
117
+ method,
118
+ params,
119
+ timeoutMs: gateway.timeoutMs,
120
+ expectFinal: extra?.expectFinal,
121
+ clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
122
+ clientDisplayName: "agent",
123
+ mode: GATEWAY_CLIENT_MODES.BACKEND,
124
+ scopes,
125
+ });
126
+ }
@@ -0,0 +1,80 @@
1
+ import { parseNodeList, parsePairingList } from "../../shared/node-list-parse.js";
2
+ import type { NodeListNode } from "../../shared/node-list-types.js";
3
+ import { resolveNodeIdFromCandidates } from "../../shared/node-match.js";
4
+ import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
5
+
6
+ export type { NodeListNode };
7
+
8
+ async function loadNodes(opts: GatewayCallOptions): Promise<NodeListNode[]> {
9
+ try {
10
+ const res = await callGatewayTool("node.list", opts, {});
11
+ return parseNodeList(res);
12
+ } catch {
13
+ const res = await callGatewayTool("node.pair.list", opts, {});
14
+ const { paired } = parsePairingList(res);
15
+ return paired.map((n) => ({
16
+ nodeId: n.nodeId,
17
+ displayName: n.displayName,
18
+ platform: n.platform,
19
+ remoteIp: n.remoteIp,
20
+ }));
21
+ }
22
+ }
23
+
24
+ function pickDefaultNode(nodes: NodeListNode[]): NodeListNode | null {
25
+ const withCanvas = nodes.filter((n) =>
26
+ Array.isArray(n.caps) ? n.caps.includes("canvas") : true,
27
+ );
28
+ if (withCanvas.length === 0) {
29
+ return null;
30
+ }
31
+
32
+ const connected = withCanvas.filter((n) => n.connected);
33
+ const candidates = connected.length > 0 ? connected : withCanvas;
34
+ if (candidates.length === 1) {
35
+ return candidates[0];
36
+ }
37
+
38
+ const local = candidates.filter(
39
+ (n) =>
40
+ n.platform?.toLowerCase().startsWith("mac") &&
41
+ typeof n.nodeId === "string" &&
42
+ n.nodeId.startsWith("mac-"),
43
+ );
44
+ if (local.length === 1) {
45
+ return local[0];
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ export async function listNodes(opts: GatewayCallOptions): Promise<NodeListNode[]> {
52
+ return loadNodes(opts);
53
+ }
54
+
55
+ export function resolveNodeIdFromList(
56
+ nodes: NodeListNode[],
57
+ query?: string,
58
+ allowDefault = false,
59
+ ): string {
60
+ const q = String(query ?? "").trim();
61
+ if (!q) {
62
+ if (allowDefault) {
63
+ const picked = pickDefaultNode(nodes);
64
+ if (picked) {
65
+ return picked.nodeId;
66
+ }
67
+ }
68
+ throw new Error("node required");
69
+ }
70
+ return resolveNodeIdFromCandidates(nodes, q);
71
+ }
72
+
73
+ export async function resolveNodeId(
74
+ opts: GatewayCallOptions,
75
+ query?: string,
76
+ allowDefault = false,
77
+ ) {
78
+ const nodes = await loadNodes(opts);
79
+ return resolveNodeIdFromList(nodes, query, allowDefault);
80
+ }
@@ -0,0 +1,34 @@
1
+ type BridgeAuth = {
2
+ token?: string;
3
+ password?: string;
4
+ };
5
+
6
+ // In-process registry for loopback-only bridge servers that require auth, but
7
+ // are addressed via dynamic ephemeral ports (e.g. sandbox browser bridge).
8
+ const authByPort = new Map<number, BridgeAuth>();
9
+
10
+ export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
11
+ if (!Number.isFinite(port) || port <= 0) {
12
+ return;
13
+ }
14
+ const token = typeof auth.token === "string" ? auth.token.trim() : "";
15
+ const password = typeof auth.password === "string" ? auth.password.trim() : "";
16
+ authByPort.set(port, {
17
+ token: token || undefined,
18
+ password: password || undefined,
19
+ });
20
+ }
21
+
22
+ export function getBridgeAuthForPort(port: number): BridgeAuth | undefined {
23
+ if (!Number.isFinite(port) || port <= 0) {
24
+ return undefined;
25
+ }
26
+ return authByPort.get(port);
27
+ }
28
+
29
+ export function deleteBridgeAuthForPort(port: number): void {
30
+ if (!Number.isFinite(port) || port <= 0) {
31
+ return;
32
+ }
33
+ authByPort.delete(port);
34
+ }