@agenticmail/enterprise 0.5.78 → 0.5.80

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 (101) hide show
  1. package/dist/chunk-7MILGDAA.js +2191 -0
  2. package/dist/chunk-7RNT4O5T.js +15198 -0
  3. package/dist/chunk-AGFOJCSB.js +2191 -0
  4. package/dist/chunk-F4GSFCM3.js +898 -0
  5. package/dist/chunk-GWUIYH7I.js +15035 -0
  6. package/dist/chunk-PZA7YOJE.js +898 -0
  7. package/dist/chunk-Q3V7VZFQ.js +2191 -0
  8. package/dist/chunk-RRFB6G6M.js +15198 -0
  9. package/dist/chunk-VX3VFMVB.js +409 -0
  10. package/dist/chunk-WRPZCOWC.js +898 -0
  11. package/dist/cli.js +1 -1
  12. package/dist/dashboard/pages/agent-detail.js +313 -1
  13. package/dist/index.js +3 -3
  14. package/dist/pw-ai-KPETTB25.js +2212 -0
  15. package/dist/routes-PDHMCIXU.js +6676 -0
  16. package/dist/runtime-7HW4GX5L.js +48 -0
  17. package/dist/runtime-GYVO3NF3.js +47 -0
  18. package/dist/runtime-XXDCZZIK.js +48 -0
  19. package/dist/server-FMP4BFGW.js +12 -0
  20. package/dist/server-JRHDUNII.js +12 -0
  21. package/dist/server-VNW6G4GB.js +12 -0
  22. package/dist/setup-AANLREEL.js +20 -0
  23. package/dist/setup-O5FPRLK4.js +20 -0
  24. package/dist/setup-S4Z4PPIJ.js +20 -0
  25. package/package.json +15 -2
  26. package/src/agent-tools/common.ts +25 -0
  27. package/src/agent-tools/index.ts +4 -0
  28. package/src/agent-tools/schema/typebox.ts +25 -0
  29. package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
  30. package/src/agent-tools/tools/browser-tool.ts +388 -0
  31. package/src/agent-tools/tools/gateway.ts +126 -0
  32. package/src/agent-tools/tools/nodes-utils.ts +80 -0
  33. package/src/browser/bridge-auth-registry.ts +34 -0
  34. package/src/browser/bridge-server.ts +93 -0
  35. package/src/browser/cdp.helpers.ts +180 -0
  36. package/src/browser/cdp.ts +466 -0
  37. package/src/browser/chrome.executables.ts +625 -0
  38. package/src/browser/chrome.profile-decoration.ts +198 -0
  39. package/src/browser/chrome.ts +349 -0
  40. package/src/browser/client-actions-core.ts +259 -0
  41. package/src/browser/client-actions-observe.ts +184 -0
  42. package/src/browser/client-actions-state.ts +284 -0
  43. package/src/browser/client-actions-types.ts +16 -0
  44. package/src/browser/client-actions-url.ts +11 -0
  45. package/src/browser/client-actions.ts +4 -0
  46. package/src/browser/client-fetch.ts +253 -0
  47. package/src/browser/client.ts +337 -0
  48. package/src/browser/config.ts +296 -0
  49. package/src/browser/constants.ts +8 -0
  50. package/src/browser/control-auth.ts +94 -0
  51. package/src/browser/control-service.ts +81 -0
  52. package/src/browser/csrf.ts +87 -0
  53. package/src/browser/enterprise-compat.ts +518 -0
  54. package/src/browser/extension-relay.ts +834 -0
  55. package/src/browser/http-auth.ts +63 -0
  56. package/src/browser/navigation-guard.ts +50 -0
  57. package/src/browser/paths.ts +49 -0
  58. package/src/browser/profiles-service.ts +187 -0
  59. package/src/browser/profiles.ts +113 -0
  60. package/src/browser/proxy-files.ts +41 -0
  61. package/src/browser/pw-ai-module.ts +52 -0
  62. package/src/browser/pw-ai-state.ts +9 -0
  63. package/src/browser/pw-ai.ts +65 -0
  64. package/src/browser/pw-role-snapshot.ts +434 -0
  65. package/src/browser/pw-session.ts +810 -0
  66. package/src/browser/pw-tools-core.activity.ts +68 -0
  67. package/src/browser/pw-tools-core.downloads.ts +281 -0
  68. package/src/browser/pw-tools-core.interactions.ts +646 -0
  69. package/src/browser/pw-tools-core.responses.ts +124 -0
  70. package/src/browser/pw-tools-core.shared.ts +70 -0
  71. package/src/browser/pw-tools-core.snapshot.ts +213 -0
  72. package/src/browser/pw-tools-core.state.ts +209 -0
  73. package/src/browser/pw-tools-core.storage.ts +128 -0
  74. package/src/browser/pw-tools-core.trace.ts +37 -0
  75. package/src/browser/pw-tools-core.ts +8 -0
  76. package/src/browser/resolved-config-refresh.ts +59 -0
  77. package/src/browser/routes/agent.act.shared.ts +52 -0
  78. package/src/browser/routes/agent.act.ts +575 -0
  79. package/src/browser/routes/agent.debug.ts +149 -0
  80. package/src/browser/routes/agent.shared.ts +143 -0
  81. package/src/browser/routes/agent.snapshot.ts +333 -0
  82. package/src/browser/routes/agent.storage.ts +451 -0
  83. package/src/browser/routes/agent.ts +13 -0
  84. package/src/browser/routes/basic.ts +202 -0
  85. package/src/browser/routes/dispatcher.ts +126 -0
  86. package/src/browser/routes/index.ts +11 -0
  87. package/src/browser/routes/path-output.ts +1 -0
  88. package/src/browser/routes/tabs.ts +217 -0
  89. package/src/browser/routes/types.ts +26 -0
  90. package/src/browser/routes/utils.ts +73 -0
  91. package/src/browser/screenshot.ts +54 -0
  92. package/src/browser/server-context.ts +688 -0
  93. package/src/browser/server-context.types.ts +65 -0
  94. package/src/browser/server-lifecycle.ts +48 -0
  95. package/src/browser/server-middleware.ts +37 -0
  96. package/src/browser/server.ts +110 -0
  97. package/src/browser/target-id.ts +30 -0
  98. package/src/browser/trash.ts +21 -0
  99. package/src/dashboard/pages/agent-detail.js +313 -1
  100. package/src/engine/agent-routes.ts +46 -0
  101. package/src/security/external-content.ts +299 -0
