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
@@ -1,14 +1,16 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename } from "node:path";
3
- import { findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
3
+ import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
5
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
5
6
  import { needsManagedSession } from "./command-policy.js";
6
- import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
7
- import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
7
+ import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
8
8
  import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
9
9
  import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
10
- export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
10
+ export { extractCommandTokens, extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, parseWaitCommandTokens } from "./argv-descriptor.js";
11
11
  import { isRecord } from "./parsing.js";
12
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
13
+ import { TARGET_AGENT_BROWSER_VERSION } from "./upstream-version.js";
12
14
  const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
13
15
  const CLOUDFLARE_HEADLESS_COMPAT_HOST = "dash.cloudflare.com";
14
16
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
@@ -18,7 +20,9 @@ const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
18
20
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
19
21
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
20
22
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
21
- const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
23
+ const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|relay(?:_|-)?state|saml(?:_|-)?request|saml(?:_|-)?response|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
24
+ const AUTH_STATE_QUERY_PARAM_PATTERN = /^(?:nonce|state)$/i;
25
+ const AUTH_URL_CONTEXT_PATTERN = /(?:^|[./_-])(?:auth|authorize|callback|login|oauth2?|oidc|saml|sso)(?:[./?#_-]|$)/i;
22
26
  const SENSITIVE_FIELD_NAME_PATTERN = /^(?:[A-Za-z0-9_-]*(?:api[_-]?key|access[_-]?key|private[_-]?key|secret(?:[_-]?(?:key|access[_-]?key))?|token|password|passwd|credentials?|database[_-]?url|db[_-]?url|connection[_-]?string|mongo(?:db)?[_-]?uri|redis[_-]?url)|[A-Za-z0-9]*(?:apiKey|ApiKey|apikey|privateKey|PrivateKey|databaseUrl|DatabaseUrl|dbUrl|DbUrl|connectionString|ConnectionString|mongoUri|MongoUri|mongodbUri|MongodbUri|mongoDbUri|MongoDbUri|redisUrl|RedisUrl|Token|Secret|Password|Credential|Credentials)|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
23
27
  const ENV_SECRET_ASSIGNMENT_PATTERN = /\b((?:export\s+)?([A-Za-z_][A-Za-z0-9_-]*)(\s*[:=]\s*))(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)/g;
24
28
  const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM = {
@@ -28,6 +32,7 @@ const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM = {
28
32
  };
29
33
  const FALLBACK_HEADLESS_COMPAT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
30
34
  const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
35
+ const ANDROID_SESSION_NAME_IDENTITY_LENGTH = 20;
31
36
  const MAX_PROJECT_SLUG_LENGTH = 24;
32
37
  const SESSION_NAME_CWD_HASH_LENGTH = 8;
33
38
  const SESSION_NAME_SESSION_ID_LENGTH = 12;
@@ -45,48 +50,57 @@ function redactUrlToken(token) {
45
50
  catch {
46
51
  return token;
47
52
  }
48
- let mutated = false;
49
- if (parsed.username.length > 0) {
53
+ if (parsed.username.length > 0)
50
54
  parsed.username = "[REDACTED]";
51
- mutated = true;
52
- }
53
- if (parsed.password.length > 0) {
55
+ if (parsed.password.length > 0)
54
56
  parsed.password = "[REDACTED]";
55
- mutated = true;
56
- }
57
+ const hashText = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
58
+ const hashParams = hashText.includes("=") ? new URLSearchParams(hashText) : undefined;
59
+ const authContext = AUTH_URL_CONTEXT_PATTERN.test(`${parsed.hostname}${parsed.pathname}`)
60
+ || [...parsed.searchParams.keys(), ...(hashParams?.keys() ?? [])].some(shouldRedactQueryParam);
57
61
  for (const [name] of parsed.searchParams) {
58
- if (shouldRedactQueryParam(name)) {
62
+ if (shouldRedactQueryParam(name) || (authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name))) {
59
63
  parsed.searchParams.set(name, "[REDACTED]");
60
- mutated = true;
61
64
  }
62
65
  }
63
- const hashText = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
64
- if (hashText.includes("=")) {
65
- const hashParams = new URLSearchParams(hashText);
66
+ if (hashParams) {
67
+ let hashMutated = false;
66
68
  for (const [name] of hashParams) {
67
- if (shouldRedactQueryParam(name)) {
69
+ if (shouldRedactQueryParam(name) || (authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name))) {
68
70
  hashParams.set(name, "[REDACTED]");
69
- mutated = true;
71
+ hashMutated = true;
70
72
  }
71
73
  }
72
- if (mutated) {
74
+ if (hashMutated)
73
75
  parsed.hash = `#${hashParams.toString()}`;
74
- }
75
76
  }
76
77
  return parsed.toString();
77
78
  }
78
79
  function redactLooseUrlParameterText(text) {
79
- return text.replace(/([?#&])([^=&#\s]+)=([^&#\s]*)/g, (match, separator, rawName) => {
80
- let name = rawName;
81
- try {
82
- name = decodeURIComponent(rawName.replace(/\+/g, " "));
83
- }
84
- catch {
85
- // Keep the raw name when percent decoding fails.
86
- }
87
- if (!shouldRedactQueryParam(name))
88
- return match;
89
- return `${separator}${rawName}=[REDACTED]`;
80
+ return text.replace(/(?<![^\s"'`<>\])}])[^\s"'`<>\])}]*[?#&][^\s"'`<>\])}]*/g, (token) => {
81
+ const queryNames = [...token.matchAll(/[?#&]([^=&#\s"'`<>\])}]+)=/g)].map((match) => {
82
+ try {
83
+ return decodeURIComponent((match[1] ?? "").replace(/\+/g, " "));
84
+ }
85
+ catch {
86
+ return match[1] ?? "";
87
+ }
88
+ });
89
+ const authContext = AUTH_URL_CONTEXT_PATTERN.test(token) || queryNames.some(shouldRedactQueryParam);
90
+ return token.replace(/([?#&])([^=&#\s"'`<>\])}]+)=([^&#\s"'`<>\])}]*)/g, (match, separator, rawName, rawValue) => {
91
+ if (rawValue === "[REDACTED" || rawValue === "[REDACTED]" || /%5Bredacted%5D/i.test(rawValue))
92
+ return match;
93
+ let name = rawName;
94
+ try {
95
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
96
+ }
97
+ catch {
98
+ // Keep the raw name when percent decoding fails.
99
+ }
100
+ if (!shouldRedactQueryParam(name) && !(authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name)))
101
+ return match;
102
+ return `${separator}${rawName}=[REDACTED]`;
103
+ });
90
104
  });
91
105
  }
92
106
  function redactLooseUrlUserinfo(text) {
@@ -222,7 +236,7 @@ function redactEnvSecretAssignments(text) {
222
236
  });
223
237
  }
224
238
  export function redactSensitiveText(text) {
225
- return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(redactManagedSessionRestoreKeys(text))))
239
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlParameterText(redactLooseUrlUserinfo(redactLooseUrlMatches(text))))
226
240
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
227
241
  .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
