agent-browser-stealth 0.17.0-fork.2 → 0.24.0-fork.2

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 (122) hide show
  1. package/README.md +1256 -240
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/bin/agent-browser.js +13 -2
  8. package/extensions/tab-group-cdp/content-script.js +425 -0
  9. package/extensions/tab-group-cdp/icons/icon.svg +7 -0
  10. package/extensions/tab-group-cdp/manifest.json +34 -0
  11. package/extensions/tab-group-cdp/page-bridge.js +133 -0
  12. package/extensions/tab-group-cdp/service-worker.js +2249 -0
  13. package/extensions/tab-group-cdp/sidepanel.css +258 -0
  14. package/extensions/tab-group-cdp/sidepanel.html +28 -0
  15. package/extensions/tab-group-cdp/sidepanel.js +1225 -0
  16. package/package.json +17 -69
  17. package/scripts/build-all-platforms.sh +6 -0
  18. package/scripts/check-version-sync.js +14 -2
  19. package/scripts/copy-native.js +8 -50
  20. package/scripts/postinstall.js +149 -165
  21. package/scripts/windows-debug/provision.sh +220 -0
  22. package/scripts/windows-debug/run.sh +92 -0
  23. package/scripts/windows-debug/start.sh +43 -0
  24. package/scripts/windows-debug/stop.sh +28 -0
  25. package/scripts/windows-debug/sync.sh +27 -0
  26. package/skills/agent-browser/SKILL.md +256 -159
  27. package/skills/agent-browser/references/authentication.md +101 -0
  28. package/skills/agent-browser/references/commands.md +34 -2
  29. package/skills/agent-browser/references/snapshot-refs.md +25 -0
  30. package/skills/agentcore/SKILL.md +115 -0
  31. package/skills/dogfood/SKILL.md +4 -2
  32. package/skills/electron/SKILL.md +26 -2
  33. package/skills/slack/SKILL.md +0 -9
  34. package/skills/slack/references/slack-tasks.md +2 -8
  35. package/skills/vercel-sandbox/SKILL.md +280 -0
  36. package/bin/agent-browser-local +0 -0
  37. package/bin/agent-browser-stealth +0 -0
  38. package/bin/agent-browser-stealth.d +0 -1
  39. package/dist/action-policy.d.ts +0 -14
  40. package/dist/action-policy.d.ts.map +0 -1
  41. package/dist/action-policy.js +0 -253
  42. package/dist/action-policy.js.map +0 -1
  43. package/dist/actions.d.ts +0 -21
  44. package/dist/actions.d.ts.map +0 -1
  45. package/dist/actions.js +0 -2139
  46. package/dist/actions.js.map +0 -1
  47. package/dist/auth-cli.d.ts +0 -2
  48. package/dist/auth-cli.d.ts.map +0 -1
  49. package/dist/auth-cli.js +0 -97
  50. package/dist/auth-cli.js.map +0 -1
  51. package/dist/auth-vault.d.ts +0 -36
  52. package/dist/auth-vault.d.ts.map +0 -1
  53. package/dist/auth-vault.js +0 -125
  54. package/dist/auth-vault.js.map +0 -1
  55. package/dist/browser.d.ts +0 -665
  56. package/dist/browser.d.ts.map +0 -1
  57. package/dist/browser.js +0 -3210
  58. package/dist/browser.js.map +0 -1
  59. package/dist/confirmation.d.ts +0 -8
  60. package/dist/confirmation.d.ts.map +0 -1
  61. package/dist/confirmation.js +0 -30
  62. package/dist/confirmation.js.map +0 -1
  63. package/dist/daemon.d.ts +0 -78
  64. package/dist/daemon.d.ts.map +0 -1
  65. package/dist/daemon.js +0 -744
  66. package/dist/daemon.js.map +0 -1
  67. package/dist/diff.d.ts +0 -18
  68. package/dist/diff.d.ts.map +0 -1
  69. package/dist/diff.js +0 -271
  70. package/dist/diff.js.map +0 -1
  71. package/dist/domain-filter.d.ts +0 -28
  72. package/dist/domain-filter.d.ts.map +0 -1
  73. package/dist/domain-filter.js +0 -149
  74. package/dist/domain-filter.js.map +0 -1
  75. package/dist/encryption.d.ts +0 -73
  76. package/dist/encryption.d.ts.map +0 -1
  77. package/dist/encryption.js +0 -171
  78. package/dist/encryption.js.map +0 -1
  79. package/dist/ios-actions.d.ts +0 -11
  80. package/dist/ios-actions.d.ts.map +0 -1
  81. package/dist/ios-actions.js +0 -228
  82. package/dist/ios-actions.js.map +0 -1
  83. package/dist/ios-manager.d.ts +0 -266
  84. package/dist/ios-manager.d.ts.map +0 -1
  85. package/dist/ios-manager.js +0 -1073
  86. package/dist/ios-manager.js.map +0 -1
  87. package/dist/protocol.d.ts +0 -26
  88. package/dist/protocol.d.ts.map +0 -1
  89. package/dist/protocol.js +0 -990
  90. package/dist/protocol.js.map +0 -1
  91. package/dist/snapshot.d.ts +0 -67
  92. package/dist/snapshot.d.ts.map +0 -1
  93. package/dist/snapshot.js +0 -514
  94. package/dist/snapshot.js.map +0 -1
  95. package/dist/state-utils.d.ts +0 -77
  96. package/dist/state-utils.d.ts.map +0 -1
  97. package/dist/state-utils.js +0 -178
  98. package/dist/state-utils.js.map +0 -1
  99. package/dist/stealth.d.ts +0 -41
  100. package/dist/stealth.d.ts.map +0 -1
  101. package/dist/stealth.js +0 -1743
  102. package/dist/stealth.js.map +0 -1
  103. package/dist/stream-server.d.ts +0 -117
  104. package/dist/stream-server.d.ts.map +0 -1
  105. package/dist/stream-server.js +0 -309
  106. package/dist/stream-server.js.map +0 -1
  107. package/dist/types.d.ts +0 -973
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/types.js +0 -2
  110. package/dist/types.js.map +0 -1
  111. package/scripts/check-creepjs-headless.js +0 -137
  112. package/scripts/check-daemon-pid-recovery.js +0 -148
  113. package/scripts/check-sannysoft-webdriver.js +0 -112
  114. package/scripts/check-stealth-regression.js +0 -199
  115. package/scripts/check-turnstile-testkey.ts +0 -125
  116. package/scripts/clawhub-sync.sh +0 -27
  117. package/scripts/sync-upstream.sh +0 -142
  118. package/scripts/verify-bundled-binaries.js +0 -71
  119. package/scripts/verify-native-version.js +0 -48
  120. package/scripts/verify-packed-host-binary.js +0 -88
  121. package/scripts/verify-registry-host-binary.js +0 -120
  122. package/skills/agent-browser-stealth/SKILL.md +0 -127
