pi-agent-browser-native 0.3.0 → 0.6.5

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 (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -4,32 +4,43 @@ import { lstat, mkdir, readdir } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join } from "node:path";
5
5
  import { env as processEnv, platform as processPlatform } from "node:process";
6
6
  import { parseArgvDescriptor } from "./argv-descriptor.js";
7
- import { needsManagedSession } from "./command-policy.js";
8
7
  import { isKnownCommandToken } from "./command-taxonomy.js";
9
- import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
10
- import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
11
- import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "./managed-session-state-policy.js";
12
- import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
8
+ import { extractExplicitSessionName, getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, resolveAgentBrowserNamespace, } from "./argv-grammar.js";
9
+ import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
10
+ import { getPageTargetValidationError, } from "./page-target-validation.js";
11
+ import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
12
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
13
13
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
14
14
  const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
15
15
  const MAX_BUFFERED_STDERR_CHARS = 32_000;
16
16
  const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
17
17
  const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
18
18
  const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
19
- const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
20
19
  const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
21
20
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
22
21
  const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
22
+ const PI_AGENT_BROWSER_SOCKET_DIR_ENV = "PI_AGENT_BROWSER_SOCKET_DIR";
23
23
  const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
24
+ const TERMUX_PACKAGE_NAME_PATTERN = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
24
25
  export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
25
26
  const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
26
27
  /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
27
28
  const EXIT_STDIO_GRACE_MS = 100;
28
29
  const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
29
30
  const attachedBrowserSessionContext = new AsyncLocalStorage();
31
+ const WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND = new Set([
32
+ "auth", "clipboard", "cookies", "dashboard", "device", "dialog", "diff", "find", "get", "is", "keyboard",
33
+ "mouse", "network", "plugin", "profiler", "react", "record", "session", "set", "skills", "state", "storage",
34
+ "stream", "tab", "trace", "webmcp", "window",
35
+ ]);
30
36
  export function withAttachedBrowserSessionContext(preserve, run) {
31
37
  return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
32
38
  }