228
242
  }
@@ -311,7 +325,7 @@ function parseTimeoutMs(rawValue, minimumValue) {
311
325
  }
312
326
  return parsedValue;
313
327
  }
314
- export function getImplicitSessionIdleTimeoutMs(env = process.env) {
328
+ export function getImplicitSessionIdleTimeoutMs(env = getAgentBrowserProcessEnvironment()) {
315
329
  return parseTimeoutMs(env[IMPLICIT_SESSION_IDLE_TIMEOUT_ENV], 0) ??
316
330
  parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
317
331
  DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS;
@@ -319,21 +333,7 @@ export function getImplicitSessionIdleTimeoutMs(env = process.env) {
319
333
  function countExplicitGlobalFlags(args, targetFlag) {
320
334
  return scanUpstreamGlobalFlagOccurrences(args, targetFlag).length;
321
335
  }
322
- function getUnsupportedLeadingIdentityAssignment(args) {
323
- const commandStartIndex = findCommandStartIndex(args) ?? args.length;
324
- for (let index = 0; index < commandStartIndex; index += 1) {
325
- const token = args[index];
326
- if (token.startsWith("--session="))
327
- return "--session";
328
- if (token.startsWith("--namespace="))
329
- return "--namespace";
330
- const flag = token.split("=", 1)[0] ?? token;
331
- if (!token.includes("=") && PREVALIDATED_VALUE_FLAGS.has(flag))
332
- index += 1;
333
- }
334
- return undefined;
335
- }
336
- export function getImplicitSessionCloseTimeoutMs(env = process.env) {
336
+ export function getImplicitSessionCloseTimeoutMs(env = getAgentBrowserProcessEnvironment()) {
337
337
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
338
338
  }
339
339
  export function resolveManagedSessionState(options) {
@@ -381,15 +381,18 @@ function getManagedSessionRestoreRank(options) {
381
381
  function getRestorableManagedSessionName(value, fallbackSessionName) {
382
382
  return typeof value === "string" && isRestorableManagedSessionName(value, fallbackSessionName) ? value : undefined;
383
383
  }
384
- function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName) {
384
+ function getElectronCleanupClosedManagedSessions(details, fallbackSessionName) {
385
385
  const electron = isRecord(details.electron) ? details.electron : undefined;
386
386
  const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
387
387
  const results = Array.isArray(cleanup?.results) ? cleanup.results : [];
388
- const closedSessionNames = new Set();
388
+ const closedSessions = [];
389
389
  for (const result of results) {
390
390
  if (!isRecord(result) || !Array.isArray(result.steps))
391
391
  continue;
392
392
  const record = isRecord(result.record) ? result.record : undefined;
393
+ const fallbackNamespace = typeof record?.namespace === "string"
394
+ ? record.namespace
395
+ : typeof details.namespace === "string" ? details.namespace : undefined;
393
396
  for (const step of result.steps) {
394
397
  if (!isRecord(step) || step.resource !== "managed-session")
395
398
  continue;
@@ -397,11 +400,12 @@ function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionNam
397
400
  continue;
398
401
  const sessionName = getRestorableManagedSessionName(step.sessionName, fallbackSessionName)
399
402
  ?? getRestorableManagedSessionName(record?.sessionName, fallbackSessionName);
403
+ const namespace = typeof step.namespace === "string" ? step.namespace : fallbackNamespace;
400
404
  if (sessionName)
401
- closedSessionNames.add(sessionName);
405
+ closedSessions.push({ namespace, sessionName });
402
406
  }
403
407
  }
404
- return [...closedSessionNames];
408
+ return closedSessions;
405
409
  }
406
410
  export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName) {
407
411
  const restoreDisabledIdentities = new Map();
@@ -441,16 +445,23 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
441
445
  if (isPlainTextInspectionArgs(args)) {
442
446
  continue;
443
447
  }
444
- for (const sessionName of getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName)) {
445
- applyManagedClose(sessionName);
448
+ for (const cleanupSession of getElectronCleanupClosedManagedSessions(details, fallbackSessionName)) {
449
+ applyManagedClose(cleanupSession.sessionName, cleanupSession.namespace);
446
450
  }
447
451
  const explicitSessionName = extractExplicitSessionName(args);
448
452
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
449
453
  const namespace = canonicalizeAgentBrowserNamespace(typeof details.namespace === "string" ? details.namespace : undefined);
450
454
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
451
455
  const usedImplicitSession = details.usedImplicitSession === true;
456
+ const commandTokens = extractUpstreamCommandTokens(args);
452
457
  const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
453
- const commandClosesSession = isCloseCommand(command);
458
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
459
+ const nestedBatchEndsClosed = batchCloseLifecycle?.endsClosed === true;
460
+ const nestedBatchRemainsActive = batchCloseLifecycle?.endsClosed === false;
461
+ const closeAllApplied = details.closeAllApplied === true
462
+ || (message.isError !== true && isCloseAllCommand(commandTokens))
463
+ || batchHasSuccessfulCloseAll(details.batchSteps);
464
+ const commandClosesSession = isCloseCommand(command) || nestedBatchEndsClosed;
454
465
  const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome : undefined;
455
466
  const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
456
467
  const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
@@ -459,6 +470,14 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
459
470
  ? outcomeAttemptedSessionName ?? getRestorableManagedSessionName(outcomeCurrentSessionName, fallbackSessionName) ?? getRestorableManagedSessionName(sessionName, fallbackSessionName)
460
471
  : undefined;
461
472
  const restorableDetailSessionName = getRestorableManagedSessionName(sessionName, fallbackSessionName);
473
+ if (closeAllApplied && restoredState.active) {
474
+ const restoredKey = getAgentBrowserSessionIdentityKey(restoredState.sessionName, restoredState.namespace);
475
+ const resultKey = restorableDetailSessionName ? getAgentBrowserSessionIdentityKey(restorableDetailSessionName, namespace) : undefined;
476
+ const retainsCurrentSession = nestedBatchRemainsActive && resultKey === restoredKey;
477
+ if (isAgentBrowserSessionIdentityKeyInNamespace(restoredKey, namespace) && !retainsCurrentSession) {
478
+ applyManagedClose(restoredState.sessionName, restoredState.namespace);
479
+ }
480
+ }
462
481
  const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
463
482
  ? restorableDetailSessionName
464
483
  : undefined;
@@ -467,13 +486,13 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
467
486
  if (details.managedSessionRestoreDisabled === true && typeof sessionName === "string") {
468
487
  restoreDisabledIdentities.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { namespace, sessionName });
469
488
  }
470
- const managedSessionName = !explicitSessionName &&
489
+ const managedSessionName = outcomeClosedSessionName ?? (!explicitSessionName &&
471
490
  restorableDetailSessionName &&
472
491
  (usedImplicitSession || sessionMode === "fresh")
473
492
  ? restorableDetailSessionName
474
493
  : commandClosesSession
475
- ? outcomeClosedSessionName ?? explicitCloseSessionName
476
- : undefined;
494
+ ? explicitCloseSessionName
495
+ : undefined);
477
496
  if (!managedSessionName) {
478
497
  continue;
479
498
  }
@@ -490,9 +509,11 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
490
509
  const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
491
510
  const outcomeActiveAfter = outcome?.activeAfter === true;
492
511
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
493
- const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
494
- if (commandClosesSession) {
495
- if (succeeded) {
512
+ const succeeded = outcomeRepresentsActiveCurrentSession || nestedBatchRemainsActive
513
+ ? true
514
+ : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
515
+ if (commandClosesSession || outcomeClosedSessionName) {
516
+ if (nestedBatchEndsClosed || outcomeClosedSessionName || succeeded) {
496
517
  restoreDisabledIdentities.delete(getAgentBrowserSessionIdentityKey(managedSessionName, namespace));
497
518
  applyManagedClose(managedSessionName, namespace);
498
519
  }
@@ -529,15 +550,24 @@ export function createEphemeralSessionSeed() {
529
550
  function createCwdHash(cwd) {
530
551
  return createHash("sha256").update(`cwd:${cwd}`).digest("hex").slice(0, SESSION_NAME_CWD_HASH_LENGTH);
531
552
  }
532
- export function createImplicitSessionName(sessionId, cwd, ephemeralSeed) {
553
+ export function createImplicitSessionName(sessionId, cwd, ephemeralSeed, platform = process.platform) {
554
+ const normalizedSessionId = sessionId?.replaceAll("-", "").toLowerCase();
555
+ if (platform === "android") {
556
+ const identity = normalizedSessionId ? `session:${normalizedSessionId}:cwd:${cwd}` : `ephemeral:${cwd}:${ephemeralSeed}`;
557
+ const digest = createHash("sha256").update(identity).digest("hex").slice(0, ANDROID_SESSION_NAME_IDENTITY_LENGTH);
558
+ return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
559
+ }
533
560
  const slug = basename(cwd)
534
561
  .toLowerCase()
535
562
  .replace(/[^a-z0-9]+/g, "-")
536
563
  .replace(/^-+|-+$/g, "")
537
564
  .slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
538
565
  const cwdHash = createCwdHash(cwd);
539
- const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
540
- if (stableSessionId && stableSessionId.length > 0) {
566
+ if (normalizedSessionId) {
567
+ const stableSessionId = createHash("sha256")
568
+ .update(`session:${normalizedSessionId}`)
569
+ .digest("hex")
570
+ .slice(0, SESSION_NAME_SESSION_ID_LENGTH);
541
571
  return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${stableSessionId}-${cwdHash}`;
542
572
  }
543
573
  const digest = createHash("sha256")
@@ -554,7 +584,7 @@ export function createFreshSessionName(baseSessionName, ephemeralSeed, ordinal)
554
584
  return `${baseSessionName}-fresh-${suffix}`;
555
585
  }
556
586
  function getSingleKeyCommandValidationError(args) {
557
- const { commandInfo, commandTokens } = parseArgvDescriptor(args);
587
+ const { commandInfo, upstreamCommandTokens: commandTokens } = parseArgvDescriptor(args);
558
588
  const command = commandInfo.command;
559
589
  if (command !== "press" && command !== "key" && command !== "keydown" && command !== "keyup")
560
590
  return undefined;
@@ -564,14 +594,20 @@ function getSingleKeyCommandValidationError(args) {
564
594
  return `agent-browser ${label} accepts exactly one key argument. Do not pass a selector or ref to ${label}; focus or click the target first, then run ${command} <key> (for example: focus @e1, then press Enter).`;
565
595
  }
566
596
  function getBareMcpValidationError(args) {
567
- const { commandInfo, commandTokens } = parseArgvDescriptor(args);
597
+ const { commandInfo, upstreamCommandTokens: commandTokens } = parseArgvDescriptor(args);
568
598
  if (commandInfo.command !== "mcp")
569
599
  return undefined;
570
600
  if (commandTokens.includes("--help") || commandTokens.includes("-h"))
571
601
  return undefined;
572
602
  return "agent-browser mcp starts a stdio MCP server for external MCP clients, not a one-shot native agent_browser tool workflow. Use the native agent_browser tool modes directly, or configure an MCP client to launch `agent-browser mcp`. Use `mcp --help` for help.";
573
603
  }
574
- export function validateToolArgs(args) {
604
+ function getUnsupportedInlineWaitDownloadError(args) {
605
+ const descriptor = parseArgvDescriptor(args);
606
+ if (descriptor.commandInfo.command !== "wait" || !descriptor.upstreamCommandTokens.some((token) => token.startsWith("--download=")))
607
+ return undefined;
608
+ return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`wait --download=<path>\`. Pass the optional path as a separate argument: \`wait --download <path>\` (or \`wait -d <path>\`).`;
609
+ }
610
+ export function validateToolArgs(args, options = {}) {
575
611
  if (args.length === 0) {
576
612
  return "`args` must contain at least one agent-browser command token.";
577
613
  }
@@ -583,31 +619,34 @@ export function validateToolArgs(args) {
583
619
  if (sessionModeArg) {
584
620
  return "Do not pass `--session-mode` in args. Use the top-level agent_browser `sessionMode` field instead, for example { args: [\"--profile\", \"Default\", \"open\", \"https://example.com\"], sessionMode: \"fresh\" }.";
585
621
  }
586
- return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args);
622
+ const inspection = !options.batchStep && isPlainTextInspectionArgs(args);
623
+ const invalidValueFlag = inspection ? undefined : getInvalidValueFlagDetails(args, !options.batchStep);
624
+ if (invalidValueFlag?.reason === "unsupported-assignment")
625
+ return formatInvalidValueFlagError(invalidValueFlag, options.batchStep);
626
+ return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
587
627
  }
588
- function getInvalidValueFlagDetails(args) {
628
+ function getInvalidValueFlagDetails(args, allowRestoreAssignment = true) {
589
629
  for (let index = 0; index < args.length; index += 1) {
590
630
  const token = args[index];
591
631
  if (!token.startsWith("-")) {
592
632
  continue;
593
633
  }
594
634
  const normalizedToken = token.split("=", 1)[0] ?? token;
595
- if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
596
- continue;
635
+ if (token.includes("=")
636
+ && (PREVALIDATED_VALUE_FLAGS.has(normalizedToken)
637
+ || GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken)
638
+ || (!allowRestoreAssignment && normalizedToken === "--restore"))) {
639
+ return {
640
+ flag: normalizedToken,
641
+ index,
642
+ reason: "unsupported-assignment",
643
+ };
597
644
  }
598
- if (token.includes("=")) {
599
- const value = token.slice(token.indexOf("=") + 1).trim();
600
- if (value.length === 0) {
601
- return {
602
- flag: normalizedToken,
603
- index,
604
- reason: "missing-value",
605
- };
606
- }
645
+ if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
607
646
  continue;
608
647
  }
609
648
  const receivedToken = args[index + 1];
610
- if (receivedToken === undefined) {
649
+ if (receivedToken === undefined || (normalizedToken === "--args" && receivedToken.length === 0)) {
611
650
  return {
612
651
  flag: normalizedToken,
613
652
  index,
@@ -626,7 +665,15 @@ function getInvalidValueFlagDetails(args) {
626
665
  }
627
666
  return undefined;
628
667
  }
629
- function formatInvalidValueFlagError(details) {
668
+ function formatInvalidValueFlagError(details, batchStep = false) {
669
+ if (details.reason === "unsupported-assignment") {
670
+ if (batchStep) {
671
+ return details.flag === "--restore"
672
+ ? "Global `--restore=<key>` belongs before `batch` in top-level args, not inside a batch step."
673
+ : `Global \`${details.flag}=<value>\` is not supported inside a batch step. Move \`${details.flag}\` and its value before \`batch\` as separate top-level args.`;
674
+ }
675
+ return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`${details.flag}=<value>\`. Pass \`${details.flag}\` and its value as separate arguments.`;
676
+ }
630
677
  if (details.reason === "unexpected-flag" && details.receivedToken) {
631
678
  return `Flag \`${details.flag}\` requires a value, but received \`${details.receivedToken}\` instead. Pass a non-flag value immediately after \`${details.flag}\`.`;
632
679
  }
@@ -683,7 +730,7 @@ function parseComparableNavigationUrl(url) {
683
730
  export function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
684
731
  return DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM[platform] ?? FALLBACK_HEADLESS_COMPAT_USER_AGENT;
685
732
  }
686
- export function canUseHeadlessCompatibilityUserAgent(args, env = process.env) {
733
+ export function canUseHeadlessCompatibilityUserAgent(args, env = getAgentBrowserProcessEnvironment()) {
687
734
  if (hasFlagToken(args, "--user-agent") || hasFlagToken(args, "--args"))
688
735
  return false;
689
736
  if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p"))
@@ -716,6 +763,14 @@ function getCompatibilityWorkaround(args, commandInfo) {
716
763
  reason: "OpenAI web properties currently challenge the default headless Chrome user agent; inject a normal Chrome user agent to preserve the default headless workflow without requiring headed mode or auto-connect.",
717
764
  };
718
765
  }
766
+ function stripExplicitSessionArgs(args) {
767
+ const sessionTokenIndexes = new Set();
768
+ for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--session")) {
769
+ sessionTokenIndexes.add(occurrence.index);
770
+ sessionTokenIndexes.add(occurrence.index + 1);
771
+ }
772
+ return args.filter((_token, index) => !sessionTokenIndexes.has(index));
773
+ }
719
774
  function stripExplicitNamespaceArgs(args) {
720
775
  const namespaceTokenIndexes = new Set();
721
776
  for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--namespace")) {
@@ -731,7 +786,6 @@ export function getStartupScopedFlags(args) {
731
786
  }
732
787
  export function buildExecutionPlan(args, options) {
733
788
  const invalidValueFlag = getInvalidValueFlagDetails(args);
734
- const unsupportedIdentityAssignment = getUnsupportedLeadingIdentityAssignment(args);
735
789
  const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
736
790
  const explicitNamespace = extractExplicitNamespace(args);
737
791
  const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
@@ -741,7 +795,7 @@ export function buildExecutionPlan(args, options) {
741
795
  const commandInfo = argvDescriptor.commandInfo;
742
796
  const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
743
797
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
744
- let namespace = explicitNamespace;
798
+ let namespace = explicitNamespacePresent ? explicitNamespace ?? "" : undefined;
745
799
  if (plainTextInspection) {
746
800
  return {
747
801
  commandInfo,
@@ -763,16 +817,6 @@ export function buildExecutionPlan(args, options) {
763
817
  validationError: formatInvalidValueFlagError(invalidValueFlag),
764
818
  };
765
819
  }
766
- if (unsupportedIdentityAssignment) {
767
- return {
768
- commandInfo: {},
769
- effectiveArgs,
770
- plainTextInspection: false,
771
- startupScopedFlags: [],
772
- usedImplicitSession: false,
773
- validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser 0.33.2. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
774
- };
775
- }
776
820
  for (const flag of ["--session", "--namespace"]) {
777
821
  if (countExplicitGlobalFlags(args, flag) <= 1)
778
822
  continue;
@@ -786,8 +830,13 @@ export function buildExecutionPlan(args, options) {
786
830
  };
787
831
  }
788
832
  const explicitSessionName = extractExplicitSessionName(args);
789
- if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
790
- namespace = resolveAgentBrowserNamespace(args, process.env.AGENT_BROWSER_NAMESPACE);
833
+ if (explicitSessionName && !explicitNamespacePresent) {
834
+ const targetsCurrentManagedSession = options.managedSessionActive
835
+ && getAgentBrowserSessionIdentityKey(explicitSessionName, managedSessionNamespace)
836
+ === getAgentBrowserSessionIdentityKey(options.managedSessionName, managedSessionNamespace);
837
+ namespace = targetsCurrentManagedSession
838
+ ? managedSessionNamespace
839
+ : resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
791
840
  }
792
841
  const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
793
842
  let argsToAppend = args;
@@ -816,8 +865,8 @@ export function buildExecutionPlan(args, options) {
816
865
  ].join(" ");
817
866
  }
818
867
  else {
819
- namespace = explicitNamespacePresent ? explicitNamespace : managedSessionNamespace;
820
- if (namespace)
868
+ namespace = explicitNamespacePresent ? explicitNamespace ?? "" : managedSessionNamespace;
869
+ if (namespace !== undefined)
821
870
  effectiveArgs.push("--namespace", namespace);
822
871
  effectiveArgs.push("--session", options.managedSessionName);
823
872
  if (explicitNamespacePresent)
@@ -828,7 +877,7 @@ export function buildExecutionPlan(args, options) {
828
877
  }
829
878
  }
830
879
  else if (shouldCreateFreshManagedSession && commandNeedsManagedSession) {
831
- if (namespace)
880
+ if (namespace !== undefined)
832
881
  effectiveArgs.push("--namespace", namespace);
833
882
  effectiveArgs.push("--session", options.freshSessionName);
834
883
  if (explicitNamespacePresent)
@@ -836,14 +885,43 @@ export function buildExecutionPlan(args, options) {
836
885
  managedSessionName = options.freshSessionName;
837
886
  sessionName = options.freshSessionName;
838
887
  }
839
- if (!compatibilityWorkaround
840
- && canUseHeadlessCompatibilityUserAgent(args)
841
- && options.managedSessionActive
888
+ const targetsActiveManagedSession = options.managedSessionActive
842
889
  && sessionName
843
- && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace)) {
844
- compatibilityWorkaround = options.managedSessionCompatibilityWorkaround;
890
+ && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace);
891
+ if (targetsActiveManagedSession && startupScopedFlags.length > 0 && !isCloseCommand(commandInfo.command) && !validationError) {
892
+ const recoveryArgs = explicitSessionName ? stripExplicitSessionArgs(args) : args;
893
+ recoveryHint = {
894
+ exampleArgs: recoveryArgs,
895
+ exampleParams: { args: recoveryArgs, sessionMode: "fresh" },
896
+ reason: `Launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) need a fresh upstream launch once the extension-managed session is already active.`,
897
+ recommendedSessionMode: "fresh",
898
+ };
899
+ validationError = [
900
+ `The current extension-managed agent-browser session is already running, so launch-scoped flags ${startupScopedFlags.join(", ")} would replace or be ignored by upstream agent-browser.`,
901
+ explicitSessionName
902
+ ? "Remove the explicit `--session` and retry with `sessionMode: \"fresh\"` to force a fresh upstream launch."
903
+ : "Retry this call with `sessionMode: \"fresh\"` to force a fresh upstream launch.",
904
+ ].join(" ");
905
+ }
906
+ if (targetsActiveManagedSession && canUseHeadlessCompatibilityUserAgent(args)) {
907
+ if (requestedCompatibilityWorkaround && !options.managedSessionCompatibilityWorkaround && !validationError) {
908
+ compatibilityWorkaround = undefined;
909
+ const recoveryArgs = explicitSessionName ? stripExplicitSessionArgs(args) : args;
910
+ recoveryHint = {
911
+ exampleArgs: recoveryArgs,
912
+ exampleParams: { args: recoveryArgs, sessionMode: "fresh" },
913
+ reason: "The requested site compatibility user agent is launch-scoped and needs a fresh browser session.",
914
+ recommendedSessionMode: "fresh",
915
+ };
916
+ validationError = explicitSessionName
917
+ ? "The current extension-managed agent-browser session is already running without the user agent required by this site. Remove the explicit `--session` and retry with `sessionMode: \"fresh\"` so the compatibility user agent is applied at launch."
918
+ : "The current extension-managed agent-browser session is already running without the user agent required by this site. Retry this call with `sessionMode: \"fresh\"` so the compatibility user agent is applied at launch.";
919
+ }
920
+ else {
921
+ compatibilityWorkaround = requestedCompatibilityWorkaround ?? options.managedSessionCompatibilityWorkaround;
922
+ }
845
923
  }
846
- if (compatibilityWorkaround) {
924
+ if (compatibilityWorkaround && !targetsActiveManagedSession) {
847
925
  effectiveArgs.push("--user-agent", getDefaultHeadlessCompatUserAgent());
848
926
  }
849
927
  effectiveArgs.push(...argsToAppend);