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,22 +1,25 @@
1
1
  /**
2
2
  * Purpose: Build safe, deterministic agent-browser invocations and persisted session state for the pi-agent-browser extension.
3
3
  * Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity, restore managed-session state from persisted tool details, redact sensitive invocation text, classify browser-oriented prompts, and build the effective CLI argument list passed to the upstream agent-browser binary.
4
- * Scope: Pure runtime-planning helpers only; no subprocess execution or filesystem access lives here.
4
+ * Scope: Runtime-planning helpers only; no subprocess execution or filesystem access.
5
5
  * Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
6
6
  * Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, keeps plain-text inspection stateless,
7
- * and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate, and the narrow
8
- * OpenAI/ChatGPT headless compatibility `--user-agent` when that workaround applies.
7
+ * and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate, the narrow
8
+ * site-specific headless compatibility `--user-agent` when that workaround applies.
9
9
  */
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { basename } from "node:path";
12
12
  import { findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
13
- import { GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, PREVALIDATED_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
13
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isBooleanFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
14
14
  import { needsManagedSession } from "./command-policy.js";
15
+ import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
15
16
  import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
16
- import { LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL } from "./launch-scoped-flags.js";
17
+ import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
18
+ import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
17
19
  export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
18
20
  import { isRecord } from "./parsing.js";
19
21
  const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
22
+ const CLOUDFLARE_HEADLESS_COMPAT_HOST = "dash.cloudflare.com";
20
23
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
21
24
  const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDLE_TIMEOUT_MS";
22
25
  const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
@@ -228,7 +231,7 @@ function redactEnvSecretAssignments(text) {
228
231
  });
229
232
  }
230
233
  export function redactSensitiveText(text) {
231
- return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(text)))
234
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(redactManagedSessionRestoreKeys(text))))
232
235
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
233
236
  .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
234
237
  }
@@ -322,11 +325,30 @@ export function getImplicitSessionIdleTimeoutMs(env = process.env) {
322
325
  parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
323
326
  DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS;
324
327
  }
328
+ function countExplicitGlobalFlags(args, targetFlag) {
329
+ return scanUpstreamGlobalFlagOccurrences(args, targetFlag).length;
330
+ }
331
+ function getUnsupportedLeadingIdentityAssignment(args) {
332
+ const commandStartIndex = findCommandStartIndex(args) ?? args.length;
333
+ for (let index = 0; index < commandStartIndex; index += 1) {
334
+ const token = args[index];
335
+ if (token.startsWith("--session="))
336
+ return "--session";
337
+ if (token.startsWith("--namespace="))
338
+ return "--namespace";
339
+ const flag = token.split("=", 1)[0] ?? token;
340
+ if (!token.includes("=") && PREVALIDATED_VALUE_FLAGS.has(flag))
341
+ index += 1;
342
+ }
343
+ return undefined;
344
+ }
325
345
  export function getImplicitSessionCloseTimeoutMs(env = process.env) {
326
346
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
327
347
  }
328
348
  export function resolveManagedSessionState(options) {
329
- const { command, managedSessionName, managedSessionNamespace, priorActive, priorNamespace, priorSessionName, succeeded } = options;
349
+ const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
350
+ const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
351
+ const priorNamespace = canonicalizeAgentBrowserNamespace(options.priorNamespace);
330
352
  if (!managedSessionName) {
331
353
  return { active: priorActive, ...(priorNamespace ? { namespace: priorNamespace } : {}), sessionName: priorSessionName };
332
354
  }
@@ -391,6 +413,7 @@ function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionNam
391
413
  return [...closedSessionNames];
392
414
  }