package/dist/daemon.js DELETED
@@ -1,744 +0,0 @@
1
- import * as net from 'net';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import * as os from 'os';
5
- import { BrowserManager } from './browser.js';
6
- import { IOSManager } from './ios-manager.js';
7
- import { parseCommand, serializeResponse, errorResponse } from './protocol.js';
8
- import { executeCommand } from './actions.js';
9
- import { executeIOSCommand } from './ios-actions.js';
10
- import { StreamServer } from './stream-server.js';
11
- import { getEncryptionKey, encryptData, isValidSessionName, cleanupExpiredStates, getAutoStateFilePath, } from './state-utils.js';
12
- /**
13
- * Backpressure-aware socket write.
14
- * If the kernel buffer is full (socket.write returns false),
15
- * waits for the 'drain' event before resolving.
16
- */
17
- export function safeWrite(socket, payload) {
18
- return new Promise((resolve, reject) => {
19
- if (socket.destroyed) {
20
- resolve();
21
- return;
22
- }
23
- const canContinue = socket.write(payload);
24
- if (canContinue) {
25
- resolve();
26
- }
27
- else if (socket.destroyed) {
28
- resolve();
29
- }
30
- else {
31
- const cleanup = () => {
32
- socket.removeListener('drain', onDrain);
33
- socket.removeListener('error', onError);
34
- socket.removeListener('close', onClose);
35
- };
36
- const onDrain = () => {
37
- cleanup();
38
- resolve();
39
- };
40
- const onError = (err) => {
41
- cleanup();
42
- reject(err);
43
- };
44
- const onClose = () => {
45
- cleanup();
46
- resolve();
47
- };
48
- socket.once('drain', onDrain);
49
- socket.once('error', onError);
50
- socket.once('close', onClose);
51
- }
52
- });
53
- }
54
- /**
55
- * Create an async executor that runs tasks strictly one-by-one.
56
- * Used to serialize daemon commands across all client connections.
57
- */
58
- export function createSerializedExecutor() {
59
- let tail = Promise.resolve();
60
- return async function runSerialized(task) {
61
- const run = tail.then(task, task);
62
- tail = run.then(() => undefined, () => undefined);
63
- return run;
64
- };
65
- }
66
- // Platform detection
67
- const isWindows = process.platform === 'win32';
68
- // Session support - each session gets its own socket/pid
69
- let currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
70
- // Stream server for browser preview
71
- let streamServer = null;
72
- // Default stream port (can be overridden with AGENT_BROWSER_STREAM_PORT)
73
- const DEFAULT_STREAM_PORT = 9223;
74
- // Default idle auto-shutdown timeout: 10 minutes
75
- const DEFAULT_IDLE_SHUTDOWN_MS = 10 * 60 * 1000;
76
- /**
77
- * Save state to file with optional encryption.
78
- */
79
- async function saveStateToFile(browser, filepath) {
80
- const context = browser.getContext();
81
- if (!context) {
82
- throw new Error('No browser context available');
83
- }
84
- const state = await context.storageState();
85
- const jsonData = JSON.stringify(state, null, 2);
86
- const key = getEncryptionKey();
87
- if (key) {
88
- const encrypted = encryptData(jsonData, key);
89
- fs.writeFileSync(filepath, JSON.stringify(encrypted, null, 2));
90
- return { encrypted: true };
91
- }
92
- fs.writeFileSync(filepath, jsonData);
93
- return { encrypted: false };
94
- }
95
- const AUTO_EXPIRE_ENV = 'AGENT_BROWSER_STATE_EXPIRE_DAYS';
96
- const DEFAULT_EXPIRE_DAYS = 30;
97
- function runCleanupExpiredStates() {
98
- const expireDaysStr = process.env[AUTO_EXPIRE_ENV];
99
- const expireDays = expireDaysStr ? parseInt(expireDaysStr, 10) : DEFAULT_EXPIRE_DAYS;
100
- if (isNaN(expireDays) || expireDays <= 0) {
101
- return;
102
- }
103
- try {
104
- const deleted = cleanupExpiredStates(expireDays);
105
- if (deleted.length > 0 && process.env.AGENT_BROWSER_DEBUG === '1') {
106
- console.error(`[DEBUG] Auto-expired ${deleted.length} state file(s) older than ${expireDays} days`);
107
- }
108
- }
109
- catch (err) {
110
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
111
- console.error(`[DEBUG] Failed to clean up expired states:`, err);
112
- }
113
- }
114
- }
115
- /**
116
- * Get the validated session name and auto-state file path.
117
- * Centralizes session name validation to prevent path traversal.
118
- */
119
- function getSessionAutoStatePath() {
120
- const sessionNameRaw = process.env.AGENT_BROWSER_SESSION_NAME;
121
- if (!sessionNameRaw)
122
- return undefined;
123
- if (!isValidSessionName(sessionNameRaw)) {
124
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
125
- console.error(`[SECURITY] Invalid session name rejected: ${sessionNameRaw}`);
126
- }
127
- return undefined;
128
- }
129
- const sessionId = process.env.AGENT_BROWSER_SESSION || 'default';
130
- try {
131
- const autoStatePath = getAutoStateFilePath(sessionNameRaw, sessionId);
132
- return autoStatePath && fs.existsSync(autoStatePath) ? autoStatePath : undefined;
133
- }
134
- catch {
135
- return undefined;
136
- }
137
- }
138
- /**
139
- * Get the auto-state file path for saving (creates sessions dir if needed).
140
- * Returns undefined if no valid session name is configured.
141
- */
142
- function getSessionSaveStatePath() {
143
- const sessionNameRaw = process.env.AGENT_BROWSER_SESSION_NAME;
144
- if (!sessionNameRaw)
145
- return undefined;
146
- if (!isValidSessionName(sessionNameRaw))
147
- return undefined;
148
- const sessionId = process.env.AGENT_BROWSER_SESSION || 'default';
149
- try {
150
- return getAutoStateFilePath(sessionNameRaw, sessionId) ?? undefined;
151
- }
152
- catch {
153
- return undefined;
154
- }
155
- }
156
- /**
157
- * Set the current session
158
- */
159
- export function setSession(session) {
160
- currentSession = session;
161
- }
162
- /**
163
- * Get the current session
164
- */
165
- export function getSession() {
166
- return currentSession;
167
- }
168
- function parseEnvList(value) {
169
- if (!value)
170
- return undefined;
171
- const items = value
172
- .split(/[,\n]/)
173
- .map((item) => item.trim())
174
- .filter((item) => item.length > 0);
175
- return items.length > 0 ? items : undefined;
176
- }
177
- export function buildAutoLaunchOptionsFromEnv(autoStateFilePath = getSessionAutoStatePath()) {
178
- const proxyServer = process.env.AGENT_BROWSER_PROXY;
179
- const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
180
- const colorSchemeEnv = process.env.AGENT_BROWSER_COLOR_SCHEME;
181
- const colorScheme = colorSchemeEnv === 'dark' || colorSchemeEnv === 'light' || colorSchemeEnv === 'no-preference'
182
- ? colorSchemeEnv
183
- : undefined;
184
- const tabGroup = process.env.AGENT_BROWSER_TAB_GROUP?.trim();
185
- const tabGroupPluginId = process.env.AGENT_BROWSER_TAB_GROUP_PLUGIN_ID?.trim();
186
- return {
187
- id: 'auto',
188
- action: 'launch',
189
- // Accept both AGENT_BROWSER_HEADED=1 and =true for daemon auto-launch.
190
- headless: process.env.AGENT_BROWSER_HEADED !== '1' && process.env.AGENT_BROWSER_HEADED !== 'true',
191
- executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
192
- extensions: parseEnvList(process.env.AGENT_BROWSER_EXTENSIONS),
193
- storageState: process.env.AGENT_BROWSER_STATE,
194
- args: parseEnvList(process.env.AGENT_BROWSER_ARGS),
195
- userAgent: process.env.AGENT_BROWSER_USER_AGENT,
196
- proxy: proxyServer
197
- ? {
198
- server: proxyServer,
199
- ...(proxyBypass && { bypass: proxyBypass }),
200
- }
201
- : undefined,
202
- ignoreHTTPSErrors: process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1',
203
- allowFileAccess: process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1',
204
- colorScheme,
205
- tabGroup: tabGroup && tabGroup.length > 0 ? tabGroup : undefined,
206
- tabGroupPluginId: tabGroupPluginId && tabGroupPluginId.length > 0 ? tabGroupPluginId : undefined,
207
- autoStateFilePath,
208
- };
209
- }
210
- /**
211
- * Get port number for TCP mode (Windows)
212
- * Uses a hash of the session name to get a consistent port
213
- */
214
- function getPortForSession(session) {
215
- let hash = 0;
216
- for (let i = 0; i < session.length; i++) {
217
- hash = (hash << 5) - hash + session.charCodeAt(i);
218
- hash |= 0;
219
- }
220
- // Port range 49152-65535 (dynamic/private ports)
221
- return 49152 + (Math.abs(hash) % 16383);
222
- }
223
- /**
224
- * Get the base directory for socket/pid files.
225
- * Priority: AGENT_BROWSER_SOCKET_DIR > XDG_RUNTIME_DIR > ~/.agent-browser > tmpdir
226
- */
227
- export function getAppDir() {
228
- // 1. XDG_RUNTIME_DIR (Linux standard)
229
- if (process.env.XDG_RUNTIME_DIR) {
230
- return path.join(process.env.XDG_RUNTIME_DIR, 'agent-browser');
231
- }
232
- // 2. Home directory fallback (like Docker Desktop's ~/.docker/run/)
233
- const homeDir = os.homedir();
234
- if (homeDir) {
235
- return path.join(homeDir, '.agent-browser');
236
- }
237
- // 3. Last resort: temp dir
238
- return path.join(os.tmpdir(), 'agent-browser');
239
- }
240
- export function getSocketDir() {
241
- // Allow explicit override for socket directory
242
- if (process.env.AGENT_BROWSER_SOCKET_DIR) {
243
- return process.env.AGENT_BROWSER_SOCKET_DIR;
244
- }
245
- return getAppDir();
246
- }
247
- /**
248
- * Get the socket path for the current session (Unix) or port (Windows)
249
- */
250
- export function getSocketPath(session) {
251
- const sess = session ?? currentSession;
252
- if (isWindows) {
253
- return String(getPortForSession(sess));
254
- }
255
- return path.join(getSocketDir(), `${sess}.sock`);
256
- }
257
- /**
258
- * Get the port file path for Windows (stores the port number)
259
- */
260
- export function getPortFile(session) {
261
- const sess = session ?? currentSession;
262
- return path.join(getSocketDir(), `${sess}.port`);
263
- }
264
- /**
265
- * Get the PID file path for the current session
266
- */
267
- export function getPidFile(session) {
268
- const sess = session ?? currentSession;
269
- return path.join(getSocketDir(), `${sess}.pid`);
270
- }
271
- /**
272
- * Get the daemon metadata file path for a session.
273
- */
274
- export function getMetaFile(session) {
275
- const sess = session ?? currentSession;
276
- return path.join(getSocketDir(), `${sess}.meta.json`);
277
- }
278
- /**
279
- * Check if daemon is running for the current session
280
- */
281
- export function isDaemonRunning(session) {
282
- const pidFile = getPidFile(session);
283
- if (!fs.existsSync(pidFile))
284
- return false;
285
- try {
286
- const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
287
- // Check if process exists (works on both Unix and Windows)
288
- process.kill(pid, 0);
289
- return true;
290
- }
291
- catch (err) {
292
- // EPERM means the process exists but we lack permission to signal it
293
- // (e.g. caller is inside a macOS sandbox). Only ESRCH means it's gone.
294
- if (err instanceof Error && err.code === 'EPERM') {
295
- return true;
296
- }
297
- // Process doesn't exist, clean up stale files
298
- cleanupSocket(session);
299
- return false;
300
- }
301
- }
302
- /**
303
- * Get connection info for the current session
304
- * Returns { type: 'unix', path: string } or { type: 'tcp', port: number }
305
- */
306
- export function getConnectionInfo(session) {
307
- const sess = session ?? currentSession;
308
- if (isWindows) {
309
- return { type: 'tcp', port: getPortForSession(sess) };
310
- }
311
- return { type: 'unix', path: path.join(getSocketDir(), `${sess}.sock`) };
312
- }
313
- /**
314
- * Clean up socket and PID file for the current session
315
- */
316
- export function cleanupSocket(session) {
317
- const pidFile = getPidFile(session);
318
- const streamPortFile = getStreamPortFile(session);
319
- const metaFile = getMetaFile(session);
320
- try {
321
- if (fs.existsSync(pidFile))
322
- fs.unlinkSync(pidFile);
323
- if (fs.existsSync(streamPortFile))
324
- fs.unlinkSync(streamPortFile);
325
- if (fs.existsSync(metaFile))
326
- fs.unlinkSync(metaFile);
327
- if (isWindows) {
328
- const portFile = getPortFile(session);
329
- if (fs.existsSync(portFile))
330
- fs.unlinkSync(portFile);
331
- }
332
- else {
333
- const socketPath = getSocketPath(session);
334
- if (fs.existsSync(socketPath))
335
- fs.unlinkSync(socketPath);
336
- }
337
- }
338
- catch {
339
- // Ignore cleanup errors
340
- }
341
- }
342
- /**
343
- * Get the stream port file path
344
- */
345
- export function getStreamPortFile(session) {
346
- const sess = session ?? currentSession;
347
- return path.join(getSocketDir(), `${sess}.stream`);
348
- }
349
- /**
350
- * Start the daemon server
351
- * @param options.streamPort Port for WebSocket stream server (0 to disable)
352
- * @param options.provider Provider type ('ios' for iOS Simulator, undefined for desktop)
353
- */
354
- export async function startDaemon(options) {
355
- // Ensure socket directory exists with restricted permissions (owner-only access)
356
- const socketDir = getSocketDir();
357
- if (!fs.existsSync(socketDir)) {
358
- fs.mkdirSync(socketDir, { recursive: true, mode: 0o700 });
359
- }
360
- // Clean up any stale socket
361
- cleanupSocket();
362
- // Clean up expired state files on startup
363
- runCleanupExpiredStates();
364
- // Determine provider from options or environment
365
- const provider = options?.provider ?? process.env.AGENT_BROWSER_PROVIDER;
366
- const isIOS = provider === 'ios';
367
- // Create appropriate manager
368
- const manager = isIOS ? new IOSManager() : new BrowserManager();
369
- let shuttingDown = false;
370
- const runSerialized = createSerializedExecutor();
371
- const residentMode = options?.resident ?? process.argv.includes('--resident');
372
- let idleTimer = null;
373
- let pendingCommands = 0;
374
- // Start stream server if port is specified (or use default if env var is set)
375
- // Note: Stream server only works with BrowserManager (desktop), not iOS
376
- const streamPort = options?.streamPort ??
377
- (process.env.AGENT_BROWSER_STREAM_PORT
378
- ? parseInt(process.env.AGENT_BROWSER_STREAM_PORT, 10)
379
- : 0);
380
- if (streamPort > 0 && !isIOS && manager instanceof BrowserManager) {
381
- streamServer = new StreamServer(manager, streamPort);
382
- await streamServer.start();
383
- // Write stream port to file for clients to discover
384
- const streamPortFile = getStreamPortFile();
385
- fs.writeFileSync(streamPortFile, streamPort.toString());
386
- }
387
- const cancelIdleTimer = () => {
388
- if (idleTimer) {
389
- clearTimeout(idleTimer);
390
- idleTimer = null;
391
- }
392
- };
393
- const scheduleIdleShutdown = () => {
394
- if (residentMode || shuttingDown || pendingCommands > 0)
395
- return;
396
- cancelIdleTimer();
397
- idleTimer = setTimeout(() => {
398
- void shutdown('idle timeout');
399
- }, DEFAULT_IDLE_SHUTDOWN_MS);
400
- };
401
- const server = net.createServer((socket) => {
402
- let buffer = '';
403
- let httpChecked = false;
404
- // Command serialization: queue incoming lines and process them one at a time.
405
- // This prevents concurrent command execution which can cause socket.write
406
- // buffer contention and EAGAIN errors on the Rust CLI side.
407
- const commandQueue = [];
408
- let processing = false;
409
- async function processQueue() {
410
- if (processing)
411
- return;
412
- processing = true;
413
- while (commandQueue.length > 0) {
414
- const line = commandQueue.shift();
415
- pendingCommands += 1;
416
- cancelIdleTimer();
417
- try {
418
- await runSerialized(async () => {
419
- try {
420
- const parseResult = parseCommand(line);
421
- if (!parseResult.success) {
422
- const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
423
- await safeWrite(socket, serializeResponse(resp) + '\n');
424
- return;
425
- }
426
- // Handle device_list specially - it works without a session and always uses IOSManager
427
- if (parseResult.command.action === 'device_list') {
428
- const iosManager = new IOSManager();
429
- try {
430
- const devices = await iosManager.listAllDevices();
431
- const response = {
432
- id: parseResult.command.id,
433
- success: true,
434
- data: { devices },
435
- };
436
- await safeWrite(socket, serializeResponse(response) + '\n');
437
- }
438
- catch (err) {
439
- const message = err instanceof Error ? err.message : String(err);
440
- await safeWrite(socket, serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
441
- }
442
- return;
443
- }
444
- // Auto-launch if not already launched and this isn't a launch/close/state_load command.
445
- // Default behavior for this fork: attach to an existing browser only.
446
- const isDoctor = parseResult.command.action === 'doctor';
447
- if (!manager.isLaunched() &&
448
- parseResult.command.action !== 'launch' &&
449
- parseResult.command.action !== 'close' &&
450
- parseResult.command.action !== 'state_load' &&
451
- parseResult.command.action !== 'doctor') {
452
- if (isIOS && manager instanceof IOSManager) {
453
- // Auto-launch iOS Safari
454
- // Check for device in command first (for reused daemons), then fall back to env vars
455
- const cmd = parseResult.command;
456
- const iosDevice = cmd.iosDevice || process.env.AGENT_BROWSER_IOS_DEVICE;
457
- await manager.launch({
458
- device: iosDevice,
459
- udid: process.env.AGENT_BROWSER_IOS_UDID,
460
- });
461
- }
462
- else if (manager instanceof BrowserManager) {
463
- // Auto-launch desktop browser
464
- const launchOptions = buildAutoLaunchOptionsFromEnv();
465
- let attachedToManagedBrowser = false;
466
- try {
467
- // Keep the preferred localhost:9333 path minimal so the daemon can
468
- // connect to or auto-start the dedicated automation Chrome profile.
469
- const cdpLaunchOptions = {
470
- id: launchOptions.id,
471
- action: launchOptions.action,
472
- cdpPort: 9333,
473
- ignoreHTTPSErrors: launchOptions.ignoreHTTPSErrors,
474
- colorScheme: launchOptions.colorScheme,
475
- userAgent: launchOptions.userAgent,
476
- tabGroup: launchOptions.tabGroup,
477
- tabGroupPluginId: launchOptions.tabGroupPluginId,
478
- };
479
- await manager.launch({
480
- ...cdpLaunchOptions,
481
- });
482
- attachedToManagedBrowser = true;
483
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
484
- console.error('[DEBUG] Auto-launch connected via managed CDP port 9333');
485
- }
486
- }
487
- catch (error) {
488
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
489
- const message = error instanceof Error ? error.message : String(error);
490
- console.error(`[DEBUG] Managed CDP port 9333 unavailable: ${message}`);
491
- }
492
- }
493
- if (!attachedToManagedBrowser) {
494
- throw new Error('Project policy requires using the dedicated automation browser on localhost:9333. Could not connect to or auto-start the managed Chrome profile.');
495
- }
496
- }
497
- }
498
- // For doctor, attempt the same managed localhost:9333 flow but do not fail hard if attach is unavailable.
499
- // This keeps diagnostics actionable even when CDP is down.
500
- if (!manager.isLaunched() && isDoctor && manager instanceof BrowserManager) {
501
- try {
502
- await manager.launch({
503
- id: 'doctor-cdp',
504
- action: 'launch',
505
- cdpPort: 9333,
506
- ignoreHTTPSErrors: process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1',
507
- userAgent: process.env.AGENT_BROWSER_USER_AGENT,
508
- colorScheme: process.env.AGENT_BROWSER_COLOR_SCHEME === 'dark' ||
509
- process.env.AGENT_BROWSER_COLOR_SCHEME === 'light' ||
510
- process.env.AGENT_BROWSER_COLOR_SCHEME === 'no-preference'
511
- ? process.env.AGENT_BROWSER_COLOR_SCHEME
512
- : undefined,
513
- tabGroup: process.env.AGENT_BROWSER_TAB_GROUP?.trim() || undefined,
514
- tabGroupPluginId: process.env.AGENT_BROWSER_TAB_GROUP_PLUGIN_ID?.trim() || undefined,
515
- });
516
- }
517
- catch {
518
- // Keep running: doctor should report failures instead of exiting early.
519
- }
520
- }
521
- // Recover from stale state: browser is launched but all pages were closed
522
- if (manager instanceof BrowserManager &&
523
- manager.isLaunched() &&
524
- !manager.hasPages() &&
525
- parseResult.command.action !== 'launch' &&
526
- parseResult.command.action !== 'close') {
527
- await manager.ensurePage();
528
- }
529
- // Handle explicit launch with auto-load state
530
- if (parseResult.command.action === 'launch' &&
531
- manager instanceof BrowserManager &&
532
- !parseResult.command.autoStateFilePath) {
533
- const autoStatePath = getSessionAutoStatePath();
534
- if (autoStatePath) {
535
- parseResult.command.autoStateFilePath = autoStatePath;
536
- }
537
- }
538
- // Handle close command specially - shuts down daemon
539
- if (parseResult.command.action === 'close') {
540
- // Auto-save state before closing
541
- if (manager instanceof BrowserManager && manager.isLaunched()) {
542
- const savePath = getSessionSaveStatePath();
543
- if (savePath) {
544
- try {
545
- const { encrypted } = await saveStateToFile(manager, savePath);
546
- fs.chmodSync(savePath, 0o600);
547
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
548
- console.error(`Auto-saved session state: ${savePath}${encrypted ? ' (encrypted)' : ''}`);
549
- }
550
- }
551
- catch (err) {
552
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
553
- console.error(`Failed to auto-save session state:`, err);
554
- }
555
- }
556
- }
557
- }
558
- const response = isIOS && manager instanceof IOSManager
559
- ? await executeIOSCommand(parseResult.command, manager)
560
- : await executeCommand(parseResult.command, manager);
561
- await safeWrite(socket, serializeResponse(response) + '\n');
562
- if (!shuttingDown) {
563
- shuttingDown = true;
564
- setTimeout(() => {
565
- server.close();
566
- cleanupSocket();
567
- process.exit(0);
568
- }, 100);
569
- }
570
- commandQueue.length = 0;
571
- processing = false;
572
- return;
573
- }
574
- // Execute command with appropriate handler
575
- const response = isIOS && manager instanceof IOSManager
576
- ? await executeIOSCommand(parseResult.command, manager)
577
- : await executeCommand(parseResult.command, manager);
578
- // Add any launch warnings to the response
579
- if (manager instanceof BrowserManager) {
580
- const warnings = manager.getAndClearWarnings();
581
- if (warnings.length > 0 && response.success && response.data) {
582
- response.data.warnings = warnings;
583
- }
584
- }
585
- await safeWrite(socket, serializeResponse(response) + '\n');
586
- }
587
- catch (err) {
588
- const message = err instanceof Error ? err.message : String(err);
589
- await safeWrite(socket, serializeResponse(errorResponse('error', message)) + '\n').catch(() => { }); // Socket may already be destroyed
590
- }
591
- });
592
- }
593
- finally {
594
- pendingCommands = Math.max(0, pendingCommands - 1);
595
- scheduleIdleShutdown();
596
- }
597
- }
598
- processing = false;
599
- }
600
- socket.on('data', (data) => {
601
- buffer += data.toString();
602
- // Security: Detect and reject HTTP requests to prevent cross-origin attacks.
603
- // Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
604
- // while legitimate clients send raw JSON starting with "{".
605
- if (!httpChecked) {
606
- httpChecked = true;
607
- const trimmed = buffer.trimStart();
608
- if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
609
- socket.destroy();
610
- return;
611
- }
612
- }
613
- // Extract complete lines and enqueue them for serial processing
614
- while (buffer.includes('\n')) {
615
- const newlineIdx = buffer.indexOf('\n');
616
- const line = buffer.substring(0, newlineIdx);
617
- buffer = buffer.substring(newlineIdx + 1);
618
- if (!line.trim())
619
- continue;
620
- commandQueue.push(line);
621
- }
622
- processQueue().catch((err) => {
623
- // Socket write failures during queue processing are non-fatal;
624
- // the client has likely disconnected.
625
- // Only log err.message to avoid leaking sensitive fields (e.g. passwords) from command objects.
626
- console.warn('[warn] processQueue error:', err?.message ?? String(err));
627
- if (process.env.AGENT_BROWSER_DEBUG === '1') {
628
- console.error('[DEBUG] processQueue error stack:', err?.stack ?? err?.message ?? String(err));
629
- }
630
- });
631
- });
632
- socket.on('error', () => {
633
- // Client disconnected, ignore
634
- });
635
- });
636
- const pidFile = getPidFile();
637
- // Write PID file before listening
638
- fs.writeFileSync(pidFile, process.pid.toString());
639
- try {
640
- fs.chmodSync(pidFile, 0o600);
641
- }
642
- catch {
643
- // Best-effort hardening; skip on platforms that don't support POSIX modes.
644
- }
645
- const metaFile = getMetaFile();
646
- // Ownership/version proof consumed by the CLI before reusing default daemon.
647
- const daemonMeta = {
648
- session: currentSession,
649
- pid: process.pid,
650
- startedAt: Date.now(),
651
- daemonPath: process.argv[1] ? path.resolve(process.argv[1]) : '',
652
- cliVersion: process.env.AGENT_BROWSER_CLI_VERSION ?? '',
653
- mode: residentMode ? 'resident' : 'idle',
654
- };
655
- fs.writeFileSync(metaFile, JSON.stringify(daemonMeta, null, 2));
656
- try {
657
- fs.chmodSync(metaFile, 0o600);
658
- }
659
- catch {
660
- // Best-effort hardening; skip on platforms that don't support POSIX modes.
661
- }
662
- if (isWindows) {
663
- // Windows: use TCP socket on localhost
664
- const port = getPortForSession(currentSession);
665
- const portFile = getPortFile();
666
- fs.writeFileSync(portFile, port.toString());
667
- server.listen(port, '127.0.0.1', () => {
668
- // Daemon is ready on TCP port
669
- });
670
- }
671
- else {
672
- // Unix: use Unix domain socket
673
- const socketPath = getSocketPath();
674
- server.listen(socketPath, () => {
675
- // Daemon is ready
676
- });
677
- }
678
- server.on('error', (err) => {
679
- console.error('Server error:', err);
680
- cancelIdleTimer();
681
- cleanupSocket();
682
- process.exit(1);
683
- });
684
- // Handle shutdown signals
685
- const shutdown = async (_reason) => {
686
- if (shuttingDown)
687
- return;
688
- shuttingDown = true;
689
- cancelIdleTimer();
690
- // Stop stream server if running
691
- if (streamServer) {
692
- await streamServer.stop();
693
- streamServer = null;
694
- // Clean up stream port file
695
- const streamPortFile = getStreamPortFile();
696
- try {
697
- if (fs.existsSync(streamPortFile))
698
- fs.unlinkSync(streamPortFile);
699
- }
700
- catch {
701
- // Ignore cleanup errors
702
- }
703
- }
704
- await manager.close();
705
- server.close();
706
- cleanupSocket();
707
- process.exit(0);
708
- };
709
- process.on('SIGINT', shutdown);
710
- process.on('SIGTERM', shutdown);
711
- process.on('SIGHUP', shutdown);
712
- // Handle unexpected errors - always cleanup
713
- process.on('uncaughtException', (err) => {
714
- console.error('Uncaught exception:', err);
715
- cancelIdleTimer();
716
- cleanupSocket();
717
- process.exit(1);
718
- });
719
- process.on('unhandledRejection', (reason) => {
720
- console.error('Unhandled rejection:', reason);
721
- cancelIdleTimer();
722
- cleanupSocket();
723
- process.exit(1);
724
- });
725
- // Cleanup on normal exit
726
- process.on('exit', () => {
727
- cancelIdleTimer();
728
- cleanupSocket();
729
- });
730
- scheduleIdleShutdown();
731
- // Keep process alive
732
- process.stdin.resume();
733
- }
734
- // Run daemon if this is the entry point
735
- if (process.argv[1]?.endsWith('daemon.js') || process.env.AGENT_BROWSER_DAEMON === '1') {
736
- startDaemon({
737
- resident: process.argv.includes('--resident'),
738
- }).catch((err) => {
739
- console.error('Daemon error:', err);
740
- cleanupSocket();
741
- process.exit(1);
742
- });
743
- }
744
- //# sourceMappingURL=daemon.js.map