@@ -0,0 +1,337 @@
1
+ import { fetchBrowserJson } from "./client-fetch.js";
2
+
3
+ export type BrowserStatus = {
4
+ enabled: boolean;
5
+ profile?: string;
6
+ running: boolean;
7
+ cdpReady?: boolean;
8
+ cdpHttp?: boolean;
9
+ pid: number | null;
10
+ cdpPort: number;
11
+ cdpUrl?: string;
12
+ chosenBrowser: string | null;
13
+ detectedBrowser?: string | null;
14
+ detectedExecutablePath?: string | null;
15
+ detectError?: string | null;
16
+ userDataDir: string | null;
17
+ color: string;
18
+ headless: boolean;
19
+ noSandbox?: boolean;
20
+ executablePath?: string | null;
21
+ attachOnly: boolean;
22
+ };
23
+
24
+ export type ProfileStatus = {
25
+ name: string;
26
+ cdpPort: number;
27
+ cdpUrl: string;
28
+ color: string;
29
+ running: boolean;
30
+ tabCount: number;
31
+ isDefault: boolean;
32
+ isRemote: boolean;
33
+ };
34
+
35
+ export type BrowserResetProfileResult = {
36
+ ok: true;
37
+ moved: boolean;
38
+ from: string;
39
+ to?: string;
40
+ };
41
+
42
+ export type BrowserTab = {
43
+ targetId: string;
44
+ title: string;
45
+ url: string;
46
+ wsUrl?: string;
47
+ type?: string;
48
+ };
49
+
50
+ export type SnapshotAriaNode = {
51
+ ref: string;
52
+ role: string;
53
+ name: string;
54
+ value?: string;
55
+ description?: string;
56
+ backendDOMNodeId?: number;
57
+ depth: number;
58
+ };
59
+
60
+ export type SnapshotResult =
61
+ | {
62
+ ok: true;
63
+ format: "aria";
64
+ targetId: string;
65
+ url: string;
66
+ nodes: SnapshotAriaNode[];
67
+ }
68
+ | {
69
+ ok: true;
70
+ format: "ai";
71
+ targetId: string;
72
+ url: string;
73
+ snapshot: string;
74
+ truncated?: boolean;
75
+ refs?: Record<string, { role: string; name?: string; nth?: number }>;
76
+ stats?: {
77
+ lines: number;
78
+ chars: number;
79
+ refs: number;
80
+ interactive: number;
81
+ };
82
+ labels?: boolean;
83
+ labelsCount?: number;
84
+ labelsSkipped?: number;
85
+ imagePath?: string;
86
+ imageType?: "png" | "jpeg";
87
+ };
88
+
89
+ function buildProfileQuery(profile?: string): string {
90
+ return profile ? `?profile=${encodeURIComponent(profile)}` : "";
91
+ }
92
+
93
+ function withBaseUrl(baseUrl: string | undefined, path: string): string {
94
+ const trimmed = baseUrl?.trim();
95
+ if (!trimmed) {
96
+ return path;
97
+ }
98
+ return `${trimmed.replace(/\/$/, "")}${path}`;
99
+ }
100
+
101
+ export async function browserStatus(
102
+ baseUrl?: string,
103
+ opts?: { profile?: string },
104
+ ): Promise<BrowserStatus> {
105
+ const q = buildProfileQuery(opts?.profile);
106
+ return await fetchBrowserJson<BrowserStatus>(withBaseUrl(baseUrl, `/${q}`), {
107
+ timeoutMs: 1500,
108
+ });
109
+ }
110
+
111
+ export async function browserProfiles(baseUrl?: string): Promise<ProfileStatus[]> {
112
+ const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>(
113
+ withBaseUrl(baseUrl, `/profiles`),
114
+ {
115
+ timeoutMs: 3000,
116
+ },
117
+ );
118
+ return res.profiles ?? [];
119
+ }
120
+
121
+ export async function browserStart(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
122
+ const q = buildProfileQuery(opts?.profile);
123
+ await fetchBrowserJson(withBaseUrl(baseUrl, `/start${q}`), {
124
+ method: "POST",
125
+ timeoutMs: 15000,
126
+ });
127
+ }
128
+
129
+ export async function browserStop(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
130
+ const q = buildProfileQuery(opts?.profile);
131
+ await fetchBrowserJson(withBaseUrl(baseUrl, `/stop${q}`), {
132
+ method: "POST",
133
+ timeoutMs: 15000,
134
+ });
135
+ }
136
+
137
+ export async function browserResetProfile(
138
+ baseUrl?: string,
139
+ opts?: { profile?: string },
140
+ ): Promise<BrowserResetProfileResult> {
141
+ const q = buildProfileQuery(opts?.profile);
142
+ return await fetchBrowserJson<BrowserResetProfileResult>(
143
+ withBaseUrl(baseUrl, `/reset-profile${q}`),
144
+ {
145
+ method: "POST",
146
+ timeoutMs: 20000,
147
+ },
148
+ );
149
+ }
150
+
151
+ export type BrowserCreateProfileResult = {
152
+ ok: true;
153
+ profile: string;
154
+ cdpPort: number;
155
+ cdpUrl: string;
156
+ color: string;
157
+ isRemote: boolean;
158
+ };
159
+
160
+ export async function browserCreateProfile(
161
+ baseUrl: string | undefined,
162
+ opts: {
163
+ name: string;
164
+ color?: string;
165
+ cdpUrl?: string;
166
+ driver?: "openclaw" | "extension";
167
+ },
168
+ ): Promise<BrowserCreateProfileResult> {
169
+ return await fetchBrowserJson<BrowserCreateProfileResult>(
170
+ withBaseUrl(baseUrl, `/profiles/create`),
171
+ {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify({
175
+ name: opts.name,
176
+ color: opts.color,
177
+ cdpUrl: opts.cdpUrl,
178
+ driver: opts.driver,
179
+ }),
180
+ timeoutMs: 10000,
181
+ },
182
+ );
183
+ }
184
+
185
+ export type BrowserDeleteProfileResult = {
186
+ ok: true;
187
+ profile: string;
188
+ deleted: boolean;
189
+ };
190
+
191
+ export async function browserDeleteProfile(
192
+ baseUrl: string | undefined,
193
+ profile: string,
194
+ ): Promise<BrowserDeleteProfileResult> {
195
+ return await fetchBrowserJson<BrowserDeleteProfileResult>(
196
+ withBaseUrl(baseUrl, `/profiles/${encodeURIComponent(profile)}`),
197
+ {
198
+ method: "DELETE",
199
+ timeoutMs: 20000,
200
+ },
201
+ );
202
+ }
203
+
204
+ export async function browserTabs(
205
+ baseUrl?: string,
206
+ opts?: { profile?: string },
207
+ ): Promise<BrowserTab[]> {
208
+ const q = buildProfileQuery(opts?.profile);
209
+ const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>(
210
+ withBaseUrl(baseUrl, `/tabs${q}`),
211
+ { timeoutMs: 3000 },
212
+ );
213
+ return res.tabs ?? [];
214
+ }
215
+
216
+ export async function browserOpenTab(
217
+ baseUrl: string | undefined,
218
+ url: string,
219
+ opts?: { profile?: string },
220
+ ): Promise<BrowserTab> {
221
+ const q = buildProfileQuery(opts?.profile);
222
+ return await fetchBrowserJson<BrowserTab>(withBaseUrl(baseUrl, `/tabs/open${q}`), {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json" },
225
+ body: JSON.stringify({ url }),
226
+ timeoutMs: 15000,
227
+ });
228
+ }
229
+
230
+ export async function browserFocusTab(
231
+ baseUrl: string | undefined,
232
+ targetId: string,
233
+ opts?: { profile?: string },
234
+ ): Promise<void> {
235
+ const q = buildProfileQuery(opts?.profile);
236
+ await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/focus${q}`), {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ targetId }),
240
+ timeoutMs: 5000,
241
+ });
242
+ }
243
+
244
+ export async function browserCloseTab(
245
+ baseUrl: string | undefined,
246
+ targetId: string,
247
+ opts?: { profile?: string },
248
+ ): Promise<void> {
249
+ const q = buildProfileQuery(opts?.profile);
250
+ await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
251
+ method: "DELETE",
252
+ timeoutMs: 5000,
253
+ });
254
+ }
255
+
256
+ export async function browserTabAction(
257
+ baseUrl: string | undefined,
258
+ opts: {
259
+ action: "list" | "new" | "close" | "select";
260
+ index?: number;
261
+ profile?: string;
262
+ },
263
+ ): Promise<unknown> {
264
+ const q = buildProfileQuery(opts.profile);
265
+ return await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/action${q}`), {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/json" },
268
+ body: JSON.stringify({
269
+ action: opts.action,
270
+ index: opts.index,
271
+ }),
272
+ timeoutMs: 10_000,
273
+ });
274
+ }
275
+
276
+ export async function browserSnapshot(
277
+ baseUrl: string | undefined,
278
+ opts: {
279
+ format: "aria" | "ai";
280
+ targetId?: string;
281
+ limit?: number;
282
+ maxChars?: number;
283
+ refs?: "role" | "aria";
284
+ interactive?: boolean;
285
+ compact?: boolean;
286
+ depth?: number;
287
+ selector?: string;
288
+ frame?: string;
289
+ labels?: boolean;
290
+ mode?: "efficient";
291
+ profile?: string;
292
+ },
293
+ ): Promise<SnapshotResult> {
294
+ const q = new URLSearchParams();
295
+ q.set("format", opts.format);
296
+ if (opts.targetId) {
297
+ q.set("targetId", opts.targetId);
298
+ }
299
+ if (typeof opts.limit === "number") {
300
+ q.set("limit", String(opts.limit));
301
+ }
302
+ if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
303
+ q.set("maxChars", String(opts.maxChars));
304
+ }
305
+ if (opts.refs === "aria" || opts.refs === "role") {
306
+ q.set("refs", opts.refs);
307
+ }
308
+ if (typeof opts.interactive === "boolean") {
309
+ q.set("interactive", String(opts.interactive));
310
+ }
311
+ if (typeof opts.compact === "boolean") {
312
+ q.set("compact", String(opts.compact));
313
+ }
314
+ if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) {
315
+ q.set("depth", String(opts.depth));
316
+ }
317
+ if (opts.selector?.trim()) {
318
+ q.set("selector", opts.selector.trim());
319
+ }
320
+ if (opts.frame?.trim()) {
321
+ q.set("frame", opts.frame.trim());
322
+ }
323
+ if (opts.labels === true) {
324
+ q.set("labels", "1");
325
+ }
326
+ if (opts.mode) {
327
+ q.set("mode", opts.mode);
328
+ }
329
+ if (opts.profile) {
330
+ q.set("profile", opts.profile);
331
+ }
332
+ return await fetchBrowserJson<SnapshotResult>(withBaseUrl(baseUrl, `/snapshot?${q.toString()}`), {
333
+ timeoutMs: 20000,
334
+ });
335
+ }
336
+
337
+ // Actions beyond the basic read-only commands live in client-actions.ts.
@@ -0,0 +1,296 @@
1
+
2
+
3
+ import {
4
+ DEFAULT_OPENCLAW_BROWSER_COLOR,
5
+ DEFAULT_OPENCLAW_BROWSER_ENABLED,
6
+ DEFAULT_BROWSER_EVALUATE_ENABLED,
7
+ DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
8
+ DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
9
+ } from "./constants.js";
10
+ import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js";
11
+ import { DEFAULT_BROWSER_CONTROL_PORT, deriveDefaultBrowserCdpPortRange, deriveDefaultBrowserControlPort, isLoopbackHost, resolveGatewayPort } from "./enterprise-compat.js";
12
+ import type { BrowserConfig, BrowserProfileConfig, OpenClawConfig, SsrFPolicy } from "./enterprise-compat.js";
13
+
14
+ export type ResolvedBrowserConfig = {
15
+ enabled: boolean;
16
+ evaluateEnabled: boolean;
17
+ controlPort: number;
18
+ cdpProtocol: "http" | "https";
19
+ cdpHost: string;
20
+ cdpIsLoopback: boolean;
21
+ remoteCdpTimeoutMs: number;
22
+ remoteCdpHandshakeTimeoutMs: number;
23
+ color: string;
24
+ executablePath?: string;
25
+ headless: boolean;
26
+ noSandbox: boolean;
27
+ attachOnly: boolean;
28
+ defaultProfile: string;
29
+ profiles: Record<string, BrowserProfileConfig>;
30
+ ssrfPolicy?: SsrFPolicy;
31
+ extraArgs: string[];
32
+ };
33
+
34
+ export type ResolvedBrowserProfile = {
35
+ name: string;
36
+ cdpPort: number;
37
+ cdpUrl: string;
38
+ cdpHost: string;
39
+ cdpIsLoopback: boolean;
40
+ color: string;
41
+ driver: "openclaw" | "extension";
42
+ };
43
+
44
+ function normalizeHexColor(raw: string | undefined) {
45
+ const value = (raw ?? "").trim();
46
+ if (!value) {
47
+ return DEFAULT_OPENCLAW_BROWSER_COLOR;
48
+ }
49
+ const normalized = value.startsWith("#") ? value : `#${value}`;
50
+ if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) {
51
+ return DEFAULT_OPENCLAW_BROWSER_COLOR;
52
+ }
53
+ return normalized.toUpperCase();
54
+ }
55
+
56
+ function normalizeTimeoutMs(raw: number | undefined, fallback: number) {
57
+ const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
58
+ return value < 0 ? fallback : value;
59
+ }
60
+
61
+ function normalizeStringList(raw: string[] | undefined): string[] | undefined {
62
+ if (!Array.isArray(raw) || raw.length === 0) {
63
+ return undefined;
64
+ }
65
+ const values = raw
66
+ .map((value) => value.trim())
67
+ .filter((value): value is string => value.length > 0);
68
+ return values.length > 0 ? values : undefined;
69
+ }
70
+
71
+ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
72
+ const allowPrivateNetwork = cfg?.ssrfPolicy?.allowPrivateNetwork;
73
+ const allowedHostnames = normalizeStringList(cfg?.ssrfPolicy?.allowedHostnames);
74
+ const hostnameAllowlist = normalizeStringList(cfg?.ssrfPolicy?.hostnameAllowlist);
75
+
76
+ if (
77
+ allowPrivateNetwork === undefined &&
78
+ allowedHostnames === undefined &&
79
+ hostnameAllowlist === undefined
80
+ ) {
81
+ return undefined;
82
+ }
83
+
84
+ return {
85
+ ...(allowPrivateNetwork === true ? { allowPrivateNetwork: true } : {}),
86
+ ...(allowedHostnames ? { allowedHostnames } : {}),
87
+ ...(hostnameAllowlist ? { hostnameAllowlist } : {}),
88
+ };
89
+ }
90
+
91
+ export function parseHttpUrl(raw: string, label: string) {
92
+ const trimmed = raw.trim();
93
+ const parsed = new URL(trimmed);
94
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
95
+ throw new Error(`${label} must be http(s), got: ${parsed.protocol.replace(":", "")}`);
96
+ }
97
+
98
+ const port =
99
+ parsed.port && Number.parseInt(parsed.port, 10) > 0
100
+ ? Number.parseInt(parsed.port, 10)
101
+ : parsed.protocol === "https:"
102
+ ? 443
103
+ : 80;
104
+
105
+ if (Number.isNaN(port) || port <= 0 || port > 65535) {
106
+ throw new Error(`${label} has invalid port: ${parsed.port}`);
107
+ }
108
+
109
+ return {
110
+ parsed,
111
+ port,
112
+ normalized: parsed.toString().replace(/\/$/, ""),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Ensure the default "openclaw" profile exists in the profiles map.
118
+ * Auto-creates it with the legacy CDP port (from browser.cdpUrl) or first port if missing.
119
+ */
120
+ function ensureDefaultProfile(
121
+ profiles: Record<string, BrowserProfileConfig> | undefined,
122
+ defaultColor: string,
123
+ legacyCdpPort?: number,
124
+ derivedDefaultCdpPort?: number,
125
+ ): Record<string, BrowserProfileConfig> {
126
+ const result = { ...profiles };
127
+ if (!result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]) {
128
+ result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] = {
129
+ cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? CDP_PORT_RANGE_START,
130
+ color: defaultColor,
131
+ };
132
+ }
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Ensure a built-in "chrome" profile exists for the Chrome extension relay.
138
+ *
139
+ * Note: this is an OpenClaw browser profile (routing config), not a Chrome user profile.
140
+ * It points at the local relay CDP endpoint (controlPort + 1).
141
+ */
142
+ function ensureDefaultChromeExtensionProfile(
143
+ profiles: Record<string, BrowserProfileConfig>,
144
+ controlPort: number,
145
+ ): Record<string, BrowserProfileConfig> {
146
+ const result = { ...profiles };
147
+ if (result.chrome) {
148
+ return result;
149
+ }
150
+ const relayPort = controlPort + 1;
151
+ if (!Number.isFinite(relayPort) || relayPort <= 0 || relayPort > 65535) {
152
+ return result;
153
+ }
154
+ // Avoid adding the built-in profile if the derived relay port is already used by another profile
155
+ // (legacy single-profile configs may use controlPort+1 for openclaw/openclaw CDP).
156
+ if (getUsedPorts(result).has(relayPort)) {
157
+ return result;
158
+ }
159
+ result.chrome = {
160
+ driver: "extension",
161
+ cdpUrl: `http://127.0.0.1:${relayPort}`,
162
+ color: "#00AA00",
163
+ };
164
+ return result;
165
+ }
166
+ export function resolveBrowserConfig(
167
+ cfg: BrowserConfig | undefined,
168
+ rootConfig?: OpenClawConfig,
169
+ ): ResolvedBrowserConfig {
170
+ const enabled = cfg?.enabled ?? DEFAULT_OPENCLAW_BROWSER_ENABLED;
171
+ const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
172
+ const gatewayPort = resolveGatewayPort(rootConfig);
173
+ const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT);
174
+ const defaultColor = normalizeHexColor(cfg?.color);
175
+ const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500);
176
+ const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs(
177
+ cfg?.remoteCdpHandshakeTimeoutMs,
178
+ Math.max(2000, remoteCdpTimeoutMs * 2),
179
+ );
180
+
181
+ const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
182
+
183
+ const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
184
+ let cdpInfo:
185
+ | {
186
+ parsed: URL;
187
+ port: number;
188
+ normalized: string;
189
+ }
190
+ | undefined;
191
+ if (rawCdpUrl) {
192
+ cdpInfo = parseHttpUrl(rawCdpUrl, "browser.cdpUrl");
193
+ } else {
194
+ const derivedPort = controlPort + 1;
195
+ if (derivedPort > 65535) {
196
+ throw new Error(
197
+ `Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`,
198
+ );
199
+ }
200
+ const derived = new URL(`http://127.0.0.1:${derivedPort}`);
201
+ cdpInfo = {
202
+ parsed: derived,
203
+ port: derivedPort,
204
+ normalized: derived.toString().replace(/\/$/, ""),
205
+ };
206
+ }
207
+
208
+ const headless = cfg?.headless === true;
209
+ const noSandbox = cfg?.noSandbox === true;
210
+ const attachOnly = cfg?.attachOnly === true;
211
+ const executablePath = cfg?.executablePath?.trim() || undefined;
212
+
213
+ const defaultProfileFromConfig = cfg?.defaultProfile?.trim() || undefined;
214
+ // Use legacy cdpUrl port for backward compatibility when no profiles configured
215
+ const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
216
+ const profiles = ensureDefaultChromeExtensionProfile(
217
+ ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, derivedCdpRange.start),
218
+ controlPort,
219
+ );
220
+ const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
221
+ const defaultProfile =
222
+ defaultProfileFromConfig ??
223
+ (profiles[DEFAULT_BROWSER_DEFAULT_PROFILE_NAME]
224
+ ? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME
225
+ : DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME);
226
+
227
+ const extraArgs = Array.isArray(cfg?.extraArgs)
228
+ ? cfg.extraArgs.filter((a): a is string => typeof a === "string" && a.trim().length > 0)
229
+ : [];
230
+ const ssrfPolicy = resolveBrowserSsrFPolicy(cfg);
231
+
232
+ return {
233
+ enabled,
234
+ evaluateEnabled,
235
+ controlPort,
236
+ cdpProtocol,
237
+ cdpHost: cdpInfo.parsed.hostname,
238
+ cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
239
+ remoteCdpTimeoutMs,
240
+ remoteCdpHandshakeTimeoutMs,
241
+ color: defaultColor,
242
+ executablePath,
243
+ headless,
244
+ noSandbox,
245
+ attachOnly,
246
+ defaultProfile,
247
+ profiles,
248
+ ssrfPolicy,
249
+ extraArgs,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Resolve a profile by name from the config.
255
+ * Returns null if the profile doesn't exist.
256
+ */
257
+ export function resolveProfile(
258
+ resolved: ResolvedBrowserConfig,
259
+ profileName: string,
260
+ ): ResolvedBrowserProfile | null {
261
+ const profile = resolved.profiles[profileName];
262
+ if (!profile) {
263
+ return null;
264
+ }
265
+
266
+ const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
267
+ let cdpHost = resolved.cdpHost;
268
+ let cdpPort = profile.cdpPort ?? 0;
269
+ let cdpUrl = "";
270
+ const driver = profile.driver === "extension" ? "extension" : "openclaw";
271
+
272
+ if (rawProfileUrl) {
273
+ const parsed = parseHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`);
274
+ cdpHost = parsed.parsed.hostname;
275
+ cdpPort = parsed.port;
276
+ cdpUrl = parsed.normalized;
277
+ } else if (cdpPort) {
278
+ cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`;
279
+ } else {
280
+ throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`);
281
+ }
282
+
283
+ return {
284
+ name: profileName,
285
+ cdpPort,
286
+ cdpUrl,
287
+ cdpHost,
288
+ cdpIsLoopback: isLoopbackHost(cdpHost),
289
+ color: profile.color,
290
+ driver,
291
+ };
292
+ }
293
+
294
+ export function shouldStartLocalBrowserServer(_resolved: ResolvedBrowserConfig) {
295
+ return true;
296
+ }
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
2
+ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
3
+ export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
4
+ export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
5
+ export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
6
+ export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
7
+ export const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 10_000;
8
+ export const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;