pi-agent-browser-native 0.2.71 → 0.2.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -12
  3. package/dist/extensions/agent-browser/index.js +104 -16
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +124 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +12 -1
  6. package/dist/extensions/agent-browser/lib/electron/cdp.js +2 -2
  7. package/dist/extensions/agent-browser/lib/electron/launch.js +48 -12
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +96 -98
  9. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +88 -2
  10. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +22 -0
  11. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +432 -0
  12. package/dist/extensions/agent-browser/lib/managed-session-restore.js +367 -0
  13. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +367 -0
  14. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +589 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +299 -0
  16. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +35 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +9 -2
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +40 -22
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +15 -6
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +54 -33
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +182 -0
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +625 -429
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +136 -56
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +28 -40
  29. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +102 -19
  30. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +13 -1
  31. package/dist/extensions/agent-browser/lib/playbook.js +10 -9
  32. package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
  33. package/dist/extensions/agent-browser/lib/process.js +270 -34
  34. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
  35. package/dist/extensions/agent-browser/lib/results/categories.js +21 -2
  36. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
  37. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +80 -12
  38. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
  39. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
  40. package/dist/extensions/agent-browser/lib/runtime.js +85 -85
  41. package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
  42. package/dist/extensions/agent-browser/lib/temp.js +13 -25
  43. package/docs/ARCHITECTURE.md +9 -8
  44. package/docs/COMMAND_REFERENCE.md +97 -32
  45. package/docs/ELECTRON.md +10 -10
  46. package/docs/RELEASE.md +3 -2
  47. package/docs/SUPPORT_MATRIX.md +22 -19
  48. package/docs/TOOL_CONTRACT.md +31 -28
  49. package/docs/platform-smoke.md +5 -5
  50. package/package.json +1 -1
  51. package/platform-smoke.config.mjs +3 -1
  52. package/scripts/agent-browser-capability-baseline.mjs +45 -3
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Purpose: Remove wrapper-owned managed session and state capabilities from caller-visible list results.
3
+ * Responsibilities: Recognize managed list rows recursively and preserve the surrounding upstream result shape.
4
+ * Scope: Presentation filtering only; command authorization lives in managed-session-state-policy.ts.
5
+ */
6
+ import { containsManagedSessionRestoreKey, isWrapperManagedSessionName } from "../../managed-session-capabilities.js";
7
+ import { isRecord } from "../../parsing.js";
8
+ function isWrapperManagedSessionListItem(item) {
9
+ if (typeof item === "string")
10
+ return isWrapperManagedSessionName(item);
11
+ if (!isRecord(item))
12
+ return false;
13
+ return ["name", "session", "id"].some((key) => isWrapperManagedSessionName(typeof item[key] === "string" ? item[key] : undefined));
14
+ }
15
+ function containsManagedStateCapability(value) {
16
+ if (typeof value === "string")
17
+ return containsManagedSessionRestoreKey(value);
18
+ if (Array.isArray(value))
19
+ return value.some(containsManagedStateCapability);
20
+ return isRecord(value) && Object.values(value).some(containsManagedStateCapability);
21
+ }
22
+ export function filterCallerOwnedSessionListItems(items) {
23
+ return items.filter((item) => !isWrapperManagedSessionListItem(item));
24
+ }
25
+ export function filterCallerOwnedStateListItems(items) {
26
+ return items.filter((item) => !containsManagedStateCapability(item));
27
+ }
28
+ export function filterManagedSessionListRows(data) {
29
+ if (!isRecord(data) || !Array.isArray(data.sessions))
30
+ return data;
31
+ return { ...data, sessions: filterCallerOwnedSessionListItems(data.sessions) };
32
+ }
33
+ export function filterManagedStateListRows(data) {
34
+ if (!isRecord(data))
35
+ return data;
36
+ return Object.fromEntries(Object.entries(data).map(([key, value]) => [
37
+ key,
38
+ (key === "states" || key === "files") && Array.isArray(value)
39
+ ? filterCallerOwnedStateListItems(value)
40
+ : value,
41
+ ]));
42
+ }
@@ -8,6 +8,7 @@
8
8
  import { buildNextToolAction, withOptionalSessionArgs } from "./next-actions.js";
9
9
  export const AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS = {
10
10
  aboutBlankListTabs: "list-tabs-for-about-blank-recovery",
11
+ connectedSessionGetUrl: "verify-connected-session-url",
11
12
  connectedSessionListTabs: "list-connected-session-tabs",
12
13
  genericTabDriftListTabs: "list-tabs-for-recovery",
13
14
  noActivePageListTabs: "list-tabs-after-no-active-page",
@@ -60,6 +61,12 @@ export function buildRecoveryNextActions(recovery) {
60
61
  const sessionArgs = (args) => withOptionalSessionArgs(recovery.sessionName, args);
61
62
  if (recovery.kind === "connected-session") {
62
63
  return [
64
+ buildNextToolAction({
65
+ args: sessionArgs(["get", "url"]),
66
+ id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionGetUrl,
67
+ reason: "Verify the attached page URL before any page-content inspection.",
68
+ safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target is a local file page or the URL cannot be verified.",
69
+ }),
63
70
  buildNextToolAction({
64
71
  args: sessionArgs(["tab", "list"]),
65
72
  id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionListTabs,
@@ -1,19 +1,21 @@
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
7
+ * and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate, the narrow
8
8
  * OpenAI/ChatGPT 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"]);
@@ -228,7 +230,7 @@ function redactEnvSecretAssignments(text) {
228
230
  });
229
231
  }
230
232
  export function redactSensitiveText(text) {
231
- return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(text)))
233
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(redactManagedSessionRestoreKeys(text))))
232
234
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
233
235
  .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
234
236
  }
@@ -322,11 +324,30 @@ export function getImplicitSessionIdleTimeoutMs(env = process.env) {
322
324
  parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
323
325
  DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS;
324
326
  }
327
+ function countExplicitGlobalFlags(args, targetFlag) {
328
+ return scanUpstreamGlobalFlagOccurrences(args, targetFlag).length;
329
+ }
330
+ function getUnsupportedLeadingIdentityAssignment(args) {
331
+ const commandStartIndex = findCommandStartIndex(args) ?? args.length;
332
+ for (let index = 0; index < commandStartIndex; index += 1) {
333
+ const token = args[index];
334
+ if (token.startsWith("--session="))
335
+ return "--session";
336
+ if (token.startsWith("--namespace="))
337
+ return "--namespace";
338
+ const flag = token.split("=", 1)[0] ?? token;
339
+ if (!token.includes("=") && PREVALIDATED_VALUE_FLAGS.has(flag))
340
+ index += 1;
341
+ }
342
+ return undefined;
343
+ }
325
344
  export function getImplicitSessionCloseTimeoutMs(env = process.env) {
326
345
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
327
346
  }
328
347
  export function resolveManagedSessionState(options) {
329
- const { command, managedSessionName, managedSessionNamespace, priorActive, priorNamespace, priorSessionName, succeeded } = options;
348
+ const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
349
+ const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
350
+ const priorNamespace = canonicalizeAgentBrowserNamespace(options.priorNamespace);
330
351
  if (!managedSessionName) {
331
352
  return { active: priorActive, ...(priorNamespace ? { namespace: priorNamespace } : {}), sessionName: priorSessionName };
332
353
  }
@@ -391,6 +412,7 @@ function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionNam
391
412
  return [...closedSessionNames];
392
413
  }
393
414
  export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName) {
415
+ const restoreDisabledIdentities = new Map();
394
416
  let restoredState = {
395
417
  active: false,
396
418
  sessionName: fallbackSessionName,
@@ -400,6 +422,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
400
422
  let freshSessionOrdinal = 0;
401
423
  const freshSessionRanks = new Map();
402
424
  const applyManagedClose = (sessionName, namespace) => {
425
+ namespace = canonicalizeAgentBrowserNamespace(namespace);
403
426
  const restoreRank = getManagedSessionRestoreRank({
404
427
  fallbackSessionName,
405
428
  freshSessionRanks,
@@ -431,7 +454,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
431
454
  }
432
455
  const explicitSessionName = extractExplicitSessionName(args);
433
456
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
434
- const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
457
+ const namespace = canonicalizeAgentBrowserNamespace(typeof details.namespace === "string" ? details.namespace : undefined);
435
458
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
436
459
  const usedImplicitSession = details.usedImplicitSession === true;
437
460
  const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
@@ -447,6 +470,11 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
447
470
  const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
448
471
  ? restorableDetailSessionName
449
472
  : undefined;
473
+ // Sticky restore policy is session-identity state and must apply even for explicit
474
+ // `--session <current-managed>` rows that are not used for managed-session lifecycle replay.
475
+ if (details.managedSessionRestoreDisabled === true && typeof sessionName === "string") {
476
+ restoreDisabledIdentities.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { namespace, sessionName });
477
+ }
450
478
  const managedSessionName = !explicitSessionName &&
451
479
  restorableDetailSessionName &&
452
480
  (usedImplicitSession || sessionMode === "fresh")
@@ -472,8 +500,10 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
472
500
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
473
501
  const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
474
502
  if (commandClosesSession) {
475
- if (succeeded)
503
+ if (succeeded) {
504
+ restoreDisabledIdentities.delete(getAgentBrowserSessionIdentityKey(managedSessionName, namespace));
476
505
  applyManagedClose(managedSessionName, namespace);
506
+ }
477
507
  continue;
478
508
  }
479
509
  const staleCompletion = succeeded && restoreRank < activeRestoreRank;
@@ -498,6 +528,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
498
528
  ...restoredState,
499
529
  ...(closedSessionName ? { closedSessionName } : {}),
500
530
  freshSessionOrdinal,
531
+ managedSessionRestoreDisabledIdentities: [...restoreDisabledIdentities.values()],
501
532
  };
502
533
  }
503
534
  export function createEphemeralSessionSeed() {
@@ -515,13 +546,13 @@ export function createImplicitSessionName(sessionId, cwd, ephemeralSeed) {
515
546
  const cwdHash = createCwdHash(cwd);
516
547
  const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
517
548
  if (stableSessionId && stableSessionId.length > 0) {
518
- return `piab-${slug}-${stableSessionId}-${cwdHash}`;
549
+ return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${stableSessionId}-${cwdHash}`;
519
550
  }
520
551
  const digest = createHash("sha256")
521
552
  .update(`ephemeral:${cwd}:${ephemeralSeed}`)
522
553
  .digest("hex")
523
554
  .slice(0, SESSION_NAME_SESSION_ID_LENGTH);
524
- return `piab-${slug}-${digest}-${cwdHash}`;
555
+ return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${digest}-${cwdHash}`;
525
556
  }
526
557
  export function createFreshSessionName(baseSessionName, ephemeralSeed, ordinal) {
527
558
  const suffix = createHash("sha256")
@@ -623,21 +654,6 @@ function getFlagValue(args, flag) {
623
654
  }
624
655
  return undefined;
625
656
  }
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
657
  function normalizeComparableUrl(url) {
642
658
  const normalizedUrl = url.trim();
643
659
  if (normalizedUrl.length === 0) {
@@ -716,57 +732,13 @@ function getCompatibilityWorkaround(args, commandInfo) {
716
732
  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
733
  };
718
734
  }
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
735
  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);
736
+ const namespaceTokenIndexes = new Set();
737
+ for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--namespace")) {
738
+ namespaceTokenIndexes.add(occurrence.index);
739
+ namespaceTokenIndexes.add(occurrence.index + 1);
752
740
  }
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
- });
741
+ return args.filter((_token, index) => !namespaceTokenIndexes.has(index));
770
742
  }
771
743
  export function getStartupScopedFlags(args) {
772
744
  return LAUNCH_SCOPED_FLAG_DEFINITIONS
@@ -775,14 +747,27 @@ export function getStartupScopedFlags(args) {
775
747
  }
776
748
  export function buildExecutionPlan(args, options) {
777
749
  const invalidValueFlag = getInvalidValueFlagDetails(args);
750
+ const unsupportedIdentityAssignment = getUnsupportedLeadingIdentityAssignment(args);
751
+ const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
778
752
  const explicitNamespace = extractExplicitNamespace(args);
779
- const startupScopedFlags = getStartupScopedFlags(args).filter((flag) => !(flag === "--namespace" && explicitNamespace === options.managedSessionNamespace));
753
+ const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
754
+ const startupScopedFlags = getStartupScopedFlags(args).filter((flag) => !(flag === "--namespace" && explicitNamespacePresent && explicitNamespace === managedSessionNamespace));
780
755
  const plainTextInspection = isPlainTextInspectionArgs(args);
781
756
  const argvDescriptor = parseArgvDescriptor(args);
782
757
  const commandInfo = argvDescriptor.commandInfo;
783
758
  const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
784
759
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
785
760
  let namespace = explicitNamespace;
761
+ if (plainTextInspection) {
762
+ return {
763
+ commandInfo,
764
+ effectiveArgs,
765
+ namespace,
766
+ plainTextInspection,
767
+ startupScopedFlags,
768
+ usedImplicitSession: false,
769
+ };
770
+ }
786
771
  if (invalidValueFlag) {
787
772
  return {
788
773
  commandInfo: {},
@@ -794,22 +779,37 @@ export function buildExecutionPlan(args, options) {
794
779
  validationError: formatInvalidValueFlagError(invalidValueFlag),
795
780
  };
796
781
  }
797
- if (plainTextInspection) {
782
+ if (unsupportedIdentityAssignment) {
798
783
  return {
799
- commandInfo,
784
+ commandInfo: {},
800
785
  effectiveArgs,
801
- namespace,
802
- plainTextInspection,
803
- startupScopedFlags,
786
+ plainTextInspection: false,
787
+ startupScopedFlags: [],
804
788
  usedImplicitSession: false,
789
+ validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser 0.33.2. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
790
+ };
791
+ }
792
+ for (const flag of ["--session", "--namespace"]) {
793
+ if (countExplicitGlobalFlags(args, flag) <= 1)
794
+ continue;
795
+ return {
796
+ commandInfo: {},
797
+ effectiveArgs,
798
+ plainTextInspection: false,
799
+ startupScopedFlags: [],
800
+ usedImplicitSession: false,
801
+ 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
802
  };
806
803
  }
807
804
  const explicitSessionName = extractExplicitSessionName(args);
805
+ if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
806
+ namespace = resolveAgentBrowserNamespace(args, process.env.AGENT_BROWSER_NAMESPACE);
807
+ }
808
808
  const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
809
809
  let argsToAppend = args;
810
810
  const compatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
811
- if (explicitSessionName && explicitNamespace) {
812
- effectiveArgs.push("--namespace", explicitNamespace);
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,7 +846,7 @@ 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;
@@ -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(() => []);