pi-agent-browser-native 0.2.71 → 0.2.74

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 (52) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -12
  3. package/dist/extensions/agent-browser/index.js +104 -16
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +124 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +12 -1
  6. package/dist/extensions/agent-browser/lib/electron/cdp.js +2 -2
  7. package/dist/extensions/agent-browser/lib/electron/launch.js +48 -12
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +96 -98
  9. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +88 -2
  10. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +22 -0
  11. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +432 -0
  12. package/dist/extensions/agent-browser/lib/managed-session-restore.js +367 -0
  13. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +367 -0
  14. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +589 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +299 -0
  16. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +35 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +9 -2
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +40 -22
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +15 -6
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +54 -33
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +182 -0
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +625 -429
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +136 -56
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +28 -40
  29. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +102 -19
  30. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +13 -1
  31. package/dist/extensions/agent-browser/lib/playbook.js +10 -9
  32. package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
  33. package/dist/extensions/agent-browser/lib/process.js +270 -34
  34. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
  35. package/dist/extensions/agent-browser/lib/results/categories.js +21 -2
  36. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
  37. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +80 -12
  38. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
  39. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
  40. package/dist/extensions/agent-browser/lib/runtime.js +85 -85
  41. package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
  42. package/dist/extensions/agent-browser/lib/temp.js +13 -25
  43. package/docs/ARCHITECTURE.md +9 -8
  44. package/docs/COMMAND_REFERENCE.md +97 -32
  45. package/docs/ELECTRON.md +10 -10
  46. package/docs/RELEASE.md +3 -2
  47. package/docs/SUPPORT_MATRIX.md +22 -19
  48. package/docs/TOOL_CONTRACT.md +31 -28
  49. package/docs/platform-smoke.md +5 -5
  50. package/package.json +1 -1
  51. package/platform-smoke.config.mjs +3 -1
  52. package/scripts/agent-browser-capability-baseline.mjs +45 -3
@@ -1,21 +1,28 @@
1
1
  /**
2
2
  * Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
3
- * Responsibilities: Spawn the agent-browser subprocess, forward parent environment variables plus wrapper overrides, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
3
+ * Responsibilities: Validate POSIX socket storage, spawn the agent-browser subprocess, forward parent environment variables plus wrapper overrides, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
4
4
  * Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
5
5
  * Usage: Called by the extension tool after argument validation and session planning are complete.
6
6
  * Invariants/Assumptions: The binary name is always `agent-browser`; Windows routes through PowerShell to invoke npm launchers with escaped argv; callers handle semantic success/error interpretation.
7
7
  */
8
8
  import { spawn } from "node:child_process";
9
- import { chmod, mkdir } from "node:fs/promises";
9
+ import { lstat, mkdir, readdir } from "node:fs/promises";
10
+ import { dirname, isAbsolute, join } from "node:path";
10
11
  import { env as processEnv, platform as processPlatform } from "node:process";
11
- import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, getFlagName } from "./argv-grammar.js";
12
- import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
12
+ import { parseArgvDescriptor } from "./argv-descriptor.js";
13
+ import { needsManagedSession } from "./command-policy.js";
14
+ import { isKnownCommandToken } from "./command-taxonomy.js";
15
+ import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
16
+ import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
17
+ import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "./managed-session-state-policy.js";
18
+ import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
13
19
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
14
20
  const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
15
21
  const MAX_BUFFERED_STDERR_CHARS = 32_000;
16
22
  const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
17
23
  const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
18
24
  const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
25
+ const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
19
26
  const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
20
27
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
21
28
  const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
@@ -24,6 +31,7 @@ export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
24
31
  const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
25
32
  /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
26
33
  const EXIT_STDIO_GRACE_MS = 100;