393
415
  export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName) {
416
+ const restoreDisabledIdentities = new Map();
394
417
  let restoredState = {
395
418
  active: false,
396
419
  sessionName: fallbackSessionName,
@@ -400,6 +423,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
400
423
  let freshSessionOrdinal = 0;
401
424
  const freshSessionRanks = new Map();
402
425
  const applyManagedClose = (sessionName, namespace) => {
426
+ namespace = canonicalizeAgentBrowserNamespace(namespace);
403
427
  const restoreRank = getManagedSessionRestoreRank({
404
428
  fallbackSessionName,
405
429
  freshSessionRanks,
@@ -431,7 +455,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
431
455
  }
432
456
  const explicitSessionName = extractExplicitSessionName(args);
433
457
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
434
- const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
458
+ const namespace = canonicalizeAgentBrowserNamespace(typeof details.namespace === "string" ? details.namespace : undefined);
435
459
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
436
460
  const usedImplicitSession = details.usedImplicitSession === true;
437
461
  const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
@@ -447,6 +471,11 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
447
471
  const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
448
472
  ? restorableDetailSessionName
449
473
  : undefined;
474
+ // Sticky restore policy is session-identity state and must apply even for explicit
475
+ // `--session <current-managed>` rows that are not used for managed-session lifecycle replay.
476
+ if (details.managedSessionRestoreDisabled === true && typeof sessionName === "string") {
477
+ restoreDisabledIdentities.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { namespace, sessionName });
478
+ }
450
479
  const managedSessionName = !explicitSessionName &&
451
480
  restorableDetailSessionName &&
452
481
  (usedImplicitSession || sessionMode === "fresh")
@@ -472,8 +501,10 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
472
501
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
473
502
  const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
474
503
  if (commandClosesSession) {
475
- if (succeeded)
504
+ if (succeeded) {
505
+ restoreDisabledIdentities.delete(getAgentBrowserSessionIdentityKey(managedSessionName, namespace));
476
506
  applyManagedClose(managedSessionName, namespace);
507
+ }
477
508
  continue;
478
509
  }
479
510
  const staleCompletion = succeeded && restoreRank < activeRestoreRank;
@@ -498,6 +529,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
498
529
  ...restoredState,
499
530
  ...(closedSessionName ? { closedSessionName } : {}),
500
531
  freshSessionOrdinal,
532
+ managedSessionRestoreDisabledIdentities: [...restoreDisabledIdentities.values()],
501
533
  };
502
534
  }
503
535
  export function createEphemeralSessionSeed() {
@@ -515,13 +547,13 @@ export function createImplicitSessionName(sessionId, cwd, ephemeralSeed) {
515
547
  const cwdHash = createCwdHash(cwd);
516
548
  const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
517
549
  if (stableSessionId && stableSessionId.length > 0) {
518
- return `piab-${slug}-${stableSessionId}-${cwdHash}`;
550
+ return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${stableSessionId}-${cwdHash}`;
519
551
  }
520
552
  const digest = createHash("sha256")
521
553
  .update(`ephemeral:${cwd}:${ephemeralSeed}`)
522
554
  .digest("hex")
523
555
  .slice(0, SESSION_NAME_SESSION_ID_LENGTH);
524
- return `piab-${slug}-${digest}-${cwdHash}`;
556
+ return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${digest}-${cwdHash}`;
525
557
  }
526
558
  export function createFreshSessionName(baseSessionName, ephemeralSeed, ordinal) {
527
559
  const suffix = createHash("sha256")
@@ -623,21 +655,6 @@ function getFlagValue(args, flag) {
623
655
  }
624
656
  return undefined;
625
657
  }
626
- function isBooleanFlagEnabled(args, flag) {
627
- for (const [index, token] of args.entries()) {
628
- if (token === flag) {
629
- const nextToken = args[index + 1]?.trim().toLowerCase();
630
- if (nextToken === "false") {
631
- return false;
632
- }
633
- return true;
634
- }
635
- if (token.startsWith(`${flag}=`)) {
636
- return token.slice(flag.length + 1).trim().toLowerCase() !== "false";
637
- }
638
- }
639
- return false;
640
- }
641
658
  function normalizeComparableUrl(url) {
642
659
  const normalizedUrl = url.trim();
643
660
  if (normalizedUrl.length === 0) {
@@ -683,90 +700,44 @@ function parseComparableNavigationUrl(url) {
683
700
  }
684
701
  }
685
702
  }
686
- function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
703
+ export function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
687
704
  return DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM[platform] ?? FALLBACK_HEADLESS_COMPAT_USER_AGENT;
688
705
  }
689
- function getCompatibilityWorkaround(args, commandInfo) {
690
- if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand) {
691
- return undefined;
692
- }
693
- if (hasFlagToken(args, "--user-agent")) {
694
- return undefined;
695
- }
696
- if (isBooleanFlagEnabled(args, "--headed")) {
697
- return undefined;
698
- }
699
- if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p") || isBooleanFlagEnabled(args, "--auto-connect")) {
700
- return undefined;
701
- }
706
+ export function canUseHeadlessCompatibilityUserAgent(args) {
707
+ if (hasFlagToken(args, "--user-agent") || isBooleanFlagEnabled(args, "--headed"))
708
+ return false;
709
+ if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p") || isBooleanFlagEnabled(args, "--auto-connect"))
710
+ return false;
702
711
  const engine = getFlagValue(args, "--engine");
703
- if (engine && engine !== "chrome") {
712
+ return !engine || engine === "chrome";
713
+ }
714
+ function getCompatibilityWorkaround(args, commandInfo) {
715
+ if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand || !canUseHeadlessCompatibilityUserAgent(args))
704
716
  return undefined;
705
- }
706
717
  const parsedTargetUrl = parseComparableNavigationUrl(commandInfo.subcommand);
707
- if (!parsedTargetUrl || !["http:", "https:"].includes(parsedTargetUrl.protocol)) {
718
+ if (!parsedTargetUrl || !["http:", "https:"].includes(parsedTargetUrl.protocol))
708
719
  return undefined;
709
- }
710
720
  const hostname = parsedTargetUrl.hostname.toLowerCase();
711
- if (!OPENAI_HEADLESS_COMPAT_HOSTS.has(hostname)) {
712
- return undefined;
721
+ if (hostname === CLOUDFLARE_HEADLESS_COMPAT_HOST) {
722
+ return {
723
+ id: "cloudflare-headless-user-agent",
724
+ reason: "Cloudflare Dashboard challenges the default headless Chrome user agent; inject a normal Chrome user agent so authenticated headless browsing reaches the dashboard instead of Turnstile.",
725
+ };
713
726
  }
727
+ if (!OPENAI_HEADLESS_COMPAT_HOSTS.has(hostname))
728
+ return undefined;
714
729
  return {
715
730
  id: "chatgpt-headless-user-agent",
716
731
  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
732
  };
718
733
  }
719
- export function extractExplicitSessionName(args) {
720
- for (const [index, token] of args.entries()) {
721
- if (token === "--session") {
722
- return args[index + 1];
723
- }
724
- if (token.startsWith("--session=")) {
725
- return token.slice("--session=".length);
726
- }
727
- }
728
- return undefined;
729
- }
730
- export function extractExplicitNamespace(args) {
731
- for (const [index, token] of args.entries()) {
732
- if (token === "--namespace") {
733
- return args[index + 1];
734
- }
735
- if (token.startsWith("--namespace=")) {
736
- return token.slice("--namespace=".length);
737
- }
738
- }
739
- return undefined;
740
- }
741
734
  function stripExplicitNamespaceArgs(args) {
742
- const stripped = [];
743
- for (let index = 0; index < args.length; index += 1) {
744
- const token = args[index];
745
- if (token === "--namespace") {
746
- index += 1;
747
- continue;
748
- }
749
- if (token.startsWith("--namespace="))
750
- continue;
751
- stripped.push(token);
735
+ const namespaceTokenIndexes = new Set();
736
+ for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--namespace")) {
737
+ namespaceTokenIndexes.add(occurrence.index);
738
+ namespaceTokenIndexes.add(occurrence.index + 1);
752
739
  }
753
- return stripped;
754
- }
755
- function hasLaunchScopedFlagToken(args, flag) {
756
- const commandStartIndex = findCommandStartIndex(args);
757
- const command = commandStartIndex === undefined ? undefined : args[commandStartIndex];
758
- return args.some((token, index) => {
759
- if (token !== flag && !token.startsWith(`${flag}=`))
760
- return false;
761
- if (flag === "--auto-connect")
762
- return isBooleanFlagEnabled(args, flag);
763
- if (flag === "--restore" && token === "--restore" && optionalGlobalValueFlagConsumesNext(flag, args[index + 1]))
764
- return true;
765
- if (flag === "--state" && command === "wait" && commandStartIndex !== undefined && index > commandStartIndex) {
766
- return false;
767
- }
768
- return true;
769
- });
740
+ return args.filter((_token, index) => !namespaceTokenIndexes.has(index));
770
741
  }
771
742
  export function getStartupScopedFlags(args) {
772
743
  return LAUNCH_SCOPED_FLAG_DEFINITIONS
@@ -775,14 +746,27 @@ export function getStartupScopedFlags(args) {
775
746
  }
776
747
  export function buildExecutionPlan(args, options) {
777
748
  const invalidValueFlag = getInvalidValueFlagDetails(args);
749
+ const unsupportedIdentityAssignment = getUnsupportedLeadingIdentityAssignment(args);
750
+ const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
778
751
  const explicitNamespace = extractExplicitNamespace(args);
779
- const startupScopedFlags = getStartupScopedFlags(args).filter((flag) => !(flag === "--namespace" && explicitNamespace === options.managedSessionNamespace));
752
+ const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
753
+ const startupScopedFlags = getStartupScopedFlags(args).filter((flag) => !(flag === "--namespace" && explicitNamespacePresent && explicitNamespace === managedSessionNamespace));
780
754
  const plainTextInspection = isPlainTextInspectionArgs(args);
781
755
  const argvDescriptor = parseArgvDescriptor(args);
782
756
  const commandInfo = argvDescriptor.commandInfo;
783
757
  const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
784
758
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
785
759
  let namespace = explicitNamespace;
760
+ if (plainTextInspection) {
761
+ return {
762
+ commandInfo,
763
+ effectiveArgs,
764
+ namespace,
765
+ plainTextInspection,
766
+ startupScopedFlags,
767
+ usedImplicitSession: false,
768
+ };
769
+ }
786
770
  if (invalidValueFlag) {
787
771
  return {
788
772
  commandInfo: {},
@@ -794,22 +778,38 @@ export function buildExecutionPlan(args, options) {
794
778
  validationError: formatInvalidValueFlagError(invalidValueFlag),
795
779
  };
796
780
  }
797
- if (plainTextInspection) {
781
+ if (unsupportedIdentityAssignment) {
798
782
  return {
799
- commandInfo,
783
+ commandInfo: {},
800
784
  effectiveArgs,
801
- namespace,
802
- plainTextInspection,
803
- startupScopedFlags,
785
+ plainTextInspection: false,
786
+ startupScopedFlags: [],
804
787
  usedImplicitSession: false,
788
+ validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser 0.33.2. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
789
+ };
790
+ }
791
+ for (const flag of ["--session", "--namespace"]) {
792
+ if (countExplicitGlobalFlags(args, flag) <= 1)
793
+ continue;
794
+ return {
795
+ commandInfo: {},
796
+ effectiveArgs,
797
+ plainTextInspection: false,
798
+ startupScopedFlags: [],
799
+ usedImplicitSession: false,
800
+ validationError: `Multiple ${flag} flags are not supported. Pass a single ${flag} value; upstream uses the last occurrence while this wrapper would otherwise mis-attribute managed-session ownership.`,
805
801
  };
806
802
  }
807
803
  const explicitSessionName = extractExplicitSessionName(args);
804
+ if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
805
+ namespace = resolveAgentBrowserNamespace(args, process.env.AGENT_BROWSER_NAMESPACE);
806
+ }
808
807
  const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
809
808
  let argsToAppend = args;
810
- const compatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
811
- if (explicitSessionName && explicitNamespace) {
812
- effectiveArgs.push("--namespace", explicitNamespace);
809
+ const requestedCompatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
810
+ let compatibilityWorkaround = requestedCompatibilityWorkaround;
811
+ if (explicitSessionName && explicitNamespacePresent) {
812
+ effectiveArgs.push("--namespace", explicitNamespace ?? "");
813
813
  argsToAppend = stripExplicitNamespaceArgs(args);
814
814
  }
815
815
  let managedSessionName;
@@ -831,11 +831,11 @@ export function buildExecutionPlan(args, options) {
831
831
  ].join(" ");
832
832
  }
833
833
  else {
834
- namespace = explicitNamespace ?? options.managedSessionNamespace;
834
+ namespace = explicitNamespacePresent ? explicitNamespace : managedSessionNamespace;
835
835
  if (namespace)
836
836
  effectiveArgs.push("--namespace", namespace);
837
837
  effectiveArgs.push("--session", options.managedSessionName);
838
- if (explicitNamespace)
838
+ if (explicitNamespacePresent)
839
839
  argsToAppend = stripExplicitNamespaceArgs(args);
840
840
  managedSessionName = options.managedSessionName;
841
841
  sessionName = options.managedSessionName;
@@ -846,11 +846,18 @@ export function buildExecutionPlan(args, options) {
846
846
  if (namespace)
847
847
  effectiveArgs.push("--namespace", namespace);
848
848
  effectiveArgs.push("--session", options.freshSessionName);
849
- if (explicitNamespace)
849
+ if (explicitNamespacePresent)
850
850
  argsToAppend = stripExplicitNamespaceArgs(args);
851
851
  managedSessionName = options.freshSessionName;
852
852
  sessionName = options.freshSessionName;
853
853
  }
854
+ if (!compatibilityWorkaround
855
+ && canUseHeadlessCompatibilityUserAgent(args)
856
+ && options.managedSessionActive
857
+ && sessionName
858
+ && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace)) {
859
+ compatibilityWorkaround = options.managedSessionCompatibilityWorkaround;
860
+ }
854
861
  if (compatibilityWorkaround) {
855
862
  effectiveArgs.push("--user-agent", getDefaultHeadlessCompatUserAgent());
856
863
  }
@@ -5,7 +5,8 @@
5
5
  * Usage: `index.ts` creates one store per Pi session lifecycle and records observations through update tokens.
6
6
  * Invariants/Assumptions: One tool-call update token must govern all page-state observations from that invocation; stale overlapping updates must not overwrite newer state.
7
7
  */
8
- import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand } from "./command-taxonomy.js";
8
+ import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
9
+ import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
9
10
  import { isRecord } from "./parsing.js";
10
11
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
11
12
  import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
@@ -82,6 +83,9 @@ function extractBatchResultCommand(item) {
82
83
  }
83
84
  export function extractSessionTabTargetFromCommandData(commandTokens, data) {
84
85
  const [command, subcommand] = commandTokens;
86
+ if (command === "get" && subcommand === "url") {
87
+ return normalizeSessionTabTarget({ url: extractStringResultField(data, "url") ?? extractStringResultField(data, "result") });
88
+ }
85
89
  return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
86
90
  }
87
91
  export function extractSessionTabTargetFromBatchResults(data) {
@@ -124,10 +128,12 @@ export function deriveSessionTabTarget(options) {
124
128
  const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
125
129
  ? undefined
126
130
  : extractSessionTabTargetFromData(options.data);
127
- return (normalizeSessionTabTarget(options.navigationSummary) ??
128
- extractSessionTabTargetFromBatchResults(options.data) ??
129
- commandDataTarget ??
130
- options.previousTarget);
131
+ const observedTarget = normalizeSessionTabTarget(options.navigationSummary)
132
+ ?? extractSessionTabTargetFromBatchResults(options.data)
133
+ ?? commandDataTarget;
134
+ if (observedTarget || !isUnverifiedPageTransitionCommand(options.command, options.subcommand))
135
+ return observedTarget ?? options.previousTarget;
136
+ return undefined;
131
137
  }
132
138
  function batchContainsOnlyReadOnlyDiagnosticTargets(data) {
133
139
  if (!Array.isArray(data) || data.length === 0) {
@@ -263,11 +269,12 @@ function getRestoredRefSnapshot(details) {
263
269
  : undefined,
264
270
  };
265
271
  }
266
- function getLatestTabTargetOrder(targets) {
272
+ function getLatestTabTargetOrder(targets, unknownTargets) {
267
273
  let latestOrder = 0;
268
- for (const target of targets.values()) {
274
+ for (const target of targets.values())
269
275
  latestOrder = Math.max(latestOrder, target.order);
270
- }
276
+ for (const order of unknownTargets.values())
277
+ latestOrder = Math.max(latestOrder, order);
271
278
  return latestOrder;
272
279
  }
273
280
  function getLatestRefStateOrder(snapshots, invalidations) {
@@ -278,8 +285,8 @@ function getLatestRefStateOrder(snapshots, invalidations) {
278
285
  latestOrder = Math.max(latestOrder, invalidation.order);
279
286
  return latestOrder;
280
287
  }
281
- function shouldApplyTabTargetUpdate(current, updateOrder) {
282
- return !current || updateOrder >= current.order;
288
+ function shouldApplyTabTargetUpdate(current, unknownOrder, updateOrder) {
289
+ return updateOrder >= Math.max(current?.order ?? 0, unknownOrder ?? 0);
283
290
  }
284
291
  function shouldApplyRefStateUpdate(options) {
285
292
  const currentOrder = Math.max(options.currentSnapshot?.order ?? 0, options.currentInvalidation?.order ?? 0);
@@ -292,14 +299,13 @@ function stripRefSnapshotInvalidationOrder(invalidation) {
292
299
  return invalidation ? { reason: invalidation.reason, summary: invalidation.summary } : undefined;
293
300
  }
294
301
  export function getSessionPageStateKey(sessionName, namespace) {
295
- if (!sessionName)
296
- return undefined;
297
- return namespace ? `${namespace}\u0000${sessionName}` : sessionName;
302
+ return sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : undefined;
298
303
  }
299
304
  export class SessionPageState {
300
305
  refSnapshotInvalidations = new Map();
301
306
  refSnapshots = new Map();
302
307
  tabPinningReasons = new Map();
308
+ tabTargetUnknownOrders = new Map();
303
309
  tabTargets = new Map();
304
310
  updateOrder = 0;
305
311
  static fromBranch(branch) {
@@ -327,13 +333,23 @@ export class SessionPageState {
327
333
  continue;
328
334
  }
329
335
  const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
336
+ const tabTargetUnknown = details.sessionTabTargetUnknown === true;
330
337
  const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
331
338
  const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
332
- if (!tabTarget && !refSnapshotInvalidation && !refSnapshot)
339
+ if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot)
333
340
  continue;
334
341
  restoredOrder += 1;
335
- if (tabTarget)
342
+ if (tabTargetUnknown) {
343
+ state.refSnapshotInvalidations.delete(sessionKey);
344
+ state.refSnapshots.delete(sessionKey);
345
+ state.tabTargets.delete(sessionKey);
346
+ state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
347
+ continue;
348
+ }
349
+ if (tabTarget) {
350
+ state.tabTargetUnknownOrders.delete(sessionKey);
336
351
  state.tabTargets.set(sessionKey, { order: restoredOrder, target: tabTarget });
352
+ }
337
353
  if (refSnapshotInvalidation) {
338
354
  state.refSnapshots.delete(sessionKey);
339
355
  state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
@@ -343,7 +359,7 @@ export class SessionPageState {
343
359
  state.refSnapshots.set(sessionKey, { ...refSnapshot, order: restoredOrder });
344
360
  }
345
361
  }
346
- state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
362
+ state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets, state.tabTargetUnknownOrders), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
347
363
  state.tabPinningReasons = new Map([...state.tabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
348
364
  return state;
349
365
  }
@@ -355,6 +371,7 @@ export class SessionPageState {
355
371
  this.refSnapshotInvalidations = new Map();
356
372
  this.refSnapshots = new Map();
357
373
  this.tabPinningReasons = new Map();
374
+ this.tabTargetUnknownOrders = new Map();
358
375
  this.tabTargets = new Map();
359
376
  this.updateOrder = 0;
360
377
  }
@@ -365,14 +382,16 @@ export class SessionPageState {
365
382
  pinningReason: this.tabPinningReasons.get(sessionName),
366
383
  refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
367
384
  refSnapshotInvalidation: stripRefSnapshotInvalidationOrder(this.refSnapshotInvalidations.get(sessionName)),
385
+ ...(this.tabTargetUnknownOrders.has(sessionName) ? { tabTargetUnknown: true } : {}),
368
386
  tabTarget: this.tabTargets.get(sessionName)?.target,
369
387
  };
370
388
  }
371
389
  applyTabTarget(options) {
372
390
  const current = this.tabTargets.get(options.sessionName);
373
- if (!shouldApplyTabTargetUpdate(current, options.update)) {
391
+ if (!shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update)) {
374
392
  return { ...this.get(options.sessionName), applied: false, stale: true };
375
393
  }
394
+ this.tabTargetUnknownOrders.delete(options.sessionName);
376
395
  this.tabTargets.set(options.sessionName, { order: options.update, target: options.target });
377
396
  return { ...this.get(options.sessionName), applied: true };
378
397
  }
@@ -401,10 +420,22 @@ export class SessionPageState {
401
420
  this.refSnapshotInvalidations.set(options.sessionName, { ...options.invalidation, order: options.update });
402
421
  return { ...this.get(options.sessionName), applied: true };
403
422
  }
423
+ markTabTargetUnknown(options) {
424
+ const current = this.tabTargets.get(options.sessionName);
425
+ if (!shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update))
426
+ return { ...this.get(options.sessionName), applied: false, stale: true };
427
+ this.refSnapshotInvalidations.delete(options.sessionName);
428
+ this.refSnapshots.delete(options.sessionName);
429
+ this.tabPinningReasons.delete(options.sessionName);
430
+ this.tabTargets.delete(options.sessionName);
431
+ this.tabTargetUnknownOrders.set(options.sessionName, options.update);
432
+ return { ...this.get(options.sessionName), applied: true };
433
+ }
404
434
  clearSession(sessionName) {
405
435
  this.refSnapshotInvalidations.delete(sessionName);
406
436
  this.refSnapshots.delete(sessionName);
407
437
  this.tabPinningReasons.delete(sessionName);
438
+ this.tabTargetUnknownOrders.delete(sessionName);
408
439
  this.tabTargets.delete(sessionName);
409
440
  }
410
441
  markPinning(sessionName, reason) {
@@ -5,26 +5,24 @@
5
5
  * Usage: Imported by result/process helpers when they need secure spill files instead of world-readable shared tmp paths.
6
6
  * Invariants/Assumptions: Temp artifacts live under the OS temp directory, each active run uses a dedicated 0700 directory, files are created with exclusive 0600 permissions, session-scoped persisted artifacts stay under the pi session directory, and stale pruning only touches roots with an explicit pi-agent-browser ownership marker.
7
7
  */
8
- import { execFile } from "node:child_process";
9
8
  import { randomBytes } from "node:crypto";
10
9
  import { existsSync, readdirSync, rmSync } from "node:fs";
11
10
  import { chmod, mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
12
11
  import { tmpdir } from "node:os";
13
12
  import { basename, dirname, join, resolve } from "node:path";
14
- import { promisify } from "node:util";
15
13
  import { isRecord, parsePositiveInteger } from "./parsing.js";
14
+ import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process-identity.js";
16
15
  const TEMP_ROOT_PREFIX = "pi-agent-browser-";
17
16
  const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
18
17
  const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
19
- const TEMP_ROOT_MARKER_VERSION = 1;
18
+ const TEMP_ROOT_LEGACY_MARKER_VERSION = 1;
19
+ const TEMP_ROOT_MARKER_VERSION = 2;
20
20
  const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
21
21
  const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
22
22
  const DEFAULT_TEMP_ROOT_MAX_BYTES = 32 * 1_024 * 1_024;
23
23
  const SESSION_ARTIFACT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES";
24
24
  const DEFAULT_SESSION_ARTIFACT_MAX_BYTES = 32 * 1_024 * 1_024;
25
25
  const SESSION_ARTIFACTS_ROOT_DIR_NAME = ".pi-agent-browser-artifacts";
26
- const PROCESS_START_IDENTITY_TIMEOUT_MS = 1_000;
27
- const execFileAsync = promisify(execFile);
28
26
  let sessionTempRootPromise;
29
27
  let exitCleanupRegistered = false;
30
28
  let tempMutationQueue = Promise.resolve();
@@ -48,7 +46,7 @@ function isProtectedTempChildName(value) {
48
46
  function isTempRootOwnershipRecord(value) {
49
47
  if (!isRecord(value))
50
48
  return false;
51
- if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version !== TEMP_ROOT_MARKER_VERSION)
49
+ if (value.kind !== TEMP_ROOT_MARKER_KIND || ![TEMP_ROOT_LEGACY_MARKER_VERSION, TEMP_ROOT_MARKER_VERSION].includes(value.version))
52
50
  return false;
53
51
  if (!isPositiveFiniteNumber(value.createdAtMs))
54
52
  return false;
@@ -153,6 +151,7 @@ async function persistProtectedTempChildren(tempRoot, protectedChildren) {
153
151
  ...ownershipMarker,
154
152
  leaseUpdatedAtMs: Date.now(),
155
153
  protectedChildNames: childNames,
154
+ version: TEMP_ROOT_MARKER_VERSION,
156
155
  });
157
156
  }
158
157
  async function getExistingProtectedChildren(tempRoot, protectedChildren) {
@@ -180,20 +179,7 @@ async function removeTempRootChildrenExcept(tempRoot, protectedChildren) {
180
179
  }));
181
180
  }
182
181
  async function getProcessStartIdentity(pid) {
183
- if (pid === undefined)
184
- return undefined;
185
- if (!Number.isSafeInteger(pid) || pid <= 0)
186
- return undefined;
187
- try {
188
- const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], {
189
- timeout: PROCESS_START_IDENTITY_TIMEOUT_MS,
190
- });
191
- const identity = stdout.trim().replace(/\s+/g, " ");
192
- return identity || undefined;
193
- }
194
- catch {
195
- return undefined;
196
- }
182
+ return pid === undefined ? undefined : await readProcessStartIdentity(pid);
197
183
  }
198
184
  export async function writeSecureTempRootOwnershipMarker(tempRoot, options = {}) {
199
185
  const createdAtMs = options.createdAtMs ?? Date.now();
@@ -219,10 +205,10 @@ async function refreshSecureTempRootLease(tempRoot) {
219
205
  if (currentUid !== undefined && ownershipMarker.ownerUid !== undefined && ownershipMarker.ownerUid !== currentUid)
220
206
  return;
221
207
  const currentProcessStartIdentity = await getProcessStartIdentity(process.pid);
222
- if (ownershipMarker.ownerProcessStartIdentity !== undefined &&
223
- currentProcessStartIdentity !== undefined &&
224
- ownershipMarker.ownerProcessStartIdentity !== currentProcessStartIdentity) {
225
- return;
208
+ if (ownershipMarker.ownerProcessStartIdentity !== undefined && currentProcessStartIdentity !== undefined) {
209
+ const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity);
210
+ if (identitiesMatch === false)
211
+ return;
226
212
  }
227
213
  const refreshedMarker = {
228
214
  ...ownershipMarker,
@@ -230,6 +216,7 @@ async function refreshSecureTempRootLease(tempRoot) {
230
216
  ownerPid: process.pid,
231
217
  ownerProcessStartIdentity: currentProcessStartIdentity ?? ownershipMarker.ownerProcessStartIdentity,
232
218
  ownerUid: currentUid,
219
+ version: TEMP_ROOT_MARKER_VERSION,
233
220
  };
234
221
  await writeTempRootOwnershipMarkerRecord(tempRoot, refreshedMarker);
235
222
  }
@@ -251,7 +238,8 @@ async function getMarkerOwnerLiveness(ownershipMarker) {
251
238
  if (ownershipMarker.ownerProcessStartIdentity === undefined || currentProcessStartIdentity === undefined) {
252
239
  return "unknown";
253
240
  }
254
- return ownershipMarker.ownerProcessStartIdentity === currentProcessStartIdentity ? "alive" : "dead";
241
+ const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity);
242
+ return identitiesMatch === undefined ? "unknown" : identitiesMatch ? "alive" : "dead";
255
243
  }
256
244
  async function pruneStaleTempRoots(currentTempRoot) {
257
245
  const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);