39
+ export function getWindowsExplicitDefaultNamespaceEnv(args, parentNamespace, platform = processPlatform) {
40
+ return platform === "win32" && resolveAgentBrowserNamespace(args, parentNamespace) === ""
41
+ ? { AGENT_BROWSER_NAMESPACE: "" }
42
+ : {};
43
+ }
33
44
  function appendTail(text, addition, maxChars) {
34
45
  const combined = text + addition;
35
46
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
@@ -43,7 +54,12 @@ export function reorderWindowsLeadingGlobalArgs(args) {
43
54
  for (let index = 0; index < args.length; index += 1) {
44
55
  const token = args[index];
45
56
  if (isKnownCommandToken(token)) {
46
- return index === 0 ? args : [token, ...leadingGlobals, ...args.slice(index + 1)];
57
+ if (index === 0)
58
+ return args;
59
+ const firstPositional = args[index + 1];
60
+ return WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND.has(token) && firstPositional && !firstPositional.startsWith("-")
61
+ ? [token, firstPositional, ...leadingGlobals, ...args.slice(index + 2)]
62
+ : [token, ...leadingGlobals, ...args.slice(index + 1)];
47
63
  }
48
64
  if (!token.startsWith("-"))
49
65
  return args;
@@ -77,6 +93,13 @@ export function reorderWindowsLeadingGlobalArgs(args) {
77
93
  const value = args[index + 1];
78
94
  if (value === undefined)
79
95
  return args;
96
+ // PowerShell -> .cmd drops empty argv values. Planning rejects empty
97
+ // caller --args; keep this defensive skip so an unexpected empty value
98
+ // cannot turn the next flag into its accidental value on native Windows.
99
+ if (value === "" && (flag === "--args" || flag === "--namespace")) {
100
+ index += 1;
101
+ continue;
102
+ }
80
103
  leadingGlobals.push(token, value);
81
104
  index += 1;
82
105
  continue;
@@ -85,27 +108,10 @@ export function reorderWindowsLeadingGlobalArgs(args) {
85
108
  }
86
109
  return args;
87
110
  }
88
- export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
89
- const filtered = [];
90
- for (let index = 0; index < args.length; index += 1) {
91
- const token = args[index];
92
- if (token.startsWith("--allow-file-access="))
93
- continue;
94
- if (token === "--allow-file-access") {
95
- if (["false", "true"].includes(args[index + 1] ?? ""))
96
- index += 1;
97
- continue;
98
- }
99
- filtered.push(token);
100
- }
101
- // These are launch-only controls. Sending them on an attached-session follow-up makes upstream replace the CDP connection with a local browser.
102
- if (preserveAttachedBrowserSession)
103
- return filtered;
104
- // Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
105
- const browserArgs = wrapperCompatibilityUserAgent
106
- ? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
107
- : "";
108
- return ["--args", browserArgs, "--allow-file-access", "false", ...filtered];
111
+ export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
112
+ if (preserveAttachedBrowserSession || !wrapperCompatibilityUserAgent)
113
+ return args;
114
+ return ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`, ...args];
109
115
  }
110
116
  export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
111
117
  if (platform !== "win32") {
@@ -216,33 +222,43 @@ function clampUpstreamDefaultTimeout(childEnv) {
216
222
  export function getAgentBrowserProcessTimeoutMs(env = processEnv) {
217
223
  return parsePositiveIntegerEnv(env[PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV]) ?? DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS;
218
224
  }
219
- export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
225
+ export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined, termuxPackageName = processEnv.TERMUX_APP__PACKAGE_NAME) {
220
226
  if (platform === "win32") {
221
227
  return undefined;
222
228
  }
223
- const prefix = platform === "darwin" ? "/private/tmp/piab" : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
224
- return `${prefix}${typeof uid === "number" ? `-${uid}` : ""}`;
229
+ const termuxAppRoot = platform === "android" && termuxPackageName && TERMUX_PACKAGE_NAME_PATTERN.test(termuxPackageName);
230
+ const prefix = platform === "darwin"
231
+ ? "/private/tmp/piab"
232
+ : termuxAppRoot
233
+ ? `/data/data/${termuxPackageName}/piab`
234
+ : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
235
+ return `${prefix}${!termuxAppRoot && typeof uid === "number" ? `-${uid}` : ""}`;
236
+ }
237
+ export function isTrustedAndroidAppDataRoot(path, metadata, uid, platform = processPlatform) {
238
+ if (platform !== "android" || metadata.uid !== uid || metadata.isSymbolicLink() || !metadata.isDirectory() || (metadata.mode & 0o777) !== 0o700)
239
+ return false;
240
+ const parent = dirname(path);
241
+ return parent === "/data/data" || /^\/data\/user\/\d+$/.test(parent);
242
+ }
243
+ export function isTrustedSocketDirAncestor(metadata, uid, platform = processPlatform) {
244
+ if (metadata.isSymbolicLink())
245
+ return metadata.uid === 0;
246
+ if (!metadata.isDirectory())
247
+ return false;
248
+ const mode = metadata.mode & 0o7777;
249
+ if (platform === "android" && uid !== 0 && metadata.uid === uid && metadata.gid === uid)
250
+ return (mode & 0o002) === 0;
251
+ if (metadata.uid === uid && uid !== 0)
252
+ return (mode & 0o022) === 0;
253
+ return metadata.uid === 0 && ((mode & 0o022) === 0 || (mode & 0o1000) !== 0);
225
254
  }
226
255
  async function hasTrustedSocketDirAncestry(socketDir, uid) {
227
256
  for (let current = dirname(socketDir);;) {
228
257
  const metadata = await lstat(current);
229
- if (metadata.isSymbolicLink()) {
230
- if (metadata.uid !== 0)
231
- return false;
232
- }
233
- else if (!metadata.isDirectory()) {
258
+ if (isTrustedAndroidAppDataRoot(current, metadata, uid))
259
+ return true;
260
+ if (!isTrustedSocketDirAncestor(metadata, uid))
234
261
  return false;
235
- }
236
- if (!metadata.isSymbolicLink()) {
237
- const mode = metadata.mode & 0o7777;
238
- if (metadata.uid === uid) {
239
- if ((mode & 0o022) !== 0)
240
- return false;
241
- }
242
- else if (metadata.uid !== 0 || ((mode & 0o022) !== 0 && (mode & 0o1000) === 0)) {
243
- return false;
244
- }
245
- }
246
262
  const parent = dirname(current);
247
263
  if (parent === current)
248
264
  return true;
@@ -273,28 +289,64 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
273
289
  }
274
290
  return true;
275
291
  }
276
- export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
277
- if (!isAbsolute(socketDir) || typeof uid !== "number")
278
- return false;
292
+ export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
293
+ if (!isAbsolute(socketDir))
294
+ return "the path is not absolute";
295
+ if (typeof uid !== "number")
296
+ return "POSIX ownership metadata is unavailable";
279
297
  try {
280
298
  if (!await hasTrustedSocketDirAncestry(socketDir, uid))
281
- return false;
299
+ return "an ancestor is writable, foreign-owned, a non-directory, or an untrusted symlink";
282
300
  try {
283
301
  await mkdir(socketDir, { mode: 0o700 });
284
302
  }
285
303
  catch (error) {
286
304
  if (error.code !== "EEXIST")
287
- return false;
305
+ return `the directory could not be created (${error.code ?? "unknown error"})`;
288
306
  }
289
307
  const metadata = await lstat(socketDir);
290
- if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.uid !== uid || (metadata.mode & 0o777) !== 0o700)
291
- return false;
292
- return await hasTrustedSocketDirAncestry(socketDir, uid) && await socketDirEntriesAreOwned(socketDir, uid);
308
+ if (!metadata.isDirectory())
309
+ return "the path is not a directory";
310
+ if (metadata.isSymbolicLink())
311
+ return "the directory is a symlink";
312
+ if (metadata.uid !== uid)
313
+ return `the directory is owned by uid ${metadata.uid}, not uid ${uid}`;
314
+ if ((metadata.mode & 0o777) !== 0o700)
315
+ return `the directory mode is ${(metadata.mode & 0o777).toString(8)}, not 700`;
316
+ if (!await hasTrustedSocketDirAncestry(socketDir, uid))
317
+ return "an ancestor became untrusted during validation";
318
+ if (!await socketDirEntriesAreOwned(socketDir, uid))
319
+ return "the directory contains a foreign-owned, symlink, special, or excessively deep entry";
320
+ return undefined;
293
321
  }
294
- catch {
295
- return false;
322
+ catch (error) {
323
+ return `the directory could not be inspected (${error.code ?? "unknown error"})`;
296
324
  }
297
325
  }
326
+ export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
327
+ return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
328
+ }
329
+ export function getAgentBrowserSocketPathValidationError(options) {
330
+ if ((options.platform ?? processPlatform) === "win32")
331
+ return undefined;
332
+ const descriptor = parseArgvDescriptor(options.args);
333
+ const { command } = descriptor.commandInfo;
334
+ // Preflight commands that can start or navigate a browser. Follow-up reads and
335
+ // cleanup may target a daemon created by an earlier wrapper version, so let
336
+ // upstream inspect those identities instead of rejecting them from path math.
337
+ if (!command || !["batch", "connect", "goto", "navigate", "open", "visit"].includes(command))
338
+ return undefined;
339
+ const sessionName = extractExplicitSessionName(options.args);
340
+ if (!sessionName)
341
+ return undefined;
342
+ const namespace = resolveAgentBrowserNamespace(options.args, options.env?.AGENT_BROWSER_NAMESPACE);
343
+ const socketRoot = namespace ? join(options.socketDir, "namespaces", namespace, "run") : options.socketDir;
344
+ const socketPath = join(socketRoot, `${sessionName}.sock`);
345
+ const pathBytes = Buffer.byteLength(socketPath);
346
+ if (pathBytes <= 103)
347
+ return undefined;
348
+ return `Agent-browser Unix socket path would be ${pathBytes} bytes (max 103) for session ${JSON.stringify(sessionName)} under ${JSON.stringify(options.socketDir)}. Set PI_AGENT_BROWSER_SOCKET_DIR to a shorter absolute private directory such as /tmp/piab-<uid> with mode 0700; retrying sessionMode \"fresh\" cannot shorten this configured root.`;
349
+ }
298
350
  export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
299
351
  const childEnv = {};
300
352
  for (const [name, value] of Object.entries(baseEnv)) {
@@ -312,51 +364,38 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
312
364
  clampUpstreamDefaultTimeout(childEnv);
313
365
  return childEnv;
314
366
  }
315
- function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessionTarget = false, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false, trustedPinnedEmptyConfig = false) {
316
- const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
317
- const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
318
- if (managedSessionTargetError)
319
- return managedSessionTargetError;
367
+ function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false) {
320
368
  if (!validateManagedSessionRestoreContextForSpawn(options)) {
321
369
  return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
322
370
  }
323
- return getManagedSessionStateAccessValidationError({
371
+ return getPageTargetValidationError({
324
372
  args: options.args,
325
373
  currentPageUrl,
326
- cwd: options.cwd,
327
- env: effectiveEnv ?? options.env,
328
374
  pageUrlUnknown,
329
- parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
330
375
  stdin: options.stdin,
331
376
  trustedFirstBatchTabSelection,
332
- trustedPinnedEmptyConfig,
333
377
  });
334
378
  }
335
379
  export async function runAgentBrowserProcess(options) {
336
- const { allowManagedSessionTarget, cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
380
+ const { cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
337
381
  const preserveAttachedBrowserSession = options.preserveAttachedBrowserSession === true || attachedBrowserSessionContext.getStore() === true;
338
382
  const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
339
- const args = canonicalizeOwnedManagedSessionCloseArgs({
340
- args: options.args,
341
- cwd,
342
- env,
343
- ownedManagedSession,
344
- restoreState: managedSessionRestoreState,
345
- stdin,
346
- });
383
+ const args = options.args;
347
384
  const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
348
385
  if (signal?.aborted) {
349
386
  return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
350
387
  }
388
+ const parentEnv = getAgentBrowserProcessEnvironment();
351
389
  const managedSessionRestoreOptions = {
352
390
  args,
353
391
  cwd,
354
392
  env,
355
393
  ownedManagedSession,
394
+ parentEnv,
356
395
  restoreState: managedSessionRestoreState,
357
396
  stdin,
358
397
  };
359
- const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, undefined, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
398
+ const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
360
399
  if (planningPolicyError) {
361
400
  return {
362
401
  aborted: false,
@@ -369,48 +408,33 @@ export async function runAgentBrowserProcess(options) {
369
408
  };
370
409
  }
371
410
  const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
372
- const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
373
- const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
374
- const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
375
- if (managedSessionRestoreConfigEnv === undefined) {
376
- return {
377
- aborted: false,
378
- agentBrowserStarted: false,
379
- exitCode: 1,
380
- spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
381
- stderr: "",
382
- stdout: "",
383
- timedOut: false,
384
- };
385
- }
386
411
  const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
387
412
  const processOverrides = {
388
413
  [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
389
414
  ...managedSessionRestoreEnv,
390
415
  ...env,
391
- ...managedSessionRestoreConfigEnv,
392
416
  ...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
393
417
  ...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
418
+ ...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
394
419
  ...ownedManagedSessionCompatibilityEnv,
395
- AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
396
- [AGENT_BROWSER_ARGS_ENV]: undefined,
397
420
  };
398
421
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
399
422
  let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
400
- if (ownedManagedSessionClose)
401
- effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
402
- const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
423
+ const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
403
424
  if (requestedSocketDir !== undefined) {
404
- const socketDirIsSecure = requestedSocketDir.length > 0 && await ensureAgentBrowserSocketDir(requestedSocketDir);
425
+ const socketDirError = requestedSocketDir.length > 0
426
+ ? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
427
+ : "the configured path is empty";
405
428
  if (signal?.aborted) {
406
429
  return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
407
430
  }
408
- if (!socketDirIsSecure) {
431
+ const socketPathError = socketDirError ? undefined : getAgentBrowserSocketPathValidationError({ args, env: effectiveEnv, socketDir: requestedSocketDir });
432
+ if (socketDirError || socketPathError) {
409
433
  return {
410
434
  aborted: false,
411
435
  agentBrowserStarted: false,
412
436
  exitCode: 1,
413
- spawnError: new Error("Agent-browser socket storage must be an absolute, non-symlink directory owned by the current user with mode 0700."),
437
+ spawnError: new Error(socketPathError ?? `Agent-browser socket storage ${JSON.stringify(requestedSocketDir)} is unusable: ${socketDirError}. Use an absolute directory owned by the current uid with mode 0700 and remove foreign, symlink, or special entries.`),
414
438
  stderr: "",
415
439
  stdout: "",
416
440
  timedOut: false,
@@ -432,6 +456,7 @@ export async function runAgentBrowserProcess(options) {
432
456
  let stdoutTail = "";
433
457
  let stdoutSpillHandle;
434
458
  let stdoutSpillPath;
459
+ let stdoutSpillPending = false;
435
460
  let pendingStdoutWrite = Promise.resolve();
436
461
  let stdoutSpillError;
437
462
  let killTimer;
@@ -443,11 +468,12 @@ export async function runAgentBrowserProcess(options) {
443
468
  stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
444
469
  if (stdoutSpillError)
445
470
  return;
446
- if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
471
+ if (!stdoutSpillPending && !stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
447
472
  stdoutBuffers.push(buffer);
448
473
  stdoutBufferedBytes += buffer.length;
449
474
  return;
450
475
  }
476
+ stdoutSpillPending = true;
451
477
  pendingStdoutWrite = pendingStdoutWrite
452
478
  .then(async () => {
453
479
  if (stdoutSpillError)
@@ -521,13 +547,13 @@ export async function runAgentBrowserProcess(options) {
521
547
  });
522
548
  });
523
549
  };
524
- const childEnv = buildAgentBrowserProcessEnv(processEnv, effectiveEnv);
525
- const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
550
+ const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
551
+ const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
526
552
  if (spawnPolicyError) {
527
553
  resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
528
554
  return;
529
555
  }
530
- const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
556
+ const spawnCommand = buildAgentBrowserSpawnCommand(prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
531
557
  const child = spawn(spawnCommand.command, spawnCommand.args, {
532
558
  cwd,
533
559
  env: childEnv,
@@ -0,0 +1,116 @@
1
+ import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
2
+ import { isRecord } from "./parsing.js";
3
+ import { isPendingRecordingArtifact } from "./results/artifact-manifest.js";
4
+ export const RECORDING_RESERVATION_ENTRY_TYPE = "agent-browser-recording-reservation";
5
+ function getReservationKey(reservation) {
6
+ return getAgentBrowserSessionIdentityKey(reservation.sessionName, reservation.namespace);
7
+ }
8
+ function getArtifactReservation(artifact) {
9
+ if (!artifact.session || artifact.command !== "record" || artifact.kind !== "video")
10
+ return undefined;
11
+ return {
12
+ absolutePath: artifact.absolutePath,
13
+ cwd: artifact.cwd ?? process.cwd(),
14
+ namespace: artifact.namespace,
15
+ path: artifact.path,
16
+ sessionName: artifact.session,
17
+ };
18
+ }
19
+ export function applyRecordingArtifactsToReservations(reservations, artifacts) {
20
+ const pendingBySession = new Map();
21
+ const terminalBySession = new Map();
22
+ for (const artifact of artifacts) {
23
+ const reservation = getArtifactReservation(artifact);
24
+ if (!reservation)
25
+ continue;
26
+ const key = getReservationKey(reservation);
27
+ if (isPendingRecordingArtifact(artifact))
28
+ pendingBySession.set(key, reservation);
29
+ else
30
+ terminalBySession.set(key, reservation);
31
+ }
32
+ const transitions = [];
33
+ for (const key of terminalBySession.keys()) {
34
+ if (pendingBySession.has(key))
35
+ continue;
36
+ const existing = reservations.get(key);
37
+ if (!existing)
38
+ continue;
39
+ reservations.delete(key);
40
+ transitions.push({ reservation: existing, state: "closed" });
41
+ }
42
+ for (const [key, pending] of pendingBySession) {
43
+ const existing = reservations.get(key);
44
+ reservations.set(key, pending);
45
+ if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd) {
46
+ transitions.push({ reservation: pending, state: "active" });
47
+ }
48
+ }
49
+ return transitions;
50
+ }
51
+ export function retireRecordingReservation(reservations, sessionName, namespace) {
52
+ const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
53
+ const reservation = reservations.get(key);
54
+ if (reservation)
55
+ reservations.delete(key);
56
+ return reservation;
57
+ }
58
+ export function appendRecordingReservationTransition(pi, transition) {
59
+ const { reservation, state } = transition;
60
+ pi.appendEntry(RECORDING_RESERVATION_ENTRY_TYPE, {
61
+ absolutePath: state === "active" ? reservation.absolutePath : undefined,
62
+ cwd: state === "active" ? reservation.cwd : undefined,
63
+ namespace: reservation.namespace,
64
+ path: state === "active" ? reservation.path : undefined,
65
+ sessionName: reservation.sessionName,
66
+ state,
67
+ version: 1,
68
+ });
69
+ }
70
+ function parseReservationTransition(data) {
71
+ if (!isRecord(data) || data.version !== 1 || (data.state !== "active" && data.state !== "closed"))
72
+ return undefined;
73
+ if (typeof data.sessionName !== "string" || data.sessionName.length === 0)
74
+ return undefined;
75
+ if (data.namespace !== undefined && typeof data.namespace !== "string")
76
+ return undefined;
77
+ if (data.state === "closed") {
78
+ return {
79
+ reservation: { absolutePath: "", cwd: "", namespace: data.namespace, path: "", sessionName: data.sessionName },
80
+ state: "closed",
81
+ };
82
+ }
83
+ if (typeof data.absolutePath !== "string" || typeof data.cwd !== "string" || typeof data.path !== "string")
84
+ return undefined;
85
+ return {
86
+ reservation: {
87
+ absolutePath: data.absolutePath,
88
+ cwd: data.cwd,
89
+ namespace: data.namespace,
90
+ path: data.path,
91
+ sessionName: data.sessionName,
92
+ },
93
+ state: "active",
94
+ };
95
+ }
96
+ export function restoreRecordingReservationStateFromBranch(branch) {
97
+ const reservations = new Map();
98
+ const terminal = new Map();
99
+ for (const entry of branch) {
100
+ if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== RECORDING_RESERVATION_ENTRY_TYPE)
101
+ continue;
102
+ const transition = parseReservationTransition(entry.data);
103
+ if (!transition)
104
+ continue;
105
+ const key = getReservationKey(transition.reservation);
106
+ if (transition.state === "active") {
107
+ reservations.set(key, transition.reservation);
108
+ terminal.delete(key);
109
+ }
110
+ else {
111
+ reservations.delete(key);
112
+ terminal.set(key, transition.reservation);
113
+ }
114
+ }
115
+ return { active: reservations, terminal };
116
+ }
@@ -1,6 +1,6 @@
1
1
  import { isOpenNavigationCommand, isPageMutationCommand } from "../command-taxonomy.js";
2
2
  import { isPendingRecordingArtifact } from "./artifact-manifest.js";
3
- import { buildNextToolAction } from "./next-actions.js";
3
+ import { applySessionToNextActions, buildNextToolAction } from "./next-actions.js";
4
4
  import { AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS, buildRecoveryNextActions, } from "./recovery-actions.js";
5
5
  function buildArtifactAction(path) {
6
6
  return {
@@ -106,7 +106,7 @@ export function buildAgentBrowserNextActions(options) {
106
106
  reason: "Inspect the opened page before choosing interactive refs.",
107
107
  }));
108
108
  }
109
- else if (isPageMutationCommand(options.command)) {
109
+ else if (isPageMutationCommand(options.command, options.subcommand)) {
110
110
  actions.push(buildNextToolAction({
111
111
  args: ["snapshot", "-i"],
112
112
  id: "inspect-after-mutation",
@@ -120,9 +120,8 @@ export function buildAgentBrowserNextActions(options) {
120
120
  actions.push(buildArtifactAction(options.savedFilePath));
121
121
  }
122
122
  for (const artifact of artifacts) {
123
- if (isPendingRecordingArtifact(artifact)) {
123
+ if (isPendingRecordingArtifact(artifact))
124
124
  continue;
125
- }
126
125
  if (artifact.exists === false) {
127
126
  if (artifact.kind === "download") {
128
127
  actions.push(buildNextToolAction({
@@ -146,7 +145,7 @@ export function buildAgentBrowserNextActions(options) {
146
145
  switch (options.failureCategory) {
147
146
  case "artifact-missing":
148
147
  for (const artifact of options.artifacts ?? []) {
149
- if (isPendingRecordingArtifact(artifact) || artifact.exists !== false)
148
+ if (isPendingRecordingArtifact(artifact) || (artifact.exists !== false && artifact.status !== "stale"))
150
149
  continue;
151
150
  if (artifact.kind === "download") {
152
151
  actions.push(buildNextToolAction({
@@ -196,6 +195,56 @@ export function buildAgentBrowserNextActions(options) {
196
195
  }));
197
196
  }
198
197
  break;
198
+ case "timeout":
199
+ {
200
+ const textAssertion = options.command === "wait" && options.args?.includes("--text") === true;
201
+ const urlAssertion = options.command === "wait" && options.args?.includes("--url") === true;
202
+ actions.push(buildNextToolAction({
203
+ args: ["snapshot", "-i"],
204
+ id: textAssertion ? "inspect-after-text-assertion-failure" : "inspect-after-timeout",
205
+ reason: textAssertion
206
+ ? "Inspect the current page after the text assertion failed before concluding the expected text is absent."
207
+ : options.command === "wait"
208
+ ? "Inspect the current page after the wait condition timed out before retrying with a different selector or timeout."
209
+ : "Inspect the current page after the timed-out browser operation.",
210
+ safety: textAssertion
211
+ ? "Read-only snapshot; use current refs or visible text from this page before retrying the assertion."
212
+ : "Read-only snapshot; do not assume the timed-out interaction completed.",
213
+ }));
214
+ if (urlAssertion) {
215
+ actions.push(buildNextToolAction({
216
+ args: ["open", "about:blank"],
217
+ id: "fresh-session-after-url-wait-timeout",
218
+ reason: "If a preceding click or form submit reported success but the page never navigated, upstream click dispatch may have silently missed (observed with agent-browser 0.34 after many spaced commands); replace about:blank with the target URL and replay the flow as one batch in a fresh session instead of retrying the wait.",
219
+ safety: "Abandons the current browser session; capture page evidence with the inspect action first, and only abandon when the wait target is not simply wrong or slow.",
220
+ sessionMode: "fresh",
221
+ }));
222
+ }
223
+ }
224
+ break;
225
+ case "upstream-error":
226
+ if (isOpenNavigationCommand(options.command)) {
227
+ actions.push(buildNextToolAction({
228
+ args: ["get", "url"],
229
+ id: "inspect-page-after-navigation-error",
230
+ reason: "Check which page, if any, remains active after the navigation or network error.",
231
+ safety: "Read-only URL inspection; verify connectivity and the target URL before retrying navigation.",
232
+ }));
233
+ }
234
+ break;
235
+ case "tab-gone":
236
+ actions.push(buildNextToolAction({
237
+ args: ["tab", "list"],
238
+ id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.tabGoneListTabs,
239
+ reason: "The pinned bound tab is gone; inspect remaining tabs before acting on a neighbor.",
240
+ safety: "Read-only. Prefer a listed tab id, label, or CDP targetId, or open a new tab to rebind.",
241
+ }), buildNextToolAction({
242
+ args: ["tab", "new"],
243
+ id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.tabGoneNewTab,
244
+ reason: "Bind a fresh tab after tab_gone instead of continuing on another session's page.",
245
+ safety: "Opens a new tab in this session and rebinds the pin; pass a URL if you know the intended page.",
246
+ }));
247
+ break;
199
248
  case "tab-drift":
200
249
  if (options.recovery?.kind === "about-blank" || options.recovery?.kind === "tab-drift") {
201
250
  break;
@@ -209,5 +258,13 @@ export function buildAgentBrowserNextActions(options) {
209
258
  break;
210
259
  }
211
260
  }
212
- return actions.length > 0 ? actions : undefined;
261
+ if ((options.artifacts ?? []).some(isPendingRecordingArtifact)) {
262
+ actions.push(buildNextToolAction({
263
+ args: ["record", "stop"],
264
+ id: "stop-pending-recording",
265
+ reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
266
+ safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
267
+ }));
268
+ }
269
+ return applySessionToNextActions(actions.length > 0 ? actions : undefined, options.sessionName);
213
270
  }