pi-agent-browser-native 0.2.72 → 0.2.75

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 +33 -0
  2. package/README.md +14 -12
  3. package/dist/extensions/agent-browser/index.js +148 -16
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +122 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +11 -0
  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 +374 -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 +629 -430
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +148 -57
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +30 -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 +9 -8
  32. package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
  33. package/dist/extensions/agent-browser/lib/process.js +271 -34
  34. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
  35. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
  36. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +35 -12
  37. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
  38. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
  39. package/dist/extensions/agent-browser/lib/runtime.js +114 -107
  40. package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
  41. package/dist/extensions/agent-browser/lib/temp.js +13 -25
  42. package/docs/ARCHITECTURE.md +10 -9
  43. package/docs/COMMAND_REFERENCE.md +43 -23
  44. package/docs/ELECTRON.md +10 -10
  45. package/docs/RELEASE.md +3 -2
  46. package/docs/REQUIREMENTS.md +1 -1
  47. package/docs/SUPPORT_MATRIX.md +19 -18
  48. package/docs/TOOL_CONTRACT.md +29 -26
  49. package/docs/platform-smoke.md +2 -2
  50. package/package.json +1 -1
  51. package/platform-smoke.config.mjs +1 -1
  52. package/scripts/agent-browser-capability-baseline.mjs +11 -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, getOwnedManagedSessionCompatibilityEnv, 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,115 @@ 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
+ ...getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions),
388
+ [AGENT_BROWSER_ARGS_ENV]: undefined,
192
389
  };
193
390
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
194
391
  let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
392
+ if (ownedManagedSessionClose)
393
+ effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
195
394
  const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
196
- if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
395
+ if (requestedSocketDir !== undefined) {
396
+ const socketDirIsSecure = requestedSocketDir.length > 0 && await ensureAgentBrowserSocketDir(requestedSocketDir);
397
+ if (signal?.aborted) {
398
+ return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
399
+ }
400
+ if (!socketDirIsSecure) {
401
+ return {
402
+ aborted: false,
403
+ agentBrowserStarted: false,
404
+ exitCode: 1,
405
+ spawnError: new Error("Agent-browser socket storage must be an absolute, non-symlink directory owned by the current user with mode 0700."),
406
+ stderr: "",
407
+ stdout: "",
408
+ timedOut: false,
409
+ };
410
+ }
197
411
  effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
198
412
  }
413
+ if (signal?.aborted) {
414
+ return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
415
+ }
199
416
  return await new Promise((resolve) => {
200
417
  let aborted = false;
418
+ let agentBrowserStarted = false;
201
419
  let settled = false;
202
420
  let spawnError;
203
421
  let stderr = "";
@@ -268,6 +486,15 @@ export async function runAgentBrowserProcess(options) {
268
486
  if (stdoutSpillHandle) {
269
487
  await stdoutSpillHandle.close().catch(() => undefined);
270
488
  }
489
+ const windowsMissingBinary = processPlatform === "win32" && exitCode !== 0 && isWindowsAgentBrowserCommandMissing(stderr);
490
+ if (processPlatform === "win32" && !windowsMissingBinary && !spawnError)
491
+ agentBrowserStarted = true;
492
+ if (windowsMissingBinary && !spawnError) {
493
+ spawnError = Object.assign(new Error("spawn agent-browser ENOENT"), { code: "ENOENT" });
494
+ }
495
+ else if (processPlatform === "win32" && shouldCommitManagedRestoreAfterWindowsProcess({ exitCode, spawnError, stderr })) {
496
+ commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
497
+ }
271
498
  if (!spawnError && stdoutSpillError) {
272
499
  spawnError = stdoutSpillError;
273
500
  }
@@ -275,6 +502,7 @@ export async function runAgentBrowserProcess(options) {
275
502
  destroySpawnedChildStreams(child);
276
503
  resolve({
277
504
  aborted,
505
+ agentBrowserStarted,
278
506
  exitCode,
279
507
  spawnError,
280
508
  stderr,
@@ -285,12 +513,24 @@ export async function runAgentBrowserProcess(options) {
285
513
  });
286
514
  });
287
515
  };
288
- const spawnCommand = buildAgentBrowserSpawnCommand(args);
516
+ const childEnv = buildAgentBrowserProcessEnv(processEnv, effectiveEnv);
517
+ const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
518
+ if (spawnPolicyError) {
519
+ resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
520
+ return;
521
+ }
522
+ const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args));
289
523
  const child = spawn(spawnCommand.command, spawnCommand.args, {
290
524
  cwd,
291
- env: buildAgentBrowserProcessEnv(processEnv, effectiveEnv),
525
+ env: childEnv,
292
526
  stdio: ["pipe", "pipe", "pipe"],
293
527
  });
528
+ if (processPlatform !== "win32") {
529
+ child.once("spawn", () => {
530
+ agentBrowserStarted = true;
531
+ commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
532
+ });
533
+ }
294
534
  const terminateChild = (reason) => {
295
535
  if (settled)
296
536
  return;
@@ -356,13 +596,10 @@ export async function runAgentBrowserProcess(options) {
356
596
  timeoutTimer.unref?.();
357
597
  }
358
598
  if (signal) {
359
- if (signal.aborted) {
599
+ abortListener = () => terminateChild("abort");
600
+ signal.addEventListener("abort", abortListener, { once: true });
601
+ if (signal.aborted)
360
602
  terminateChild("abort");
361
- }
362
- else {
363
- abortListener = () => terminateChild("abort");
364
- signal.addEventListener("abort", abortListener, { once: true });
365
- }
366
603
  }
367
604
  writeChildStdin();
368
605
  });