34
+ const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
27
35
  function appendTail(text, addition, maxChars) {
28
36
  const combined = text + addition;
29
37
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
@@ -31,37 +39,90 @@ function appendTail(text, addition, maxChars) {
31
39
  function quoteWindowsPowerShellArg(value) {
32
40
  return `'${value.replace(/'/g, "''")}'`;
33
41
  }
34
- const WINDOWS_LEADING_GLOBAL_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
35
42
  /** Exported for unit tests that lock Windows launcher argv ordering. */
36
43
  export function reorderWindowsLeadingGlobalArgs(args) {
37
44
  const leadingGlobals = [];
38
- let index = 0;
39
- while (index < args.length && args[index]?.startsWith("-")) {
45
+ for (let index = 0; index < args.length; index += 1) {
40
46
  const token = args[index];
41
- const flagName = getFlagName(token);
42
- leadingGlobals.push(token);
43
- index += 1;
44
- if (WINDOWS_LEADING_GLOBAL_VALUE_FLAGS.has(flagName) && !token.includes("=") && index < args.length) {
45
- leadingGlobals.push(args[index]);
46
- index += 1;
47
+ if (isKnownCommandToken(token)) {
48
+ return index === 0 ? args : [token, ...leadingGlobals, ...args.slice(index + 1)];
49
+ }
50
+ if (!token.startsWith("-"))
51
+ return args;
52
+ if (token.startsWith("--restore=")) {
53
+ leadingGlobals.push(token);
54
+ continue;
55
+ }
56
+ if (token === "--restore") {
57
+ const value = args[index + 1];
58
+ if (optionalGlobalValueFlagConsumesNext(token, value)) {
59
+ leadingGlobals.push(`--restore=${value}`);
60
+ index += 1;
61
+ }
62
+ else {
63
+ leadingGlobals.push(token);
64
+ }
47
65
  continue;
48
66
  }
49
- if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(flagName) && ["true", "false"].includes(args[index] ?? "")) {
50
- leadingGlobals.push(args[index]);
67
+ if (token.includes("="))
68
+ return args;
69
+ const flag = getFlagName(token);
70
+ if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(flag)) {
71
+ leadingGlobals.push(token);
72
+ if (["true", "false"].includes(args[index + 1] ?? "")) {
73
+ leadingGlobals.push(args[index + 1]);
74
+ index += 1;
75
+ }
76
+ continue;
77
+ }
78
+ if (GLOBAL_VALUE_FLAGS.includes(flag)) {
79
+ const value = args[index + 1];
80
+ if (value === undefined)
81
+ return args;
82
+ leadingGlobals.push(token, value);
51
83
  index += 1;
84
+ continue;
52
85
  }
53
- }
54
- if (leadingGlobals.length === 0 || index >= args.length)
55
86
  return args;
56
- return [args[index], ...leadingGlobals, ...args.slice(index + 1)];
87
+ }
88
+ return args;
89
+ }
90
+ export function pinAgentBrowserFileAccessDisabled(args) {
91
+ const filtered = [];
92
+ for (let index = 0; index < args.length; index += 1) {
93
+ const token = args[index];
94
+ if (token.startsWith("--allow-file-access="))
95
+ continue;
96
+ if (token === "--allow-file-access") {
97
+ if (["false", "true"].includes(args[index + 1] ?? ""))
98
+ index += 1;
99
+ continue;
100
+ }
101
+ filtered.push(token);
102
+ }
103
+ return ["--args", "", "--allow-file-access", "false", ...filtered];
57
104
  }
58
105
  export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
59
106
  if (platform !== "win32") {
60
107
  return { command: "agent-browser", args };
61
108
  }
62
- const commandLine = ["&", "agent-browser.cmd", ...reorderWindowsLeadingGlobalArgs(args).map(quoteWindowsPowerShellArg)].join(" ");
109
+ const invocationArgs = reorderWindowsLeadingGlobalArgs(args).map(quoteWindowsPowerShellArg).join(" ");
110
+ const commandLine = [
111
+ "$agentBrowser = Get-Command agent-browser.cmd -ErrorAction SilentlyContinue;",
112
+ `if (-not $agentBrowser) { [Console]::Error.WriteLine('${WINDOWS_AGENT_BROWSER_MISSING_MARKER}'); exit 127 };`,
113
+ `& $agentBrowser.Source ${invocationArgs}`.trimEnd(),
114
+ ].join(" ");
63
115
  return { command: "powershell.exe", args: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", commandLine] };
64
116
  }
117
+ export function isWindowsAgentBrowserCommandMissing(stderr) {
118
+ const normalized = stderr.toLowerCase();
119
+ return normalized.includes(WINDOWS_AGENT_BROWSER_MISSING_MARKER.toLowerCase()) || (normalized.includes("agent-browser.cmd") && (normalized.includes("commandnotfoundexception") ||
120
+ normalized.includes("not recognized as the name of a cmdlet") ||
121
+ normalized.includes("not recognized as an internal or external command")));
122
+ }
123
+ export function shouldCommitManagedRestoreAfterWindowsProcess(input) {
124
+ return !input.spawnError && !(input.exitCode !== 0 && isWindowsAgentBrowserCommandMissing(input.stderr));
125
+ }
65
126
  function terminateSpawnedChild(child, signal) {
66
127
  if (processPlatform === "win32" && child.pid) {
67
128
  const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
@@ -154,13 +215,76 @@ export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeo
154
215
  if (platform === "win32") {
155
216
  return undefined;
156
217
  }
157
- return `${DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX}${typeof uid === "number" ? `-${uid}` : ""}`;
218
+ const prefix = platform === "darwin" ? "/private/tmp/piab" : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
219
+ return `${prefix}${typeof uid === "number" ? `-${uid}` : ""}`;
158
220
  }
159
- async function ensureAgentBrowserSocketDir(socketDir) {
221
+ async function hasTrustedSocketDirAncestry(socketDir, uid) {
222
+ for (let current = dirname(socketDir);;) {
223
+ const metadata = await lstat(current);
224
+ if (metadata.isSymbolicLink()) {
225
+ if (metadata.uid !== 0)
226
+ return false;
227
+ }
228
+ else if (!metadata.isDirectory()) {
229
+ return false;
230
+ }
231
+ if (!metadata.isSymbolicLink()) {
232
+ const mode = metadata.mode & 0o7777;
233
+ if (metadata.uid === uid) {
234
+ if ((mode & 0o022) !== 0)
235
+ return false;
236
+ }
237
+ else if (metadata.uid !== 0 || ((mode & 0o022) !== 0 && (mode & 0o1000) === 0)) {
238
+ return false;
239
+ }
240
+ }
241
+ const parent = dirname(current);
242
+ if (parent === current)
243
+ return true;
244
+ current = parent;
245
+ }
246
+ }
247
+ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 }) {
248
+ for (const name of await readdir(socketDir)) {
249
+ if ((visited.count += 1) > 16_384)
250
+ return false;
251
+ try {
252
+ const path = join(socketDir, name);
253
+ const metadata = await lstat(path);
254
+ if (metadata.uid !== uid || metadata.isSymbolicLink())
255
+ return false;
256
+ if (metadata.isDirectory()) {
257
+ if (!await socketDirEntriesAreOwned(path, uid, visited))
258
+ return false;
259
+ }
260
+ else if (!metadata.isFile() && !metadata.isSocket()) {
261
+ return false;
262
+ }
263
+ }
264
+ catch (error) {
265
+ if (error.code !== "ENOENT")
266
+ return false;
267
+ }
268
+ }
269
+ return true;
270
+ }
271
+ export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
272
+ if (!isAbsolute(socketDir) || typeof uid !== "number")
273
+ return false;
160
274
  try {
161
- await mkdir(socketDir, { recursive: true, mode: 0o700 });
162
- await chmod(socketDir, 0o700).catch(() => undefined);
163
- return true;
275
+ if (!await hasTrustedSocketDirAncestry(socketDir, uid))
276
+ return false;
277
+ try {
278
+ await mkdir(socketDir, { mode: 0o700 });
279
+ }
280
+ catch (error) {
281
+ if (error.code !== "EEXIST")
282
+ return false;
283
+ }
284
+ const metadata = await lstat(socketDir);
285
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.uid !== uid || (metadata.mode & 0o777) !== 0o700)
286
+ return false;
287
+ return await hasTrustedSocketDirAncestry(socketDir, uid) && await socketDirEntriesAreOwned(socketDir, uid);
164
288
  }
165
289
  catch {
166
290
  return false;
@@ -183,21 +307,114 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
183
307
  clampUpstreamDefaultTimeout(childEnv);
184
308
  return childEnv;
185
309
  }
310
+ function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessionTarget = false, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false, trustedPinnedEmptyConfig = false) {
311
+ const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
312
+ const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
313
+ if (managedSessionTargetError)
314
+ return managedSessionTargetError;
315
+ if (!validateManagedSessionRestoreContextForSpawn(options)) {
316
+ return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
317
+ }
318
+ return getManagedSessionStateAccessValidationError({
319
+ args: options.args,
320
+ currentPageUrl,
321
+ cwd: options.cwd,
322
+ env: effectiveEnv ?? options.env,
323
+ pageUrlUnknown,
324
+ parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
325
+ stdin: options.stdin,
326
+ trustedFirstBatchTabSelection,
327
+ trustedPinnedEmptyConfig,
328
+ });
329
+ }
186
330
  export async function runAgentBrowserProcess(options) {
187
- const { args, cwd, env, signal, stdin } = options;
331
+ const { allowManagedSessionTarget, cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
332
+ const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
333
+ const args = canonicalizeOwnedManagedSessionCloseArgs({
334
+ args: options.args,
335
+ cwd,
336
+ env,
337
+ ownedManagedSession,
338
+ restoreState: managedSessionRestoreState,
339
+ stdin,
340
+ });
188
341
  const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
342
+ if (signal?.aborted) {
343
+ return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
344
+ }
345
+ const managedSessionRestoreOptions = {
346
+ args,
347
+ cwd,
348
+ env,
349
+ ownedManagedSession,
350
+ restoreState: managedSessionRestoreState,
351
+ stdin,
352
+ };
353
+ const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, undefined, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
354
+ if (planningPolicyError) {
355
+ return {
356
+ aborted: false,
357
+ agentBrowserStarted: false,
358
+ exitCode: 1,
359
+ spawnError: new Error(planningPolicyError),
360
+ stderr: "",
361
+ stdout: "",
362
+ timedOut: false,
363
+ };
364
+ }
365
+ const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
366
+ const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
367
+ const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
368
+ const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
369
+ if (managedSessionRestoreConfigEnv === undefined) {
370
+ return {
371
+ aborted: false,
372
+ agentBrowserStarted: false,
373
+ exitCode: 1,
374
+ spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
375
+ stderr: "",
376
+ stdout: "",
377
+ timedOut: false,
378
+ };
379
+ }
189
380
  const processOverrides = {
190
381
  [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
382
+ ...managedSessionRestoreEnv,
191
383
  ...env,
384
+ ...managedSessionRestoreConfigEnv,
385
+ ...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
386
+ ...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
387
+ [AGENT_BROWSER_ARGS_ENV]: undefined,
192
388
  };
193
389
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
194
390
  let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
391
+ if (ownedManagedSessionClose)
392
+ effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
195
393
  const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
196
- if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
394
+ if (requestedSocketDir !== undefined) {
395
+ const socketDirIsSecure = requestedSocketDir.length > 0 && await ensureAgentBrowserSocketDir(requestedSocketDir);
396
+ if (signal?.aborted) {
397
+ return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
398
+ }
399
+ if (!socketDirIsSecure) {
400
+ return {
401
+ aborted: false,
402
+ agentBrowserStarted: false,
403
+ exitCode: 1,
404
+ spawnError: new Error("Agent-browser socket storage must be an absolute, non-symlink directory owned by the current user with mode 0700."),
405
+ stderr: "",
406
+ stdout: "",
407
+ timedOut: false,
408
+ };
409
+ }
197
410
  effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
198
411
  }
412
+ if (signal?.aborted) {
413
+ return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
414
+ }
199
415
  return await new Promise((resolve) => {
200
416
  let aborted = false;
417
+ let agentBrowserStarted = false;
201
418
  let settled = false;
202
419
  let spawnError;
203
420
  let stderr = "";
@@ -268,6 +485,15 @@ export async function runAgentBrowserProcess(options) {
268
485
  if (stdoutSpillHandle) {
269
486
  await stdoutSpillHandle.close().catch(() => undefined);
270
487
  }
488
+ const windowsMissingBinary = processPlatform === "win32" && exitCode !== 0 && isWindowsAgentBrowserCommandMissing(stderr);
489
+ if (processPlatform === "win32" && !windowsMissingBinary && !spawnError)
490
+ agentBrowserStarted = true;
491
+ if (windowsMissingBinary && !spawnError) {
492
+ spawnError = Object.assign(new Error("spawn agent-browser ENOENT"), { code: "ENOENT" });
493
+ }
494
+ else if (processPlatform === "win32" && shouldCommitManagedRestoreAfterWindowsProcess({ exitCode, spawnError, stderr })) {
495
+ commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
496
+ }
271
497
  if (!spawnError && stdoutSpillError) {
272
498
  spawnError = stdoutSpillError;
273
499
  }
@@ -275,6 +501,7 @@ export async function runAgentBrowserProcess(options) {
275
501
  destroySpawnedChildStreams(child);
276
502
  resolve({
277
503
  aborted,
504
+ agentBrowserStarted,
278
505
  exitCode,
279
506
  spawnError,
280
507
  stderr,
@@ -285,12 +512,24 @@ export async function runAgentBrowserProcess(options) {
285
512
  });
286
513
  });
287
514
  };
288
- const spawnCommand = buildAgentBrowserSpawnCommand(args);
515
+ const childEnv = buildAgentBrowserProcessEnv(processEnv, effectiveEnv);
516
+ const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
517
+ if (spawnPolicyError) {
518
+ resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
519
+ return;
520
+ }
521
+ const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args));
289
522
  const child = spawn(spawnCommand.command, spawnCommand.args, {
290
523
  cwd,
291
- env: buildAgentBrowserProcessEnv(processEnv, effectiveEnv),
524
+ env: childEnv,
292
525
  stdio: ["pipe", "pipe", "pipe"],
293
526
  });
527
+ if (processPlatform !== "win32") {
528
+ child.once("spawn", () => {
529
+ agentBrowserStarted = true;
530
+ commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
531
+ });
532
+ }
294
533
  const terminateChild = (reason) => {
295
534
  if (settled)
296
535
  return;
@@ -356,13 +595,10 @@ export async function runAgentBrowserProcess(options) {
356
595
  timeoutTimer.unref?.();
357
596
  }
358
597
  if (signal) {
359
- if (signal.aborted) {
598
+ abortListener = () => terminateChild("abort");
599
+ signal.addEventListener("abort", abortListener, { once: true });
600
+ if (signal.aborted)
360
601
  terminateChild("abort");
361
- }
362
- else {
363
- abortListener = () => terminateChild("abort");
364
- signal.addEventListener("abort", abortListener, { once: true });
365
- }
366
602
  }
367
603
  writeChildStdin();
368
604
  });
@@ -73,16 +73,18 @@ export function formatSessionArtifactRetentionSummary(manifest) {
73
73
  parts.push(`${missingCount} missing`);
74
74
  return `Session artifacts: ${parts.join(", ")} (${manifest.entries.length}/${manifest.maxEntries} recent).`;
75
75
  }
76
+ export function getSessionArtifactManifestEntryKey(entry) {
77
+ return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
78
+ }
76
79
  export function mergeSessionArtifactManifest(options) {
77
80
  const nowMs = options.nowMs ?? Date.now();
78
81
  const maxEntries = getSessionArtifactManifestMaxEntries();
79
- const getEntryKey = (entry) => entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
80
82
  const byPath = new Map();
81
83
  for (const entry of options.base?.entries ?? []) {
82
- byPath.set(getEntryKey(entry), entry);
84
+ byPath.set(getSessionArtifactManifestEntryKey(entry), entry);
83
85
  }
84
86
  for (const entry of options.entries ?? []) {
85
- const key = getEntryKey(entry);
87
+ const key = getSessionArtifactManifestEntryKey(entry);
86
88
  const existing = byPath.get(key);
87
89
  byPath.set(key, {
88
90
  ...existing,
@@ -22,10 +22,29 @@ export function classifyAgentBrowserFailureCategory(options) {
22
22
  const text = [options.errorText, options.validationError, options.parseError, options.spawnError, options.stderr].filter(Boolean).join("\n");
23
23
  const command = options.command ?? "";
24
24
  const usedRef = options.args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
25
- if (options.confirmationRequired || /confirmation required|pending confirmation|requires confirmation/i.test(text))
25
+ // Explicit confirmation flag wins. Text-derived confirmation phrases come after locator-miss detection so a
26
+ // missed control named "Confirmation required" still gets selector recovery.
27
+ if (options.confirmationRequired)
26
28
  return "confirmation-required";
27
- if (options.timedOut || /timeout|timed out|watchdog|IPC read timeout|must stay under its 30s IPC read timeout/i.test(text))
29
+ // Upstream 0.32.4+ locator misses keep detail and may echo getByRole/getByText or Names seen lists.
30
+ // Evaluate before text-derived timeout/confirmation so accessible-name substrings cannot suppress recovery.
31
+ const isUpstreamLocatorMiss = /\bNo element found:\s*(?:getBy[A-Za-z]+|role=|text=|label=|placeholder=|alt=|title=|testid=)/i.test(text) ||
32
+ // No trailing \b after ":" — colon is non-word, so "Element not found: text=…" would not match.
33
+ (/\bElement not found:/i.test(text) && /\bVerify the selector, role, or name\b/i.test(text)) ||
34
+ /\bnone match name\b/i.test(text) ||
35
+ // Scope Names seen to role/name miss context (or find) so unrelated prose cannot trip selector-not-found.
36
+ (/\bNames seen:/i.test(text) && (command === "find" || /\belement has role\b|\bnone match name\b|\bgetByRole\b/i.test(text))) ||
37
+ /\belement has role\b[\s\S]*\bnone match\b/i.test(text);
38
+ if (isUpstreamLocatorMiss)
39
+ return "selector-not-found";
40
+ if (/confirmation required|pending confirmation|requires confirmation/i.test(text))
41
+ return "confirmation-required";
42
+ // Match real timeout phrasing only. Do not treat bare "timeout" as a hit — accessible names can include that word,
43
+ // and `timed?\s*out` would also match the substring "timeout" as time+out.
44
+ if (options.timedOut ||
45
+ /\b(?:timed\s+out|timeout exceeded|watchdog|IPC read timeout)\b|must stay under its 30s IPC read timeout|Operation timed out/i.test(text)) {
28
46
  return "timeout";
47
+ }
29
48
  if (/ENOENT|not found on PATH|could not find.*agent-browser|agent-browser is required but was not found/i.test(text))
30
49
  return "missing-binary";
31
50
  if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text))
@@ -3,6 +3,7 @@
3
3
  * Responsibilities: Normalize scalar fields, stringify model-facing values, and apply sensitive-text redaction.
4
4
  * Scope: Leaf helpers only; command-family formatting lives in sibling modules.
5
5
  */
6
+ import { containsManagedSessionRestoreKey } from "../../managed-session-capabilities.js";
6
7
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
7
8
  import { stringifyUnknown, truncateText } from "../text.js";
8
9
  export function stringifyModelFacing(value) {
@@ -27,7 +28,7 @@ export function redactModelFacingText(text) {
27
28
  return redactSensitiveText(text);
28
29
  }
29
30
  export function redactModelFacingTextIfSensitive(text) {
30
- return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
31
+ return containsManagedSessionRestoreKey(text) || /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
31
32
  ? redactModelFacingText(text)
32
33
  : text;
33
34
  }
@@ -9,6 +9,7 @@ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifa
9
9
  import { withOptionalSessionArgs } from "../next-actions.js";
10
10
  import { stringifyUnknown, truncateText } from "../text.js";
11
11
  import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
12
+ import { filterCallerOwnedSessionListItems, filterCallerOwnedStateListItems, filterManagedSessionListRows, filterManagedStateListRows, } from "./managed-list-filter.js";
12
13
  const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
13
14
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
14
15
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
@@ -114,7 +115,7 @@ export function formatDiagnosticSummary(commandInfo, data) {
114
115
  if (commandInfo.command === "session") {
115
116
  const sessions = getArrayField(data, "sessions");
116
117
  if (sessions)
117
- return `Sessions: ${sessions.length}`;
118
+ return `Sessions: ${filterCallerOwnedSessionListItems(sessions).length}`;
118
119
  const session = getStringField(data, "session");
119
120
  if (session)
120
121
  return `Session: ${session}`;
@@ -172,11 +173,13 @@ export function formatDiagnosticSummary(commandInfo, data) {
172
173
  }
173
174
  if (commandInfo.command === "state") {
174
175
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
175
- if (states)
176
- return `States: ${states.length}`;
176
+ if (states) {
177
+ const visibleStates = commandInfo.subcommand === "list" ? filterCallerOwnedStateListItems(states) : states;
178
+ return `States: ${visibleStates.length}`;
179
+ }
177
180
  if (commandInfo.subcommand === "load")
178
181
  return undefined;
179
- const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
182
+ const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "filename") ?? getStringField(data, "path") ?? commandInfo.subcommand;
180
183
  if (stateName)
181
184
  return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
182
185
  }
@@ -261,9 +264,10 @@ export function formatDiagnosticSummary(commandInfo, data) {
261
264
  function formatSessionText(data) {
262
265
  const sessions = getArrayField(data, "sessions");
263
266
  if (sessions) {
264
- if (sessions.length === 0)
265
- return "No active sessions.";
266
- return sessions
267
+ const visibleSessions = filterCallerOwnedSessionListItems(sessions);
268
+ if (visibleSessions.length === 0)
269
+ return sessions.length === 0 ? "No active sessions." : "No caller-owned active sessions.";
270
+ return visibleSessions
267
271
  .map((item, index) => {
268
272
  if (!isRecord(item))
269
273
  return `${index + 1}. ${stringifyModelFacing(item)}`;
@@ -617,6 +621,49 @@ function formatConsoleText(data, commandInfo) {
617
621
  }
618
622
  return shown.join("\n");
619
623
  }
624
+ function formatA11yText(data) {
625
+ const counts = isRecord(data.counts) ? data.counts : undefined;
626
+ const violations = getArrayField(data, "violations") ?? [];
627
+ const incomplete = getArrayField(data, "incomplete") ?? [];
628
+ if (!counts && violations.length === 0 && incomplete.length === 0)
629
+ return undefined;
630
+ const lines = [];
631
+ const axeVersion = getStringField(data, "axeVersion");
632
+ if (axeVersion)
633
+ lines.push(`axe-core ${redactModelFacingText(axeVersion)}`);
634
+ const url = getStringField(data, "url");
635
+ if (url)
636
+ lines.push(`URL: ${redactModelFacingText(url)}`);
637
+ const violationCount = typeof counts?.violations === "number" ? counts.violations : violations.length;
638
+ const incompleteCount = typeof counts?.incomplete === "number" ? counts.incomplete : incomplete.length;
639
+ const passCount = typeof counts?.passes === "number" ? counts.passes : undefined;
640
+ const inapplicableCount = typeof counts?.inapplicable === "number" ? counts.inapplicable : undefined;
641
+ const countParts = [`${violationCount} violation${violationCount === 1 ? "" : "s"}`, `${incompleteCount} incomplete`];
642
+ if (passCount !== undefined)
643
+ countParts.push(`${passCount} passes`);
644
+ if (inapplicableCount !== undefined)
645
+ countParts.push(`${inapplicableCount} inapplicable`);
646
+ lines.push(`A11y audit: ${countParts.join(", ")}.`);
647
+ const previewLimit = Math.min(10, DIAGNOSTIC_LOG_PREVIEW_LIMIT);
648
+ const preview = violations.slice(0, previewLimit).map((item, index) => {
649
+ if (!isRecord(item))
650
+ return `${index + 1}. ${stringifyModelFacing(item)}`;
651
+ const id = redactModelFacingText(getStringField(item, "id") ?? "rule");
652
+ const impact = redactModelFacingText(getStringField(item, "impact") ?? "unknown");
653
+ const help = firstLine(redactModelFacingText(getStringField(item, "help") ?? "").replace(/\s+/g, " ").trim(), 160);
654
+ const nodeCount = typeof item.nodeCount === "number" ? item.nodeCount : getArrayField(item, "nodes")?.length;
655
+ const nodePart = typeof nodeCount === "number" ? `, ${nodeCount} node${nodeCount === 1 ? "" : "s"}` : "";
656
+ return `${index + 1}. [${impact}] ${id}${nodePart}${help ? ` — ${help}` : ""}`;
657
+ });
658
+ lines.push(...preview);
659
+ if (violations.length > preview.length) {
660
+ lines.push(`... (${violations.length - preview.length} additional violations omitted from preview)`);
661
+ }
662
+ if (incompleteCount > 0) {
663
+ lines.push(`${incompleteCount} incomplete check${incompleteCount === 1 ? "" : "s"} need manual review (see details.data.incomplete).`);
664
+ }
665
+ return lines.join("\n");
666
+ }
620
667
  function formatErrorsText(data, commandInfo) {
621
668
  const errors = getArrayField(data, "errors");
622
669
  if (!errors)
@@ -869,12 +916,25 @@ function formatFrameText(data) {
869
916
  const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
870
917
  return lines.length > 0 ? lines.join("\n") : undefined;
871
918
  }
872
- function formatStateText(data) {
919
+ function formatStateText(data, subcommand) {
920
+ if (subcommand === "show") {
921
+ const filename = getStringField(data, "filename") ?? getStringField(data, "name") ?? "saved state";
922
+ const summary = getStringField(data, "summary");
923
+ const lines = [`Saved state: ${redactModelFacingText(filename)}`];
924
+ if (summary)
925
+ lines.push(`Summary: ${redactModelFacingText(summary)}`);
926
+ if (typeof data.encrypted === "boolean")
927
+ lines.push(`Encrypted: ${data.encrypted ? "yes" : "no"}`);
928
+ if (typeof data.size === "number")
929
+ lines.push(`Size: ${data.size} bytes`);
930
+ return lines.join("\n");
931
+ }
873
932
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
874
933
  if (states) {
875
- if (states.length === 0)
876
- return "No saved states.";
877
- return states
934
+ const visibleStates = filterCallerOwnedStateListItems(states);
935
+ if (visibleStates.length === 0)
936
+ return "No caller-owned saved states.";
937
+ return visibleStates
878
938
  .map((item, index) => {
879
939
  if (!isRecord(item))
880
940
  return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
@@ -917,6 +977,12 @@ export function redactPresentationData(commandInfo, data) {
917
977
  return redactStatefulValues(data, new Set(["value"]));
918
978
  if (commandInfo.command === "storage")
919
979
  return redactStorageData(data);
980
+ if (commandInfo.command === "session" && commandInfo.subcommand === "list")
981
+ return redactStructuredPresentationValue(filterManagedSessionListRows(data));
982
+ if (commandInfo.command === "state" && commandInfo.subcommand === "list")
983
+ return redactStructuredPresentationValue(filterManagedStateListRows(data));
984
+ if (commandInfo.command === "state" && commandInfo.subcommand === "show")
985
+ return redactStatefulValues(data, new Set(["value"]));
920
986
  return redactStructuredPresentationValue(data);
921
987
  }
922
988
  export function formatDiagnosticText(commandInfo, data) {
@@ -943,7 +1009,7 @@ export function formatDiagnosticText(commandInfo, data) {
943
1009
  if (commandInfo.command === "frame")
944
1010
  return formatFrameText(data);
945
1011
  if (commandInfo.command === "state")
946
- return formatStateText(data);
1012
+ return formatStateText(data, commandInfo.subcommand);
947
1013
  if (commandInfo.command === "network" && commandInfo.subcommand === "requests")
948
1014
  return formatNetworkRequestsText(data, commandInfo);
949
1015
  if (commandInfo.command === "network" && commandInfo.subcommand === "request")
@@ -966,6 +1032,8 @@ export function formatDiagnosticText(commandInfo, data) {
966
1032
  return formatConsoleText(data, commandInfo);
967
1033
  if (commandInfo.command === "errors")
968
1034
  return formatErrorsText(data, commandInfo);
1035
+ if (commandInfo.command === "a11y")
1036
+ return formatA11yText(data);
969
1037
  if (commandInfo.command === "dashboard")
970
1038
  return formatDashboardText(data);
971
1039
  if (commandInfo.command === "doctor")