@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,518 @@
1
+ /**
2
+ * Enterprise Compatibility Layer
3
+ *
4
+ * Replaces OpenClaw framework imports (config, logging, gateway, infra, etc.)
5
+ * with enterprise-native equivalents. Single import point for all browser system files.
6
+ */
7
+
8
+ import crypto from 'node:crypto';
9
+ import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+ import net from 'node:net';
14
+ import { execFile } from 'node:child_process';
15
+
16
+ // ─── Config Types ──────────────────────────────────────────
17
+
18
+ export interface BrowserProfileConfig {
19
+ name: string;
20
+ executable?: string;
21
+ userDataDir?: string;
22
+ args?: string[];
23
+ cdpUrl?: string;
24
+ headless?: boolean;
25
+ launchTimeout?: number;
26
+ /** Allow non-loopback CDP connections */
27
+ allowRemoteCdp?: boolean;
28
+ }
29
+
30
+ export interface BrowserConfig {
31
+ enabled?: boolean;
32
+ headless?: boolean;
33
+ port?: number;
34
+ host?: string;
35
+ profiles?: Record<string, BrowserProfileConfig>;
36
+ defaultProfile?: string;
37
+ /** Max concurrent browser contexts */
38
+ maxContexts?: number;
39
+ /** Idle timeout before closing browser (ms) */
40
+ idleTimeoutMs?: number;
41
+ /** Navigation timeout (ms) */
42
+ navigationTimeoutMs?: number;
43
+ /** Screenshot quality (0-100) */
44
+ screenshotQuality?: number;
45
+ /** Max page size for content extraction */
46
+ maxContentLength?: number;
47
+ /** SSRF protection mode */
48
+ ssrfProtection?: 'strict' | 'permissive' | 'off';
49
+ /** Allowed URL patterns (when ssrfProtection is 'strict') */
50
+ allowedUrlPatterns?: string[];
51
+ /** Blocked URL patterns */
52
+ blockedUrlPatterns?: string[];
53
+ /** Allow file:// URLs */
54
+ allowFileUrls?: boolean;
55
+ /** CDP port range */
56
+ cdpPortRange?: { start: number; end: number };
57
+ /** Auth token for browser control server */
58
+ authToken?: string;
59
+ /** Temp directory for downloads/uploads */
60
+ tmpDir?: string;
61
+ /** Maximum screenshot size in bytes */
62
+ maxScreenshotBytes?: number;
63
+ /** Enable console log capture */
64
+ captureConsole?: boolean;
65
+ /** Enable request interception */
66
+ interceptRequests?: boolean;
67
+ }
68
+
69
+ export interface OpenClawConfig {
70
+ browser?: BrowserConfig;
71
+ gateway?: {
72
+ port?: number;
73
+ host?: string;
74
+ auth?: { secret?: string };
75
+ nodes?: {
76
+ browser?: { mode?: string; node?: string };
77
+ };
78
+ };
79
+ }
80
+
81
+ // ─── Runtime Config Store ──────────────────────────────────
82
+
83
+ let _currentConfig: OpenClawConfig = {};
84
+ let _configDir = path.join(os.homedir(), '.agenticmail-enterprise');
85
+
86
+ export function setEnterpriseConfig(config: OpenClawConfig) {
87
+ _currentConfig = config;
88
+ }
89
+
90
+ export function setConfigDir(dir: string) {
91
+ _configDir = dir;
92
+ }
93
+
94
+ export const CONFIG_DIR = _configDir;
95
+
96
+ export function loadConfig(): OpenClawConfig {
97
+ return _currentConfig;
98
+ }
99
+
100
+ export function createConfigIO() {
101
+ return {
102
+ load: () => _currentConfig,
103
+ save: (cfg: OpenClawConfig) => { _currentConfig = cfg; },
104
+ path: path.join(_configDir, 'config.json'),
105
+ };
106
+ }
107
+
108
+ export function writeConfigFile(cfg: OpenClawConfig) {
109
+ _currentConfig = cfg;
110
+ }
111
+
112
+ // ─── Resolved Browser Config ──────────────────────────────
113
+
114
+ export interface ResolvedBrowserConfig {
115
+ enabled: boolean;
116
+ headless: boolean;
117
+ port: number;
118
+ host: string;
119
+ profiles: Map<string, ResolvedBrowserProfile>;
120
+ defaultProfile: string;
121
+ authToken: string;
122
+ ssrfProtection: 'strict' | 'permissive' | 'off';
123
+ allowedUrlPatterns: string[];
124
+ blockedUrlPatterns: string[];
125
+ allowFileUrls: boolean;
126
+ tmpDir: string;
127
+ maxContexts: number;
128
+ idleTimeoutMs: number;
129
+ navigationTimeoutMs: number;
130
+ maxContentLength: number;
131
+ captureConsole: boolean;
132
+ maxScreenshotBytes: number;
133
+ cdpPortRange: { start: number; end: number };
134
+ }
135
+
136
+ export interface ResolvedBrowserProfile {
137
+ name: string;
138
+ executable?: string;
139
+ userDataDir?: string;
140
+ args: string[];
141
+ cdpUrl?: string;
142
+ headless: boolean;
143
+ launchTimeout: number;
144
+ allowRemoteCdp: boolean;
145
+ }
146
+
147
+ export function resolveBrowserConfig(browserCfg?: BrowserConfig, _fullCfg?: OpenClawConfig): ResolvedBrowserConfig {
148
+ const cfg = browserCfg || {};
149
+ const profiles = new Map<string, ResolvedBrowserProfile>();
150
+
151
+ if (cfg.profiles) {
152
+ for (const [name, p] of Object.entries(cfg.profiles)) {
153
+ profiles.set(name, {
154
+ name: p.name || name,
155
+ executable: p.executable,
156
+ userDataDir: p.userDataDir,
157
+ args: p.args || [],
158
+ cdpUrl: p.cdpUrl,
159
+ headless: p.headless ?? cfg.headless ?? true,
160
+ launchTimeout: p.launchTimeout ?? 30000,
161
+ allowRemoteCdp: p.allowRemoteCdp ?? false,
162
+ });
163
+ }
164
+ }
165
+
166
+ // Ensure 'openclaw' default profile exists
167
+ if (!profiles.has('openclaw')) {
168
+ profiles.set('openclaw', {
169
+ name: 'openclaw',
170
+ args: [],
171
+ headless: cfg.headless ?? true,
172
+ launchTimeout: 30000,
173
+ allowRemoteCdp: false,
174
+ });
175
+ }
176
+
177
+ return {
178
+ enabled: cfg.enabled ?? true,
179
+ headless: cfg.headless ?? true,
180
+ port: cfg.port ?? 9222,
181
+ host: cfg.host ?? '127.0.0.1',
182
+ profiles,
183
+ defaultProfile: cfg.defaultProfile || 'openclaw',
184
+ authToken: cfg.authToken || crypto.randomUUID(),
185
+ ssrfProtection: cfg.ssrfProtection || 'permissive',
186
+ allowedUrlPatterns: cfg.allowedUrlPatterns || [],
187
+ blockedUrlPatterns: cfg.blockedUrlPatterns || ['*://169.254.*', '*://metadata.google.*'],
188
+ allowFileUrls: cfg.allowFileUrls ?? false,
189
+ tmpDir: cfg.tmpDir || path.join(os.tmpdir(), 'agenticmail-browser'),
190
+ maxContexts: cfg.maxContexts ?? 10,
191
+ idleTimeoutMs: cfg.idleTimeoutMs ?? 300_000,
192
+ navigationTimeoutMs: cfg.navigationTimeoutMs ?? 30_000,
193
+ maxContentLength: cfg.maxContentLength ?? 500_000,
194
+ captureConsole: cfg.captureConsole ?? true,
195
+ maxScreenshotBytes: cfg.maxScreenshotBytes ?? 10_000_000,
196
+ cdpPortRange: cfg.cdpPortRange || { start: 9222, end: 9322 },
197
+ };
198
+ }
199
+
200
+ export function parseHttpUrl(url: string): { host: string; port: number; protocol: string } | null {
201
+ try {
202
+ const u = new URL(url);
203
+ return { host: u.hostname, port: parseInt(u.port) || (u.protocol === 'https:' ? 443 : 80), protocol: u.protocol };
204
+ } catch { return null; }
205
+ }
206
+
207
+ export function resolveProfile(profiles: Map<string, ResolvedBrowserProfile>, name?: string): ResolvedBrowserProfile | undefined {
208
+ if (!name) return profiles.values().next().value;
209
+ return profiles.get(name);
210
+ }
211
+
212
+ // ─── Logging ───────────────────────────────────────────────
213
+
214
+ export interface SubsystemLogger {
215
+ info: (...args: any[]) => void;
216
+ warn: (...args: any[]) => void;
217
+ error: (...args: any[]) => void;
218
+ debug: (...args: any[]) => void;
219
+ child: (name: string) => SubsystemLogger;
220
+ }
221
+
222
+ export function createSubsystemLogger(name: string): SubsystemLogger {
223
+ const prefix = `[browser:${name}]`;
224
+ const logger: SubsystemLogger = {
225
+ info: (...args: any[]) => console.log(prefix, ...args),
226
+ warn: (...args: any[]) => console.warn(prefix, ...args),
227
+ error: (...args: any[]) => console.error(prefix, ...args),
228
+ debug: (...args: any[]) => { /* silent in production */ },
229
+ child: (sub: string) => createSubsystemLogger(`${name}:${sub}`),
230
+ };
231
+ return logger;
232
+ }
233
+
234
+ // ─── Error Utilities ───────────────────────────────────────
235
+
236
+ export function formatErrorMessage(err: unknown): string {
237
+ if (err instanceof Error) return err.message;
238
+ if (typeof err === 'string') return err;
239
+ return String(err);
240
+ }
241
+
242
+ export function extractErrorCode(err: unknown): string | undefined {
243
+ if (err && typeof err === 'object' && 'code' in err) return String((err as any).code);
244
+ return undefined;
245
+ }
246
+
247
+ // ─── SSRF Protection ───────────────────────────────────────
248
+
249
+ export interface SsrFPolicy {
250
+ mode: 'strict' | 'permissive' | 'off';
251
+ allowedPatterns: string[];
252
+ blockedPatterns: string[];
253
+ allowFileUrls: boolean;
254
+ allowPrivateIps: boolean;
255
+ }
256
+
257
+ export class SsrFBlockedError extends Error {
258
+ constructor(url: string, reason: string) {
259
+ super(`SSRF blocked: ${url} — ${reason}`);
260
+ this.name = 'SsrFBlockedError';
261
+ }
262
+ }
263
+
264
+ const PRIVATE_IP_RANGES = [
265
+ /^10\./,
266
+ /^172\.(1[6-9]|2\d|3[01])\./,
267
+ /^192\.168\./,
268
+ /^127\./,
269
+ /^169\.254\./,
270
+ /^0\./,
271
+ /^fc00:/i,
272
+ /^fd/i,
273
+ /^fe80:/i,
274
+ /^::1$/,
275
+ /^localhost$/i,
276
+ ];
277
+
278
+ export function isPrivateHost(host: string): boolean {
279
+ return PRIVATE_IP_RANGES.some(re => re.test(host));
280
+ }
281
+
282
+ export function validateSsrf(url: string, policy: SsrFPolicy): void {
283
+ if (policy.mode === 'off') return;
284
+
285
+ let parsed: URL;
286
+ try { parsed = new URL(url); } catch { throw new SsrFBlockedError(url, 'Invalid URL'); }
287
+
288
+ if (!policy.allowFileUrls && parsed.protocol === 'file:') {
289
+ throw new SsrFBlockedError(url, 'file:// URLs not allowed');
290
+ }
291
+
292
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' && parsed.protocol !== 'file:') {
293
+ throw new SsrFBlockedError(url, `Protocol ${parsed.protocol} not allowed`);
294
+ }
295
+
296
+ for (const pattern of policy.blockedPatterns) {
297
+ if (matchUrlPattern(url, pattern)) {
298
+ throw new SsrFBlockedError(url, `Matched blocked pattern: ${pattern}`);
299
+ }
300
+ }
301
+
302
+ if (policy.mode === 'strict') {
303
+ if (!policy.allowPrivateIps && isPrivateHost(parsed.hostname)) {
304
+ throw new SsrFBlockedError(url, 'Private/internal IP addresses not allowed');
305
+ }
306
+ if (policy.allowedPatterns.length > 0) {
307
+ const allowed = policy.allowedPatterns.some(p => matchUrlPattern(url, p));
308
+ if (!allowed) throw new SsrFBlockedError(url, 'URL not in allowlist');
309
+ }
310
+ }
311
+ }
312
+
313
+ function matchUrlPattern(url: string, pattern: string): boolean {
314
+ const regex = pattern
315
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
316
+ .replace(/\*/g, '.*');
317
+ return new RegExp(`^${regex}$`, 'i').test(url);
318
+ }
319
+
320
+ // ─── Network Utilities ─────────────────────────────────────
321
+
322
+ export function isLoopbackHost(host: string): boolean {
323
+ return /^(127\.\d+\.\d+\.\d+|::1|localhost)$/i.test(host);
324
+ }
325
+
326
+ export function isLoopbackAddress(addr: string): boolean {
327
+ return isLoopbackHost(addr);
328
+ }
329
+
330
+ export async function ensurePortAvailable(port: number, host?: string): Promise<boolean> {
331
+ return new Promise((resolve) => {
332
+ const server = net.createServer();
333
+ server.once('error', () => resolve(false));
334
+ server.once('listening', () => { server.close(() => resolve(true)); });
335
+ server.listen(port, host || '127.0.0.1');
336
+ });
337
+ }
338
+
339
+ // ─── Gateway / Auth ────────────────────────────────────────
340
+
341
+ export function resolveGatewayAuth(): { secret: string } | null {
342
+ const cfg = loadConfig();
343
+ const secret = cfg.gateway?.auth?.secret;
344
+ return secret ? { secret } : null;
345
+ }
346
+
347
+ export function resolveGatewayPort(): number {
348
+ return loadConfig().gateway?.port || 3000;
349
+ }
350
+
351
+ export function ensureGatewayStartupAuth(): { secret: string } {
352
+ const auth = resolveGatewayAuth();
353
+ if (auth) return auth;
354
+ const secret = crypto.randomUUID();
355
+ return { secret };
356
+ }
357
+
358
+ // ─── Paths ─────────────────────────────────────────────────
359
+
360
+ export function resolvePreferredOpenClawTmpDir(): string {
361
+ const dir = path.join(os.tmpdir(), 'agenticmail-enterprise');
362
+ fs.mkdirSync(dir, { recursive: true });
363
+ return dir;
364
+ }
365
+
366
+ export const DEFAULT_UPLOAD_DIR = path.join(os.tmpdir(), 'agenticmail-uploads');
367
+
368
+ export function resolvePathsWithinRoot(root: string, ...paths: string[]): string[] {
369
+ return paths.map(p => {
370
+ const resolved = path.resolve(root, p);
371
+ if (!resolved.startsWith(root)) throw new Error(`Path traversal blocked: ${p}`);
372
+ return resolved;
373
+ });
374
+ }
375
+
376
+ // ─── Port Defaults ─────────────────────────────────────────
377
+
378
+ export function deriveDefaultBrowserCdpPortRange(): { start: number; end: number } {
379
+ return { start: 9222, end: 9322 };
380
+ }
381
+
382
+ // ─── WebSocket Utilities ───────────────────────────────────
383
+
384
+ export function rawDataToString(data: any): string {
385
+ if (typeof data === 'string') return data;
386
+ if (Buffer.isBuffer(data)) return data.toString('utf-8');
387
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf-8');
388
+ if (Array.isArray(data)) return Buffer.concat(data).toString('utf-8');
389
+ return String(data);
390
+ }
391
+
392
+ // ─── CLI / Formatting ──────────────────────────────────────
393
+
394
+ export function formatCliCommand(cmd: string, args: string[]): string {
395
+ return [cmd, ...args].join(' ');
396
+ }
397
+
398
+ // ─── Security ──────────────────────────────────────────────
399
+
400
+ export function safeEqualSecret(a: string, b: string): boolean {
401
+ if (a.length !== b.length) return false;
402
+ const bufA = Buffer.from(a);
403
+ const bufB = Buffer.from(b);
404
+ return crypto.timingSafeEqual(bufA, bufB);
405
+ }
406
+
407
+ // ─── Media ─────────────────────────────────────────────────
408
+
409
+ export async function saveMediaBuffer(buf: Buffer, opts?: { ext?: string; dir?: string }): Promise<string> {
410
+ const dir = opts?.dir || path.join(os.tmpdir(), 'agenticmail-media');
411
+ await fsp.mkdir(dir, { recursive: true });
412
+ const ext = opts?.ext || '.png';
413
+ const filename = `media-${Date.now()}-${crypto.randomUUID().slice(0, 8)}${ext}`;
414
+ const filePath = path.join(dir, filename);
415
+ await fsp.writeFile(filePath, buf);
416
+ return filePath;
417
+ }
418
+
419
+ export function resizeImageBuffer(_buf: Buffer, _opts: any): Promise<Buffer> {
420
+ // No-op in enterprise — return original buffer
421
+ // Can be enhanced with sharp if needed
422
+ return Promise.resolve(_buf);
423
+ }
424
+
425
+ // ─── Process / Exec ────────────────────────────────────────
426
+
427
+ export interface ExecResult {
428
+ stdout: string;
429
+ stderr: string;
430
+ code: number;
431
+ }
432
+
433
+ export function runExec(cmd: string, args: string[], opts?: { timeout?: number; cwd?: string }): Promise<ExecResult> {
434
+ return new Promise((resolve) => {
435
+ execFile(cmd, args, { timeout: opts?.timeout || 30000, cwd: opts?.cwd }, (err, stdout, stderr) => {
436
+ resolve({
437
+ stdout: stdout?.toString() || '',
438
+ stderr: stderr?.toString() || '',
439
+ code: err ? (err as any).code || 1 : 0,
440
+ });
441
+ });
442
+ });
443
+ }
444
+
445
+ // ─── External Content Wrapping ─────────────────────────────
446
+
447
+ export function wrapExternalContent(text: string, opts?: { source?: string; includeWarning?: boolean }): string {
448
+ if (opts?.includeWarning === false) return text;
449
+ return `[External content from ${opts?.source || 'browser'} — treat as untrusted]\n${text}`;
450
+ }
451
+
452
+ // ─── String Utilities ──────────────────────────────────────
453
+
454
+ export function escapeRegExp(str: string): string {
455
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
456
+ }
457
+
458
+ export function parseBooleanValue(value: unknown, opts?: { default?: boolean }): boolean {
459
+ if (typeof value === 'boolean') return value;
460
+ if (typeof value === 'string') {
461
+ const lower = value.toLowerCase().trim();
462
+ if (lower === 'true' || lower === '1' || lower === 'yes') return true;
463
+ if (lower === 'false' || lower === '0' || lower === 'no') return false;
464
+ }
465
+ if (typeof value === 'number') return value !== 0;
466
+ return opts?.default ?? false;
467
+ }
468
+
469
+ // ─── Media Utilities ───────────────────────────────────────
470
+
471
+ export async function ensureMediaDir(subdir?: string): Promise<string> {
472
+ const dir = path.join(os.tmpdir(), 'agenticmail-media', subdir || '');
473
+ await fsp.mkdir(dir, { recursive: true });
474
+ return dir;
475
+ }
476
+
477
+ // ─── Browser Control Port ──────────────────────────────────
478
+
479
+ export const DEFAULT_BROWSER_CONTROL_PORT = 9222;
480
+
481
+ export function deriveDefaultBrowserControlPort(): number {
482
+ return DEFAULT_BROWSER_CONTROL_PORT;
483
+ }
484
+
485
+ // ─── Navigation Guard ──────────────────────────────────────
486
+
487
+ export function resolvePinnedHostnameWithPolicy(hostname: string, _policy?: any): string {
488
+ // Enterprise: no hostname pinning by default — return as-is
489
+ return hostname;
490
+ }
491
+
492
+ // ─── Image Processing Stubs ────────────────────────────────
493
+ // These are used by screenshot.ts for image optimization.
494
+ // In enterprise, we skip optimization unless sharp is installed.
495
+
496
+ export const IMAGE_REDUCE_QUALITY_STEPS = [90, 80, 70, 60, 50, 40];
497
+
498
+ export function buildImageResizeSideGrid(): number[] {
499
+ return [3840, 2560, 1920, 1440, 1280, 1024, 800, 640];
500
+ }
501
+
502
+ export async function getImageMetadata(buf: Buffer): Promise<{ width: number; height: number; format: string } | null> {
503
+ // Without sharp, just return null — screenshot won't be resized
504
+ try {
505
+ // Try to detect PNG dimensions from header
506
+ if (buf[0] === 0x89 && buf[1] === 0x50) { // PNG
507
+ const width = buf.readUInt32BE(16);
508
+ const height = buf.readUInt32BE(20);
509
+ return { width, height, format: 'png' };
510
+ }
511
+ return null;
512
+ } catch { return null; }
513
+ }
514
+
515
+ export async function resizeToJpeg(buf: Buffer, _opts?: { width?: number; quality?: number }): Promise<Buffer> {
516
+ // No-op without sharp — return original buffer
517
+ return buf;
518
+ }