@@ -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,
@@ -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)}`;
@@ -912,12 +916,25 @@ function formatFrameText(data) {
912
916
  const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
913
917
  return lines.length > 0 ? lines.join("\n") : undefined;
914
918
  }
915
- 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
+ }
916
932
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
917
933
  if (states) {
918
- if (states.length === 0)
919
- return "No saved states.";
920
- return states
934
+ const visibleStates = filterCallerOwnedStateListItems(states);
935
+ if (visibleStates.length === 0)
936
+ return "No caller-owned saved states.";
937
+ return visibleStates
921
938
  .map((item, index) => {
922
939
  if (!isRecord(item))
923
940
  return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
@@ -960,6 +977,12 @@ export function redactPresentationData(commandInfo, data) {
960
977
  return redactStatefulValues(data, new Set(["value"]));
961
978
  if (commandInfo.command === "storage")
962
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"]));
963
986
  return redactStructuredPresentationValue(data);
964
987
  }
965
988
  export function formatDiagnosticText(commandInfo, data) {
@@ -986,7 +1009,7 @@ export function formatDiagnosticText(commandInfo, data) {
986
1009
  if (commandInfo.command === "frame")
987
1010
  return formatFrameText(data);
988
1011
  if (commandInfo.command === "state")
989
- return formatStateText(data);
1012
+ return formatStateText(data, commandInfo.subcommand);
990
1013
  if (commandInfo.command === "network" && commandInfo.subcommand === "requests")
991
1014
  return formatNetworkRequestsText(data, commandInfo);
992
1015
  if (commandInfo.command === "network" && commandInfo.subcommand === "request")
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Purpose: Remove wrapper-owned managed session and state capabilities from caller-visible list results.
3
+ * Responsibilities: Recognize managed list rows recursively and preserve the surrounding upstream result shape.
4
+ * Scope: Presentation filtering only; command authorization lives in managed-session-state-policy.ts.
5
+ */
6
+ import { containsManagedSessionRestoreKey, isWrapperManagedSessionName } from "../../managed-session-capabilities.js";
7
+ import { isRecord } from "../../parsing.js";
8
+ function isWrapperManagedSessionListItem(item) {
9
+ if (typeof item === "string")
10
+ return isWrapperManagedSessionName(item);
11
+ if (!isRecord(item))
12
+ return false;
13
+ return ["name", "session", "id"].some((key) => isWrapperManagedSessionName(typeof item[key] === "string" ? item[key] : undefined));
14
+ }
15
+ function containsManagedStateCapability(value) {
16
+ if (typeof value === "string")
17
+ return containsManagedSessionRestoreKey(value);
18
+ if (Array.isArray(value))
19
+ return value.some(containsManagedStateCapability);
20
+ return isRecord(value) && Object.values(value).some(containsManagedStateCapability);
21
+ }
22
+ export function filterCallerOwnedSessionListItems(items) {
23
+ return items.filter((item) => !isWrapperManagedSessionListItem(item));
24
+ }
25
+ export function filterCallerOwnedStateListItems(items) {
26
+ return items.filter((item) => !containsManagedStateCapability(item));
27
+ }
28
+ export function filterManagedSessionListRows(data) {
29
+ if (!isRecord(data) || !Array.isArray(data.sessions))
30
+ return data;
31
+ return { ...data, sessions: filterCallerOwnedSessionListItems(data.sessions) };
32
+ }
33
+ export function filterManagedStateListRows(data) {
34
+ if (!isRecord(data))
35
+ return data;
36
+ return Object.fromEntries(Object.entries(data).map(([key, value]) => [
37
+ key,
38
+ (key === "states" || key === "files") && Array.isArray(value)
39
+ ? filterCallerOwnedStateListItems(value)
40
+ : value,
41
+ ]));
42
+ }
@@ -8,6 +8,7 @@
8
8
  import { buildNextToolAction, withOptionalSessionArgs } from "./next-actions.js";
9
9
  export const AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS = {
10
10
  aboutBlankListTabs: "list-tabs-for-about-blank-recovery",
11
+ connectedSessionGetUrl: "verify-connected-session-url",
11
12
  connectedSessionListTabs: "list-connected-session-tabs",
12
13
  genericTabDriftListTabs: "list-tabs-for-recovery",
13
14
  noActivePageListTabs: "list-tabs-after-no-active-page",
@@ -60,6 +61,12 @@ export function buildRecoveryNextActions(recovery) {
60
61
  const sessionArgs = (args) => withOptionalSessionArgs(recovery.sessionName, args);
61
62
  if (recovery.kind === "connected-session") {
62
63
  return [
64
+ buildNextToolAction({
65
+ args: sessionArgs(["get", "url"]),
66
+ id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionGetUrl,
67
+ reason: "Verify the attached page URL before any page-content inspection.",
68
+ safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target is a local file page or the URL cannot be verified.",
69
+ }),
63
70
  buildNextToolAction({
64
71
  args: sessionArgs(["tab", "list"]),
65
72
  id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionListTabs,