pi-agent-browser-native 0.3.0 → 0.5.0

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 (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -1,13 +1,13 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { chmodSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { extractCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
3
+ import { extractUpstreamCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
5
4
  import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, extractRequestedRestoreKey, getAgentBrowserSessionIdentityKey, isUpstreamEnvFlagEnabled, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
6
5
  import { hasLaunchScopedFlagToken, MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS, MANAGED_RESTORE_INCOMPATIBLE_ENVS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS, } from "./launch-scoped-flags.js";
7
- import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
6
+ import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
8
7
  import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
8
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
9
9
  import { writeSecureTempFile } from "./temp.js";
10
- export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure } from "./managed-session-storage.js";
10
+ export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope } from "./managed-session-storage.js";
11
11
  export { pruneOwnedManagedSessionRestoreSnapshots } from "./managed-session-snapshots.js";
12
12
  const AGENT_BROWSER_CONFIG_ENV = "AGENT_BROWSER_CONFIG";
13
13
  const AGENT_BROWSER_RESTORE_ENV = "AGENT_BROWSER_RESTORE";
@@ -61,7 +61,7 @@ export class ManagedSessionRestoreState {
61
61
  if (sessionName)
62
62
  this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), restoreKey);
63
63
  }
64
- replace(identities, options = {}) {
64
+ replace(identities = [], options = {}) {
65
65
  if (!options.preserveDaemonRestoreKeys)
66
66
  this.#daemonRestoreKeys.clear();
67
67
  this.#disabled.clear();
@@ -73,6 +73,9 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
73
73
  export async function withOwnedManagedSessionContext(context, run) {
74
74
  return await ownedManagedSessionStorage.run(context, run);
75
75
  }
76
+ export function getOwnedManagedSessionRestoreKey() {
77
+ return ownedManagedSessionStorage.getStore()?.restoreKey;
78
+ }
76
79
  export function resolveOwnedManagedSessionContext(options) {
77
80
  const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
78
81
  const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
@@ -96,33 +99,18 @@ function ownedContextMatches(sessionName, namespace) {
96
99
  export function isOwnedManagedSessionTarget(args) {
97
100
  return ownedContextMatches(extractExplicitSessionName(args), extractExplicitNamespace(args)) !== undefined;
98
101
  }
99
- function pathExistsOrIsUnreadable(path) {
100
- try {
101
- lstatSync(path);
102
- return true;
103
- }
104
- catch (error) {
105
- return error.code !== "ENOENT";
106
- }
107
- }
108
102
  function hasExplicitConfigArg(args) {
109
103
  return scanUpstreamGlobalFlagOccurrences(args, "--config").length > 0;
110
104
  }
111
105
  function closesBrowserSession(args) {
112
106
  return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
113
107
  }
114
- export function agentBrowserConfigIsPresent(cwd, parentEnv = process.env, args = [], platform = process.platform) {
115
- if (hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV))
116
- return true;
117
- const paths = [join(cwd, "agent-browser.json")];
118
- const home = resolveManagedSessionRestoreHome(parentEnv, platform);
119
- if (home)
120
- paths.push(join(home, ".agent-browser", "config.json"));
121
- return paths.some(pathExistsOrIsUnreadable);
108
+ export function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
109
+ return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
122
110
  }
123
- /** Any upstream config disables automatic restore; content inspection would add parser and resource-exhaustion gaps. */
124
- export function agentBrowserConfigBlocksManagedRestore(cwd, parentEnv = process.env, args = [], platform = process.platform) {
125
- return !resolveManagedSessionRestoreHome(parentEnv, platform) || agentBrowserConfigIsPresent(cwd, parentEnv, args, platform);
111
+ /** Explicit config overrides disable restore; passive files are ignored because every browser-backed subprocess pins a protected empty config. */
112
+ export function agentBrowserConfigBlocksManagedRestore(_cwd, parentEnv = getAgentBrowserProcessEnvironment(), args = [], platform = process.platform) {
113
+ return !resolveManagedSessionRestoreHome(parentEnv, platform) || agentBrowserExplicitConfigIsPresent(parentEnv, args);
126
114
  }
127
115
  function omitWrapperInjectedUserAgent(args, enabled) {
128
116
  if (!enabled)
@@ -131,7 +119,7 @@ function omitWrapperInjectedUserAgent(args, enabled) {
131
119
  return index < 0 ? args : [...args.slice(0, index), ...args.slice(index + 2)];
132
120
  }
133
121
  function batchHasManagedSessionRestoreConflict(args, stdin) {
134
- const [command, ...commandArgs] = extractCommandTokens(args);
122
+ const [command, ...commandArgs] = extractUpstreamCommandTokens(args);
135
123
  if (command !== "batch")
136
124
  return false;
137
125
  if (commandArgs.some((token) => token !== "--bail"))
@@ -142,7 +130,7 @@ function batchHasManagedSessionRestoreConflict(args, stdin) {
142
130
  return parsed.steps.some((step) => ["connect", "batch"].includes(parseCommandInfo(step).command ?? ""));
143
131
  }
144
132
  function hasManagedSessionRestoreLaunchConflict(options) {
145
- const parentEnv = options.parentEnv ?? process.env;
133
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
146
134
  const effectiveEnv = { ...parentEnv, ...options.env };
147
135
  const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
148
136
  if (MANAGED_RESTORE_INCOMPATIBLE_ENVS.some((name) => hasUpstreamEnvValue(effectiveEnv, name)))
@@ -156,7 +144,7 @@ function hasManagedSessionRestoreLaunchConflict(options) {
156
144
  return hasExplicitConfigArg(args) || hasUpstreamEnvValue({ ...parentEnv, ...options.env }, AGENT_BROWSER_CONFIG_ENV);
157
145
  }
158
146
  function managedSessionRestoreOptedOut(options) {
159
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
147
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
160
148
  return isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]);
161
149
  }
162
150
  function isManagedSessionRestoreIncompatible(options) {
@@ -164,7 +152,7 @@ function isManagedSessionRestoreIncompatible(options) {
164
152
  return true;
165
153
  if (managedSessionRestoreOptedOut(options))
166
154
  return false;
167
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
155
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
168
156
  const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
169
157
  if (options.cwd && !hasManagedSessionRestoreProjectIdentity(options.cwd))
170
158
  return true;
@@ -173,7 +161,7 @@ function isManagedSessionRestoreIncompatible(options) {
173
161
  return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, extractExplicitNamespace(args));
174
162
  }
175
163
  function resolveManagedSessionRestorePolicy(options) {
176
- const parentEnv = options.parentEnv ?? process.env;
164
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
177
165
  const sessionName = extractExplicitSessionName(options.args);
178
166
  const namespace = extractExplicitNamespace(options.args);
179
167
  const ownedContext = ownedContextMatches(sessionName, namespace);
@@ -204,7 +192,7 @@ export function getOwnedManagedSessionCompatibilityEnv(options) {
204
192
  const { owned, ownedContext } = resolveManagedSessionRestorePolicy(options);
205
193
  if (!owned || !ownedContext)
206
194
  return {};
207
- const parentEnv = options.parentEnv ?? process.env;
195
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
208
196
  const callEnv = options.env ?? {};
209
197
  const explicitRawInterval = Object.hasOwn(callEnv, "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS")
210
198
  ? callEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS
@@ -295,7 +283,7 @@ export function getManagedSessionRestoreProtectedEnv(options, restoreEnv) {
295
283
  return {};
296
284
  if (ownedContext?.protectedStorageEnv)
297
285
  return { ...ownedContext.protectedStorageEnv };
298
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
286
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
299
287
  return getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv);
300
288
  }
301
289
  export function validateManagedSessionRestoreContextForSpawn(options) {
@@ -303,7 +291,7 @@ export function validateManagedSessionRestoreContextForSpawn(options) {
303
291
  if (closesBrowserSession(options.args) || ownedContext?.restoreDecision !== "enabled")
304
292
  return true;
305
293
  const ownedCwd = ownedContext.cwd ?? options.cwd;
306
- if (!ownedContext.restoreKey || createManagedSessionRestoreKey(ownedCwd) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
294
+ if (!ownedContext.restoreKey || !ownedContext.restoreScope || createManagedSessionRestoreKey(ownedCwd, ownedContext.restoreScope) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
307
295
  return false;
308
296
  const effectiveEnv = { ...parentEnv, ...options.env };
309
297
  if (isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]))
@@ -330,7 +318,7 @@ export function getManagedSessionRestoreEnv(options) {
330
318
  return {};
331
319
  if (restoreState.isDisabled(sessionName, namespace) || !sessionName)
332
320
  return {};
333
- return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd) };
321
+ return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd, getManagedSessionRestoreScope(sessionName)) };
334
322
  }
335
323
  /** Commit sticky suppression only after an owned-context subprocess has actually started. */
336
324
  export function commitManagedSessionRestoreSuppression(options) {
@@ -375,8 +363,9 @@ export function buildOwnedManagedSessionRestoreContext(options) {
375
363
  const projectIdentityAvailable = !optedOut && hasManagedSessionRestoreProjectIdentity(ownedCwd);
376
364
  const incompatible = !optedOut && isManagedSessionRestoreIncompatible(policyOptions);
377
365
  const enabled = !optedOut && !incompatible;
378
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
379
- const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd) : undefined;
366
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
367
+ const restoreScope = getManagedSessionRestoreScope(owned.sessionName);
368
+ const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd, restoreScope) : undefined;
380
369
  return {
381
370
  ...owned,
382
371
  compatibilityUserAgent: options.compatibilityUserAgent,
@@ -386,6 +375,7 @@ export function buildOwnedManagedSessionRestoreContext(options) {
386
375
  protectedStorageEnv: enabled ? getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv) : undefined,
387
376
  restoreDecision: optedOut ? "opted-out" : incompatible ? "incompatible" : "enabled",
388
377
  restoreKey,
378
+ restoreScope,
389
379
  restoreSuppressed: optedOut || incompatible,
390
380
  };
391
381
  }
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join } from "node:path";
4
- import { createManagedSessionRestoreKey, directoryContainsSymlink, ensureManagedSessionRestoreStorageIsSecure, ensureOwnerOnlyDirectory, getManagedRestoreSessionsDirectory, hasManagedSessionRestoreProjectIdentity, isManagedSessionRestoreKey, resolveManagedSessionRestoreCheckoutRoot, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
4
+ import { createManagedSessionRestoreKey, directoryContainsSymlink, ensureManagedSessionRestoreStorageIsSecure, hasManagedSessionRestoreProjectIdentity, ensureOwnerOnlyDirectory, getManagedRestoreSessionsDirectory, isManagedSessionRestoreKey, resolveManagedSessionRestoreCheckoutRoot, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
5
5
  const OWNED_RESTORE_SNAPSHOT_FAMILIES_TO_KEEP = 2;
6
6
  const OWNED_RESTORE_SNAPSHOT_MAX_RECORDS = 256;
7
7
  const OWNED_RESTORE_SNAPSHOT_RECORD_MAX_BYTES = 16 * 1_024;
@@ -344,9 +344,7 @@ export function pruneOwnedManagedSessionRestoreSnapshots(options) {
344
344
  if (!changed)
345
345
  break;
346
346
  }
347
- const protectedRestoreKey = hasManagedSessionRestoreProjectIdentity(options.cwd)
348
- ? createManagedSessionRestoreKey(options.cwd)
349
- : restoreKey;
347
+ const protectedRestoreKey = restoreKey;
350
348
  if (!lineage)
351
349
  return removed;
352
350
  removed += pruneExpiredOtherRestoreKeys({
@@ -4,18 +4,20 @@ import { fileURLToPath } from "node:url";
4
4
  import { parseArgvDescriptor } from "./argv-descriptor.js";
5
5
  import { needsManagedSession } from "./command-policy.js";
6
6
  import { getScreenshotPathTokenIndex } from "./orchestration/browser-run/artifact-paths.js";
7
- import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
7
+ import { getUpstreamEffectiveBatchSteps, parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
8
8
  import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
9
9
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, extractExplicitSessionName, isUpstreamEnvFlagEnabled, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
10
10
  import { extractManagedSessionRestoreKeys, isWrapperManagedSessionName } from "./managed-session-capabilities.js";
11
- import { agentBrowserConfigIsPresent } from "./managed-session-restore.js";
12
- import { createManagedSessionRestoreKey, hasManagedSessionRestoreProjectIdentity } from "./managed-session-storage.js";
11
+ import { agentBrowserExplicitConfigIsPresent } from "./managed-session-restore.js";
12
+ import { hasManagedSessionRestoreProjectIdentity } from "./managed-session-storage.js";
13
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
13
14
  const BLOCKED_GLOBAL_STATE_MESSAGE = "This operation could read or modify wrapper-owned browser state outside the current checkout. Use a caller-owned state name or path instead.";
14
15
  const BLOCKED_MANAGED_BROWSER_FILE_MESSAGE = "Browser access to local .agent-browser storage is blocked because state files can contain authenticated cookies and storage. Use guarded state commands instead.";
15
16
  const BLOCKED_MANAGED_SESSION_MESSAGE = "This session name is reserved for a browser managed by this extension instance. Use the current managed session or a caller-owned session name instead.";
16
17
  const FILE_ACCESS_FLAG_MESSAGE = "Browser file-access enablement is blocked because a local page could read and exfiltrate authenticated .agent-browser state.";
17
- const UPSTREAM_CONFIG_MESSAGE = "Upstream agent-browser config is blocked for browser-backed native calls because it can load protected state, profiles, extensions, or file-access settings. Remove the config and pass safe settings explicitly.";
18
+ const UPSTREAM_CONFIG_MESSAGE = "Explicit upstream agent-browser config from --config or AGENT_BROWSER_CONFIG is blocked for browser-backed native calls because it can load protected state, profiles, extensions, or file-access settings. Remove that override and pass safe settings explicitly. Passive agent-browser.json files are ignored through the wrapper's protected empty config.";
18
19
  const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly to a safe URL before page-content inspection.";
20
+ const BATCH_UNVERIFIED_PAGE_MESSAGE = `${UNVERIFIED_PAGE_MESSAGE} In a batch, put get url after the transition before later content steps, or split the batch at that boundary.`;
19
21
  const UNSAFE_BATCH_ARGUMENT_MESSAGE = "Batch command arguments could not be safely inspected. Use batch stdin JSON command arrays instead.";
20
22
  const NESTED_BATCH_ARGUMENT_MESSAGE = "Nested batch commands are blocked by the wrapper's page-state safety policy. Flatten the batch steps instead.";
21
23
  const NON_BAIL_BATCH_NAVIGATION_MESSAGE = "Batches that navigate before page-content access must use exact batch --bail so a failed navigation cannot expose the prior page.";
@@ -59,7 +61,8 @@ function hasAgentBrowserPathComponent(value) {
59
61
  function getFileUrlPath(value) {
60
62
  if (!value)
61
63
  return undefined;
62
- const nestedFileUrl = /^(?:(?:blob|filesystem|view-source):)*(file:.*)$/i.exec(value)?.[1];
64
+ const trimmedValue = value.replaceAll(/[\t\n\r]/g, "").trim();
65
+ const nestedFileUrl = /^(?:(?:blob|filesystem|view-source):)*(file:.*)$/i.exec(trimmedValue)?.[1];
63
66
  if (!nestedFileUrl)
64
67
  return undefined;
65
68
  try {
@@ -70,7 +73,7 @@ function getFileUrlPath(value) {
70
73
  return decodeUrlComponent(new URL(nestedFileUrl).pathname);
71
74
  }
72
75
  catch {
73
- return value;
76
+ return trimmedValue;
74
77
  }
75
78
  }
76
79
  }
@@ -148,7 +151,7 @@ function getPositionalOperands(commandTokens) {
148
151
  }
149
152
  function getExplicitNavigationTarget(args) {
150
153
  const descriptor = parseArgvDescriptor(args);
151
- const positionals = getPositionalOperands(descriptor.commandTokens);
154
+ const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
152
155
  if (EXPLICIT_NAVIGATION_COMMANDS.has(descriptor.commandInfo.command ?? ""))
153
156
  return positionals[0];
154
157
  if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
@@ -179,11 +182,11 @@ function getResultingExplicitNavigationTarget(args, currentPageUrl) {
179
182
  function getBrowserContentUrlOperands(args) {
180
183
  const descriptor = parseArgvDescriptor(args);
181
184
  const command = descriptor.commandInfo.command;
182
- const positionals = getPositionalOperands(descriptor.commandTokens);
185
+ const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
183
186
  if (["a11y", "read", "vitals", "web-vitals"].includes(command ?? ""))
184
187
  return positionals.slice(0, 1);
185
188
  if (command === "auth" && descriptor.commandInfo.subcommand === "login")
186
- return getFlagValues(descriptor.commandTokens, "--url", true);
189
+ return getFlagValues(descriptor.upstreamCommandTokens, "--url", true);
187
190
  if (command === "diff" && descriptor.commandInfo.subcommand === "url")
188
191
  return positionals.slice(1, 3);
189
192
  if (command === "record" && ["start", "restart"].includes(descriptor.commandInfo.subcommand ?? ""))
@@ -221,7 +224,7 @@ function getBrowserFileOperands(args, env) {
221
224
  const descriptor = parseArgvDescriptor(args);
222
225
  const command = descriptor.commandInfo.command;
223
226
  const subcommand = descriptor.commandInfo.subcommand;
224
- const positionals = getPositionalOperands(descriptor.commandTokens);
227
+ const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
225
228
  const values = [
226
229
  getExplicitNavigationTarget(args),
227
230
  ...getBrowserContentUrlOperands(args),
@@ -238,23 +241,23 @@ function getBrowserFileOperands(args, env) {
238
241
  if (command === "pdf")
239
242
  values.push(positionals[0]);
240
243
  if (command === "screenshot") {
241
- const pathIndex = getScreenshotPathTokenIndex(descriptor.commandTokens);
242
- values.push(pathIndex === undefined ? undefined : descriptor.commandTokens[pathIndex]);
244
+ const pathIndex = getScreenshotPathTokenIndex(descriptor.upstreamCommandTokens);
245
+ values.push(pathIndex === undefined ? undefined : descriptor.upstreamCommandTokens[pathIndex]);
243
246
  }
244
247
  if (["profiler", "trace"].includes(command ?? "") && subcommand === "stop")
245
248
  values.push(...positionals.slice(1));
246
249
  if (command === "network" && subcommand === "har")
247
250
  values.push(...positionals.slice(2));
248
251
  if (command === "wait")
249
- values.push(...getFlagValues(descriptor.commandTokens, "--download", true), ...getFlagValues(descriptor.commandTokens, "-d", true));
252
+ values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--download", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-d", true));
250
253
  if (command === "cookies" && subcommand === "set")
251
- values.push(...getFlagValues(descriptor.commandTokens, "--curl", true));
254
+ values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--curl", true));
252
255
  if (command === "state" && ["load", "rename", "save", "show"].includes(subcommand ?? ""))
253
256
  values.push(...positionals.slice(1));
254
257
  if (command === "diff" && subcommand === "url")
255
258
  values.push(...positionals.slice(1));
256
259
  if (command === "diff")
257
- values.push(...getFlagValues(descriptor.commandTokens, "--baseline", true), ...getFlagValues(descriptor.commandTokens, "--output", true), ...getFlagValues(descriptor.commandTokens, "-o", true));
260
+ values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--baseline", true), ...getFlagValues(descriptor.upstreamCommandTokens, "--output", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-o", true));
258
261
  if (command === "record" && ["start", "restart"].includes(subcommand ?? ""))
259
262
  values.push(...positionals.slice(1));
260
263
  return values.filter((value) => typeof value === "string" && value.length > 0);
@@ -306,13 +309,21 @@ function getManagedBrowserFileAccessValidationError(options) {
306
309
  return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
307
310
  return undefined;
308
311
  }
312
+ // Deliberately unions raw argv steps AND parseable stdin steps even though
313
+ // upstream executes argv steps exclusively when any exist: this pre-spawn
314
+ // validator must stay a superset of anything upstream could run, so it fails
315
+ // closed on unsafe content in either source. Stdin parse failures are fatal
316
+ // only when upstream would actually read stdin (no argv steps); ignored
317
+ // unparseable stdin cannot execute and must not reject a valid raw-argv call.
318
+ // The upstream-exact selection lives in getUpstreamEffectiveBatchSteps
319
+ // (batch-stdin.ts) for guard/folding paths.
309
320
  function getBatchCommandSteps(args, stdin) {
310
321
  const descriptor = parseArgvDescriptor(args);
311
322
  if (descriptor.commandInfo.command !== "batch")
312
323
  return { steps: [] };
313
324
  const steps = [];
314
- for (const command of descriptor.commandTokens.slice(1)) {
315
- if (command === "--bail" || command.startsWith("--bail="))
325
+ for (const command of descriptor.upstreamCommandTokens.slice(1)) {
326
+ if (command === "--bail")
316
327
  continue;
317
328
  if (decodeUrlComponent(command).toLowerCase().includes(".agent-browser"))
318
329
  return { error: BLOCKED_MANAGED_BROWSER_FILE_MESSAGE, steps: [] };
@@ -324,7 +335,7 @@ function getBatchCommandSteps(args, stdin) {
324
335
  steps.push(parsed.step);
325
336
  }
326
337
  const parsedStdin = parseUserBatchStdin(stdin);
327
- if (parsedStdin.error)
338
+ if (parsedStdin.error && steps.length === 0)
328
339
  return { error: parsedStdin.error, steps: [] };
329
340
  for (const step of parsedStdin.steps ?? []) {
330
341
  if (parseArgvDescriptor(step).commandInfo.command === "batch")
@@ -333,9 +344,16 @@ function getBatchCommandSteps(args, stdin) {
333
344
  }
334
345
  return { steps };
335
346
  }
347
+ export function invocationMayNavigateToLocalFile(args, stdin) {
348
+ if (isFileUrl(getResultingExplicitNavigationTarget(args)))
349
+ return true;
350
+ const descriptor = parseArgvDescriptor(args);
351
+ return getUpstreamEffectiveBatchSteps(descriptor.upstreamCommandTokens, stdin)
352
+ .some((step) => isFileUrl(getResultingExplicitNavigationTarget(step)));
353
+ }
336
354
  function batchBailsOnFirstError(args) {
337
355
  const descriptor = parseArgvDescriptor(args);
338
- return descriptor.commandInfo.command === "batch" && descriptor.commandTokens.slice(1).includes("--bail");
356
+ return descriptor.commandInfo.command === "batch" && descriptor.upstreamCommandTokens.slice(1).includes("--bail");
339
357
  }
340
358
  function commandMayChangePageTarget(args, trustedBatchTabSelection) {
341
359
  const descriptor = parseArgvDescriptor(args);
@@ -419,11 +437,11 @@ export function getCallerOwnedSessionLivePageVerificationRequirement(options) {
419
437
  trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
420
438
  trustedPinnedEmptyConfig: true,
421
439
  });
422
- return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
440
+ return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
423
441
  ? UNVERIFIED_PAGE_MESSAGE
424
442
  : undefined;
425
443
  }
426
- export function getManagedSessionTargetAccessValidationError(args, ownedManagedSession, env = process.env) {
444
+ export function getManagedSessionTargetAccessValidationError(args, ownedManagedSession, env = getAgentBrowserProcessEnvironment()) {
427
445
  const sessionName = extractExplicitSessionName(args) ?? env.AGENT_BROWSER_SESSION;
428
446
  return sessionName && isWrapperManagedSessionName(sessionName) && !ownedManagedSession
429
447
  ? BLOCKED_MANAGED_SESSION_MESSAGE
@@ -465,7 +483,7 @@ function getReferencedValues(args, cwd, env) {
465
483
  env.AGENT_BROWSER_STATE,
466
484
  ].filter((value) => typeof value === "string" && value.length > 0);
467
485
  if (descriptor.commandInfo.command === "state" && ["clear", "load", "rename", "save", "show"].includes(descriptor.commandInfo.subcommand ?? "")) {
468
- values.push(...descriptor.commandTokens.slice(2));
486
+ values.push(...descriptor.upstreamCommandTokens.slice(2));
469
487
  }
470
488
  for (const value of [...values]) {
471
489
  const realPath = resolveExistingPath(cwd, value);
@@ -475,13 +493,13 @@ function getReferencedValues(args, cwd, env) {
475
493
  return values;
476
494
  }
477
495
  export function getManagedSessionStateAccessValidationError(options) {
478
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
496
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
479
497
  const descriptor = parseArgvDescriptor(options.args);
480
498
  const command = descriptor.commandInfo.command;
481
499
  const subcommand = descriptor.commandInfo.subcommand;
482
500
  if (["close", "exit", "quit"].includes(command ?? ""))
483
501
  return undefined;
484
- if (!options.trustedPinnedEmptyConfig && needsManagedSession(descriptor) && agentBrowserConfigIsPresent(options.cwd, effectiveEnv, options.args))
502
+ if (!options.trustedPinnedEmptyConfig && needsManagedSession(descriptor) && agentBrowserExplicitConfigIsPresent(effectiveEnv, options.args))
485
503
  return UPSTREAM_CONFIG_MESSAGE;
486
504
  if (command === "batch") {
487
505
  const batch = getBatchCommandSteps(options.args, options.stdin);
@@ -520,7 +538,7 @@ export function getManagedSessionStateAccessValidationError(options) {
520
538
  }
521
539
  }
522
540
  if (directError)
523
- return directError;
541
+ return directError === UNVERIFIED_PAGE_MESSAGE ? BATCH_UNVERIFIED_PAGE_MESSAGE : directError;
524
542
  if (failedNavigationHazard)
525
543
  return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
526
544
  const mayChangePageTarget = commandMayChangePageTarget(step, trustedBatchTabSelection);
@@ -562,8 +580,8 @@ export function getManagedSessionStateAccessValidationError(options) {
562
580
  if (command === "state" && subcommand === "clean")
563
581
  return BLOCKED_GLOBAL_STATE_MESSAGE;
564
582
  if (command === "state" && subcommand === "clear") {
565
- const target = descriptor.commandTokens.slice(2).find((token) => !token.startsWith("-"));
566
- if (!target || descriptor.commandTokens.includes("--all") || descriptor.commandTokens.includes("-a") || isWrapperManagedSessionName(target)) {
583
+ const target = descriptor.upstreamCommandTokens.slice(2).find((token) => !token.startsWith("-"));
584
+ if (!target || descriptor.upstreamCommandTokens.includes("--all") || descriptor.upstreamCommandTokens.includes("-a") || isWrapperManagedSessionName(target)) {
567
585
  return BLOCKED_GLOBAL_STATE_MESSAGE;
568
586
  }
569
587
  }
@@ -576,8 +594,8 @@ export function getManagedSessionStateAccessValidationError(options) {
576
594
  return undefined;
577
595
  if (command === "state" && ["rename", "save"].includes(subcommand ?? ""))
578
596
  return BLOCKED_GLOBAL_STATE_MESSAGE;
579
- if (!hasManagedSessionRestoreProjectIdentity(options.cwd))
597
+ if (!hasManagedSessionRestoreProjectIdentity(options.cwd) || !options.managedSessionRestoreKey)
580
598
  return BLOCKED_GLOBAL_STATE_MESSAGE;
581
- const currentKey = createManagedSessionRestoreKey(options.cwd).toLowerCase();
599
+ const currentKey = options.managedSessionRestoreKey.toLowerCase();
582
600
  return referencedKeys.every((key) => key === currentKey) ? undefined : BLOCKED_GLOBAL_STATE_MESSAGE;
583
601
  }
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, parse, resolve, win32 } from "node:path";
5
5
  import { canonicalizeAgentBrowserNamespace } from "./argv-grammar.js";
6
6
  export { isManagedSessionRestoreKey } from "./managed-session-capabilities.js";
7
7
  const MANAGED_SESSION_NAME_PREFIX = "piab-r2-";
8
+ const MANAGED_SESSION_FRESH_SUFFIX_PATTERN = /-fresh-[a-f\d]{10}$/i;
8
9
  const MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH = 32;
9
10
  const PROJECT_GENERATION_MARKER_NAME = "pi-agent-browser-project-generation-v1.json";
10
11
  const PROJECT_GENERATION_MARKER_MAX_BYTES = 1_024;
@@ -15,7 +16,7 @@ function isAbsoluteHome(path, platform) {
15
16
  function currentUid() {
16
17
  return typeof process.getuid === "function" ? process.getuid() : undefined;
17
18
  }
18
- function isTrustedPosixDirectory(path, requireCurrentOwner) {
19
+ function isTrustedPosixDirectory(path, requireCurrentOwner, platform = process.platform) {
19
20
  const uid = currentUid();
20
21
  if (uid === undefined)
21
22
  return false;
@@ -32,11 +33,13 @@ function isTrustedPosixDirectory(path, requireCurrentOwner) {
32
33
  }
33
34
  if (entry.isSymbolicLink() || !entry.isDirectory())
34
35
  return false;
35
- if (entry.uid !== 0 && entry.uid !== uid)
36
+ const androidSystemAncestor = platform === "android" && (cursor === "/data" || cursor === "/data/data") && entry.uid === 1000 && (entry.mode & 0o002) === 0;
37
+ const androidAppDirectory = platform === "android" && entry.uid === uid && entry.gid === uid && (entry.mode & 0o002) === 0;
38
+ if (!androidSystemAncestor && entry.uid !== 0 && entry.uid !== uid)
36
39
  return false;
37
40
  const writableByOthers = (entry.mode & 0o022) !== 0;
38
41
  const rootOwnedStickyDirectory = entry.uid === 0 && (entry.mode & 0o1000) !== 0;
39
- if (writableByOthers && !rootOwnedStickyDirectory)
42
+ if (writableByOthers && !rootOwnedStickyDirectory && !androidSystemAncestor && !androidAppDirectory)
40
43
  return false;
41
44
  }
42
45
  try {
@@ -56,7 +59,7 @@ export function resolveManagedSessionRestoreHome(parentEnv, platform = process.p
56
59
  return candidate;
57
60
  try {
58
61
  const canonical = realpathSync(candidate);
59
- return isTrustedPosixDirectory(canonical, true) ? canonical : undefined;
62
+ return isTrustedPosixDirectory(canonical, true, platform) ? canonical : undefined;
60
63
  }
61
64
  catch {
62
65
  return undefined;
@@ -138,12 +141,15 @@ function readProjectGenerationMarker(path, platform) {
138
141
  return undefined;
139
142
  }
140
143
  }
141
- function getDirectoryFilesystemIdentity(path) {
144
+ function getDirectoryFilesystemIdentity(path, platform) {
142
145
  try {
143
146
  const entry = statSync(path, { bigint: true });
144
- return entry.isDirectory() && entry.dev > 0n && entry.ino > 0n && entry.birthtimeNs > 0n
145
- ? `${entry.dev}:${entry.ino}:${entry.birthtimeNs}`
146
- : undefined;
147
+ if (!entry.isDirectory() || entry.dev <= 0n || entry.ino <= 0n)
148
+ return undefined;
149
+ // ponytail: Android reports mutable ctime as birthtime; use statx birthtime/inode generation when Node exposes either reliably.
150
+ return platform === "android"
151
+ ? `${entry.dev}:${entry.ino}`
152
+ : entry.birthtimeNs > 0n ? `${entry.dev}:${entry.ino}:${entry.birthtimeNs}` : undefined;
147
153
  }
148
154
  catch {
149
155
  return undefined;
@@ -157,13 +163,13 @@ function resolveManagedSessionRestoreProjectCheckout(cwd, platform) {
157
163
  catch {
158
164
  return undefined;
159
165
  }
160
- if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false))
166
+ if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false, platform))
161
167
  return undefined;
162
168
  const checkout = resolveGitCheckout(canonicalCwd, platform);
163
169
  if (!checkout)
164
170
  return undefined;
165
- if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true)
166
- || !isTrustedPosixDirectory(checkout.gitDirectory, true)))
171
+ if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true, platform)
172
+ || !isTrustedPosixDirectory(checkout.gitDirectory, true, platform)))
167
173
  return undefined;
168
174
  return { canonicalCwd, ...checkout };
169
175
  }
@@ -175,8 +181,8 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
175
181
  if (!checkout)
176
182
  return undefined;
177
183
  const { canonicalCwd } = checkout;
178
- const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory);
179
- const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory);
184
+ const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory, platform);
185
+ const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory, platform);
180
186
  if (!gitFilesystemIdentity || !worktreeFilesystemIdentity)
181
187
  return undefined;
182
188
  const markerPath = join(checkout.gitDirectory, PROJECT_GENERATION_MARKER_NAME);
@@ -192,22 +198,35 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
192
198
  projectGenerationCache.delete(canonicalCwd);
193
199
  try {
194
200
  if (!marker) {
195
- const candidatePath = `${markerPath}.candidate-${process.pid}-${randomUUID()}`;
196
- try {
197
- writeFileSync(candidatePath, JSON.stringify({ id: randomUUID(), version: 1 }), { encoding: "utf8", flag: "wx", mode: 0o600 });
201
+ const content = JSON.stringify({ id: randomUUID(), version: 1 });
202
+ if (platform === "android") {
203
+ // ponytail: Android denies hard links in app storage; use renameat2(RENAME_NOREPLACE) if Node exposes it.
198
204
  try {
199
- linkSync(candidatePath, markerPath);
205
+ writeFileSync(markerPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
200
206
  }
201
207
  catch (error) {
202
208
  if (error.code !== "EEXIST")
203
209
  return undefined;
204
210
  }
205
211
  }
206
- finally {
212
+ else {
213
+ const candidatePath = `${markerPath}.candidate-${process.pid}-${randomUUID()}`;
207
214
  try {
208
- unlinkSync(candidatePath);
215
+ writeFileSync(candidatePath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
216
+ try {
217
+ linkSync(candidatePath, markerPath);
218
+ }
219
+ catch (error) {
220
+ if (error.code !== "EEXIST")
221
+ return undefined;
222
+ }
223
+ }
224
+ finally {
225
+ try {
226
+ unlinkSync(candidatePath);
227
+ }
228
+ catch { }
209
229
  }
210
- catch { }
211
230
  }
212
231
  marker = readProjectGenerationMarker(markerPath, platform);
213
232
  }
@@ -231,16 +250,23 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
231
250
  export function hasManagedSessionRestoreProjectIdentity(cwd) {
232
251
  return resolveProjectGenerationIdentity(cwd) !== undefined;
233
252
  }
234
- /** Stable for one checkout generation; deliberately changes when a path is replaced by another checkout. */
235
- export function createManagedSessionRestoreKey(cwd) {
253
+ /** Keep fresh rotations from one Pi transcript in one private upstream restore pool. */
254
+ export function getManagedSessionRestoreScope(sessionName) {
255
+ return sessionName.replace(MANAGED_SESSION_FRESH_SUFFIX_PATTERN, "");
256
+ }
257
+ /** Stable for one Pi transcript and checkout generation; isolated from other concurrent transcripts. */
258
+ export function createManagedSessionRestoreKey(cwd, restoreScope = "", platform = process.platform) {
236
259
  let canonicalCwd = resolve(cwd);
237
260
  try {
238
261
  canonicalCwd = realpathSync(canonicalCwd);
239
262
  }
240
263
  catch { }
241
- const identity = resolveProjectGenerationIdentity(canonicalCwd);
264
+ const identity = resolveProjectGenerationIdentity(canonicalCwd, platform);
242
265
  const material = identity ?? `unavailable:${canonicalCwd}`;
243
- const digest = createHash("sha256").update(`restore-v2:${material}`).digest("hex").slice(0, MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH);
266
+ const digest = createHash("sha256")
267
+ .update(`restore-v3:${material}:scope:${restoreScope}`)
268
+ .digest("hex")
269
+ .slice(0, MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH);
244
270
  return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
245
271
  }
246
272
  function hasValidEncryptionKey(parentEnv) {