pi-agent-browser-native 0.3.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,5 +1,5 @@
1
1
  import { findCommandStartIndex } from "./argv-descriptor.js";
2
- import { isBooleanFlagEnabled, optionalGlobalValueFlagConsumesNext } from "./argv-grammar.js";
2
+ import { isBooleanFlagEnabled } from "./argv-grammar.js";
3
3
  export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
4
4
  {
5
5
  flag: "--auto-connect",
@@ -17,6 +17,14 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
17
17
  flag: "--cdp",
18
18
  reason: "selects the browser/CDP endpoint used when an upstream session is launched",
19
19
  },
20
+ {
21
+ flag: "--ca-cert",
22
+ reason: "selects isolated Chromium CA trust and may relaunch the browser",
23
+ },
24
+ {
25
+ flag: "--no-ca-cert",
26
+ reason: "clears retained isolated Chromium CA trust and may relaunch the browser",
27
+ },
20
28
  {
21
29
  flag: "--enable",
22
30
  reason: "selects built-in page init scripts before the upstream browser session is launched",
@@ -29,6 +37,10 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
29
37
  flag: "--webgpu",
30
38
  reason: "selects the platform-specific WebGPU browser launch preset",
31
39
  },
40
+ {
41
+ flag: "--no-webmcp",
42
+ reason: "selects whether locally launched Chrome enables experimental WebMCP support",
43
+ },
32
44
  {
33
45
  flag: "--init-script",
34
46
  reason: "registers page init scripts before the upstream browser session is launched",
@@ -37,6 +49,14 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
37
49
  flag: "--idle-timeout",
38
50
  reason: "configures background browser lifecycle for the launched session",
39
51
  },
52
+ {
53
+ flag: "--args",
54
+ reason: "selects raw Chrome arguments for the browser launch",
55
+ },
56
+ {
57
+ flag: "--user-agent",
58
+ reason: "selects the browser user agent at launch time",
59
+ },
40
60
  {
41
61
  flag: "--headed",
42
62
  reason: "selects whether the launched browser has a visible window",
@@ -112,6 +132,7 @@ export const MANAGED_RESTORE_INCOMPATIBLE_FLAGS = [
112
132
  "--user-agent",
113
133
  "--proxy",
114
134
  "--proxy-bypass",
135
+ "--ca-cert",
115
136
  "--ignore-https-errors",
116
137
  "--allow-file-access",
117
138
  "--webgpu",
@@ -140,6 +161,7 @@ export const MANAGED_RESTORE_INCOMPATIBLE_ENVS = [
140
161
  "AGENT_BROWSER_USER_AGENT",
141
162
  "AGENT_BROWSER_PROXY",
142
163
  "AGENT_BROWSER_PROXY_BYPASS",
164
+ "AGENT_BROWSER_CA_CERT",
143
165
  "AGENT_BROWSER_PLUGINS",
144
166
  "AGENT_BROWSER_IOS_DEVICE",
145
167
  "AGENT_BROWSER_IOS_UDID",
@@ -162,12 +184,12 @@ export function hasLaunchScopedFlagToken(args, flag) {
162
184
  const commandStartIndex = findCommandStartIndex(args);
163
185
  const command = commandStartIndex === undefined ? undefined : args[commandStartIndex];
164
186
  return args.some((token, index) => {
165
- if (token !== flag && !token.startsWith(`${flag}=`))
187
+ if (token.startsWith(`${flag}=`))
188
+ return flag === "--restore";
189
+ if (token !== flag)
166
190
  return false;
167
191
  if (flag === "--auto-connect")
168
192
  return isBooleanFlagEnabled(args, flag);
169
- if (flag === "--restore" && token === "--restore" && optionalGlobalValueFlagConsumesNext(flag, args[index + 1]))
170
- return true;
171
193
  if (flag === "--state" && command === "wait" && commandStartIndex !== undefined && index > commandStartIndex)
172
194
  return false;
173
195
  return true;
@@ -12,7 +12,9 @@ const LOCK_TICKET_FILE = "ticket.json";
12
12
  function getCoordinationDirectory(platform = process.platform) {
13
13
  if (platform !== "win32") {
14
14
  const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
15
- return `/tmp/pi-agent-browser-policy${uid === undefined ? "" : `-${uid}`}`;
15
+ return platform === "android"
16
+ ? join(tmpdir(), `pi-agent-browser-policy${uid === undefined ? "" : `-${uid}`}`)
17
+ : `/tmp/pi-agent-browser-policy${uid === undefined ? "" : `-${uid}`}`;
16
18
  }
17
19
  const user = process.env.USERNAME ?? process.env.USER ?? "unknown";
18
20
  const suffix = createHash("sha256").update(user).digest("hex").slice(0, 12);
@@ -48,9 +50,6 @@ function getPolicyLockDigest(sessionName, namespace) {
48
50
  export function getManagedSessionPolicyLockPath(sessionName, namespace) {
49
51
  return join(getCoordinationDirectory(), `.pi-agent-browser-policy-${getPolicyLockDigest(sessionName, namespace)}.lock-v3`);
50
52
  }
51
- export function getLegacyManagedSessionPolicyLockPath(sessionName, namespace) {
52
- return join(getCoordinationDirectory(), `.pi-agent-browser-policy-${getPolicyLockDigest(sessionName, namespace)}.lock-v2`);
53
- }
54
53
  function parseOwner(content) {
55
54
  if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
56
55
  return undefined;
@@ -67,22 +66,6 @@ function parseOwner(content) {
67
66
  return undefined;
68
67
  }
69
68
  }
70
- function parseLegacyBridgeOwner(content) {
71
- if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
72
- return undefined;
73
- try {
74
- const parsed = JSON.parse(content);
75
- return (parsed.version === 2 || parsed.version === 3)
76
- && Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
77
- && typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
78
- && typeof parsed.token === "string" && parsed.token.length > 0
79
- ? parsed
80
- : undefined;
81
- }
82
- catch {
83
- return undefined;
84
- }
85
- }
86
69
  function parseTicket(content, token) {
87
70
  if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
88
71
  return undefined;
@@ -137,26 +120,6 @@ async function readClaim(path) {
137
120
  return undefined;
138
121
  }
139
122
  }
140
- async function readLegacyBridgeOwner(path) {
141
- try {
142
- const directory = await lstat(path);
143
- const ownerPath = join(path, LOCK_OWNER_FILE);
144
- const ownerEntry = await lstat(ownerPath);
145
- if (!directory.isDirectory() || directory.isSymbolicLink() || !ownerEntry.isFile() || ownerEntry.isSymbolicLink())
146
- return undefined;
147
- if (ownerEntry.size > POLICY_LOCK_MAX_BYTES)
148
- return undefined;
149
- if (process.platform !== "win32") {
150
- const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
151
- if (uid === undefined || directory.uid !== uid || ownerEntry.uid !== uid || (directory.mode & 0o077) !== 0 || (ownerEntry.mode & 0o177) !== 0)
152
- return undefined;
153
- }
154
- return parseLegacyBridgeOwner(await readFile(ownerPath, "utf8"));
155
- }
156
- catch {
157
- return undefined;
158
- }
159
- }
160
123
  async function readClaims(basePath) {
161
124
  const directory = dirname(basePath);
162
125
  const prefix = `${basename(basePath)}.claim-`;
@@ -223,28 +186,6 @@ async function removeClaimOwnedBy(path, token) {
223
186
  await rm(movedPath, { force: true, recursive: true });
224
187
  return true;
225
188
  }
226
- async function removeLegacyBridgeOwnedBy(path, token) {
227
- const current = await readLegacyBridgeOwner(path);
228
- if (current?.version !== 3 || current.token !== token)
229
- return false;
230
- const movedPath = join(dirname(path), `.pi-agent-browser-policy-bridge-remove-${token}-${randomUUID()}`);
231
- try {
232
- await rename(path, movedPath);
233
- }
234
- catch (error) {
235
- return error.code === "ENOENT";
236
- }
237
- const moved = await readLegacyBridgeOwner(movedPath);
238
- if (moved?.version !== 3 || moved.token !== token) {
239
- try {
240
- await rename(movedPath, path);
241
- }
242
- catch { }
243
- return false;
244
- }
245
- await rm(movedPath, { force: true, recursive: true });
246
- return true;
247
- }
248
189
  async function cleanDeadPolicyArtifacts(directory) {
249
190
  let names;
250
191
  try {
@@ -254,72 +195,13 @@ async function cleanDeadPolicyArtifacts(directory) {
254
195
  return;
255
196
  }
256
197
  for (const name of names.filter((candidate) => candidate.startsWith(".pi-agent-browser-policy-remove-")
257
- || candidate.startsWith(".pi-agent-browser-policy-bridge-remove-")
258
- || candidate.includes(".lock-v2.bridge-candidate-")
259
- || candidate.includes(".lock-v2.candidate-")
260
198
  || candidate.includes(".lock-v3.candidate-"))) {
261
199
  const path = join(directory, name);
262
- const owner = await readLegacyBridgeOwner(path);
263
- if (owner && await ownerAlive(owner) === false)
200
+ const claim = await readClaim(path);
201
+ if (claim && await ownerAlive(claim.owner) === false)
264
202
  await rm(path, { force: true, recursive: true }).catch(() => undefined);
265
203
  }
266
204
  }
267
- async function hasLegacyV2Contender(path) {
268
- try {
269
- const prefix = `${basename(path)}.candidate-`;
270
- return (await readdir(dirname(path))).some((name) => name.startsWith(prefix));
271
- }
272
- catch {
273
- return undefined;
274
- }
275
- }
276
- async function acquireLegacyPolicyBridge(options) {
277
- const path = getLegacyManagedSessionPolicyLockPath(options.sessionName, options.namespace);
278
- const candidatePath = `${path}.bridge-candidate-${options.owner.token}`;
279
- try {
280
- await mkdir(candidatePath, { mode: 0o700 });
281
- await writeFile(join(candidatePath, LOCK_OWNER_FILE), JSON.stringify(options.owner), { encoding: "utf8", flag: "wx", mode: 0o600 });
282
- while (!options.signal?.aborted) {
283
- const legacyContender = await hasLegacyV2Contender(path);
284
- if (legacyContender === undefined)
285
- return undefined;
286
- if (legacyContender) {
287
- if (Date.now() >= options.deadline)
288
- return undefined;
289
- await waitForRetry(options.signal);
290
- continue;
291
- }
292
- try {
293
- await rename(candidatePath, path);
294
- const installed = await readLegacyBridgeOwner(path);
295
- return installed?.version === 3 && installed.token === options.owner.token
296
- ? { release: async () => { await removeLegacyBridgeOwnedBy(path, options.owner.token); } }
297
- : undefined;
298
- }
299
- catch (error) {
300
- if (!["EACCES", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error.code ?? ""))
301
- return undefined;
302
- }
303
- const observed = await readLegacyBridgeOwner(path);
304
- if (!observed)
305
- return undefined;
306
- if (observed.version === 3 && await ownerAlive(observed) === false) {
307
- if (await removeLegacyBridgeOwnedBy(path, observed.token))
308
- continue;
309
- }
310
- if (Date.now() >= options.deadline)
311
- return undefined;
312
- await waitForRetry(options.signal);
313
- }
314
- return undefined;
315
- }
316
- catch {
317
- return undefined;
318
- }
319
- finally {
320
- await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
321
- }
322
- }
323
205
  function claimPrecedes(left, right) {
324
206
  if (left.ticket === null)
325
207
  return true;
@@ -356,7 +238,6 @@ export async function acquireManagedSessionPolicyLock(options) {
356
238
  const candidatePath = `${basePath}.candidate-${token}`;
357
239
  const claimPath = `${basePath}.claim-${token}`;
358
240
  let claimPublished = false;
359
- let legacyBridge;
360
241
  let lockAcquired = false;
361
242
  try {
362
243
  await mkdir(candidatePath, { mode: 0o700 });
@@ -395,20 +276,8 @@ export async function acquireManagedSessionPolicyLock(options) {
395
276
  }
396
277
  if (!blocked) {
397
278
  await cleanDeadPolicyArtifacts(directory);
398
- legacyBridge = await acquireLegacyPolicyBridge({
399
- deadline,
400
- owner,
401
- signal: options.signal,
402
- sessionName: options.sessionName,
403
- namespace: options.namespace,
404
- });
405
- if (!legacyBridge)
406
- return undefined;
407
279
  lockAcquired = true;
408
- return { release: async () => {
409
- await legacyBridge?.release();
410
- await removeClaimOwnedBy(claimPath, token);
411
- } };
280
+ return { release: async () => { await removeClaimOwnedBy(claimPath, token); } };
412
281
  }
413
282
  if (Date.now() >= deadline)
414
283
  return undefined;
@@ -421,8 +290,6 @@ export async function acquireManagedSessionPolicyLock(options) {
421
290
  }
422
291
  finally {
423
292
  await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
424
- if (!lockAcquired)
425
- await legacyBridge?.release();
426
293
  if (claimPublished && !lockAcquired)
427
294
  await removeClaimOwnedBy(claimPath, token);
428
295
  }
@@ -1,23 +1,17 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { chmodSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { extractCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
2
+ import { extractUpstreamCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
5
3
  import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, extractRequestedRestoreKey, getAgentBrowserSessionIdentityKey, isUpstreamEnvFlagEnabled, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
6
4
  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";
5
+ import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
8
6
  import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
9
- import { writeSecureTempFile } from "./temp.js";
10
- export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure } from "./managed-session-storage.js";
7
+ import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
8
+ export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope } from "./managed-session-storage.js";
11
9
  export { pruneOwnedManagedSessionRestoreSnapshots } from "./managed-session-snapshots.js";
12
10
  const AGENT_BROWSER_CONFIG_ENV = "AGENT_BROWSER_CONFIG";
13
11
  const AGENT_BROWSER_RESTORE_ENV = "AGENT_BROWSER_RESTORE";
14
12
  const MANAGED_SESSION_RESTORE_ENV = "PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE";
15
13
  export const MANAGED_SESSION_NAME_PREFIX = "piab-";
16
- const MANAGED_SESSION_RESTORE_EMPTY_CONFIG_CONTENT = "{}\n";
17
- const MANAGED_SESSION_RESTORE_EMPTY_CONFIG_NAME = ".pi-agent-browser-managed-restore-config-v1.json";
18
14
  const MANAGED_SESSION_RESTORE_SPAWN_PINNED_ENVS = new Set([AGENT_BROWSER_CONFIG_ENV, AGENT_BROWSER_RESTORE_ENV, "AGENT_BROWSER_NAMESPACE"]);
19
- let managedSessionRestoreEmptyConfigPath;
20
- let managedSessionRestoreEmptyConfigPromise;
21
15
  function isDisabledEnvFlag(value) {
22
16
  if (value === undefined)
23
17
  return false;
@@ -61,7 +55,7 @@ export class ManagedSessionRestoreState {
61
55
  if (sessionName)
62
56
  this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), restoreKey);
63
57
  }
64
- replace(identities, options = {}) {
58
+ replace(identities = [], options = {}) {
65
59
  if (!options.preserveDaemonRestoreKeys)
66
60
  this.#daemonRestoreKeys.clear();
67
61
  this.#disabled.clear();
@@ -73,6 +67,9 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
73
67
  export async function withOwnedManagedSessionContext(context, run) {
74
68
  return await ownedManagedSessionStorage.run(context, run);
75
69
  }
70
+ export function getOwnedManagedSessionRestoreKey() {
71
+ return ownedManagedSessionStorage.getStore()?.restoreKey;
72
+ }
76
73
  export function resolveOwnedManagedSessionContext(options) {
77
74
  const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
78
75
  const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
@@ -96,33 +93,18 @@ function ownedContextMatches(sessionName, namespace) {
96
93
  export function isOwnedManagedSessionTarget(args) {
97
94
  return ownedContextMatches(extractExplicitSessionName(args), extractExplicitNamespace(args)) !== undefined;
98
95
  }
99
- function pathExistsOrIsUnreadable(path) {
100
- try {
101
- lstatSync(path);
102
- return true;
103
- }
104
- catch (error) {
105
- return error.code !== "ENOENT";
106
- }
107
- }
108
96
  function hasExplicitConfigArg(args) {
109
97
  return scanUpstreamGlobalFlagOccurrences(args, "--config").length > 0;
110
98
  }
111
99
  function closesBrowserSession(args) {
112
100
  return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
113
101
  }
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);
102
+ export function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
103
+ return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
122
104
  }
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);
105
+ /** Caller-selected upstream config disables the wrapper's automatic restore injection without blocking that config. */
106
+ export function agentBrowserConfigBlocksManagedRestore(_cwd, parentEnv = getAgentBrowserProcessEnvironment(), args = [], platform = process.platform) {
107
+ return !resolveManagedSessionRestoreHome(parentEnv, platform) || agentBrowserExplicitConfigIsPresent(parentEnv, args);
126
108
  }
127
109
  function omitWrapperInjectedUserAgent(args, enabled) {
128
110
  if (!enabled)
@@ -131,7 +113,7 @@ function omitWrapperInjectedUserAgent(args, enabled) {
131
113
  return index < 0 ? args : [...args.slice(0, index), ...args.slice(index + 2)];
132
114
  }
133
115
  function batchHasManagedSessionRestoreConflict(args, stdin) {
134
- const [command, ...commandArgs] = extractCommandTokens(args);
116
+ const [command, ...commandArgs] = extractUpstreamCommandTokens(args);
135
117
  if (command !== "batch")
136
118
  return false;
137
119
  if (commandArgs.some((token) => token !== "--bail"))
@@ -142,7 +124,7 @@ function batchHasManagedSessionRestoreConflict(args, stdin) {
142
124
  return parsed.steps.some((step) => ["connect", "batch"].includes(parseCommandInfo(step).command ?? ""));
143
125
  }
144
126
  function hasManagedSessionRestoreLaunchConflict(options) {
145
- const parentEnv = options.parentEnv ?? process.env;
127
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
146
128
  const effectiveEnv = { ...parentEnv, ...options.env };
147
129
  const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
148
130
  if (MANAGED_RESTORE_INCOMPATIBLE_ENVS.some((name) => hasUpstreamEnvValue(effectiveEnv, name)))
@@ -156,7 +138,7 @@ function hasManagedSessionRestoreLaunchConflict(options) {
156
138
  return hasExplicitConfigArg(args) || hasUpstreamEnvValue({ ...parentEnv, ...options.env }, AGENT_BROWSER_CONFIG_ENV);
157
139
  }
158
140
  function managedSessionRestoreOptedOut(options) {
159
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
141
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
160
142
  return isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]);
161
143
  }
162
144
  function isManagedSessionRestoreIncompatible(options) {
@@ -164,7 +146,7 @@ function isManagedSessionRestoreIncompatible(options) {
164
146
  return true;
165
147
  if (managedSessionRestoreOptedOut(options))
166
148
  return false;
167
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
149
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
168
150
  const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
169
151
  if (options.cwd && !hasManagedSessionRestoreProjectIdentity(options.cwd))
170
152
  return true;
@@ -173,7 +155,7 @@ function isManagedSessionRestoreIncompatible(options) {
173
155
  return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, extractExplicitNamespace(args));
174
156
  }
175
157
  function resolveManagedSessionRestorePolicy(options) {
176
- const parentEnv = options.parentEnv ?? process.env;
158
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
177
159
  const sessionName = extractExplicitSessionName(options.args);
178
160
  const namespace = extractExplicitNamespace(options.args);
179
161
  const ownedContext = ownedContextMatches(sessionName, namespace);
@@ -204,7 +186,7 @@ export function getOwnedManagedSessionCompatibilityEnv(options) {
204
186
  const { owned, ownedContext } = resolveManagedSessionRestorePolicy(options);
205
187
  if (!owned || !ownedContext)
206
188
  return {};
207
- const parentEnv = options.parentEnv ?? process.env;
189
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
208
190
  const callEnv = options.env ?? {};
209
191
  const explicitRawInterval = Object.hasOwn(callEnv, "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS")
210
192
  ? callEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS
@@ -215,87 +197,13 @@ export function getOwnedManagedSessionCompatibilityEnv(options) {
215
197
  ...(ownedContext.headedManagedAutosaveInterval !== undefined && !explicitIntervalMatches ? { AGENT_BROWSER_AUTOSAVE_INTERVAL_MS: ownedContext.headedManagedAutosaveInterval } : {}),
216
198
  };
217
199
  }
218
- export function shouldOmitOwnedManagedSessionRestoreEnv(options) {
219
- return resolveManagedSessionRestorePolicy(options).owned && closesBrowserSession(options.args);
220
- }
221
- export function canonicalizeOwnedManagedSessionCloseArgs(options, force = false) {
222
- const policy = resolveManagedSessionRestorePolicy(options);
223
- if (!policy.owned || !closesBrowserSession(options.args))
224
- return options.args;
225
- const sessionName = policy.ownedContext?.sessionName ?? policy.sessionName;
226
- if (!sessionName)
227
- return options.args;
228
- const namespace = canonicalizeAgentBrowserNamespace(policy.ownedContext?.namespace ?? policy.namespace) ?? "";
229
- const command = options.args.at(-1);
230
- const prefix = options.args.slice(0, -1);
231
- const safePrefixes = [
232
- ["--session", sessionName],
233
- ["--json", "--session", sessionName],
234
- ["--namespace", namespace, "--session", sessionName],
235
- ["--json", "--namespace", namespace, "--session", sessionName],
236
- ];
237
- if (!force && command && ["close", "exit", "quit"].includes(command)
238
- && safePrefixes.some((candidate) => candidate.length === prefix.length && candidate.every((token, index) => token === prefix[index]))) {
239
- return options.args;
240
- }
241
- return ["--json", "--namespace", namespace, "--session", sessionName, "close"];
242
- }
243
- export function cleanupManagedSessionRestoreConfig() {
244
- if (managedSessionRestoreEmptyConfigPath) {
245
- try {
246
- unlinkSync(managedSessionRestoreEmptyConfigPath);
247
- }
248
- catch { }
249
- }
250
- managedSessionRestoreEmptyConfigPath = undefined;
251
- managedSessionRestoreEmptyConfigPromise = undefined;
252
- }
253
- async function ensureManagedSessionRestoreEmptyConfig(platform) {
254
- for (let attempt = 0; attempt < 2; attempt += 1) {
255
- try {
256
- managedSessionRestoreEmptyConfigPromise ??= writeSecureTempFile({
257
- content: MANAGED_SESSION_RESTORE_EMPTY_CONFIG_CONTENT,
258
- prefix: MANAGED_SESSION_RESTORE_EMPTY_CONFIG_NAME.replace(/\.json$/, ""),
259
- suffix: ".json",
260
- }).then((path) => {
261
- if (platform !== "win32")
262
- chmodSync(path, 0o400);
263
- managedSessionRestoreEmptyConfigPath = path;
264
- return path;
265
- });
266
- const path = await managedSessionRestoreEmptyConfigPromise;
267
- let entry = lstatSync(path);
268
- if (entry.isSymbolicLink() || !entry.isFile())
269
- throw new Error("Managed restore config is not a regular file.");
270
- if (platform !== "win32" && (entry.mode & 0o777) !== 0o400) {
271
- chmodSync(path, 0o400);
272
- entry = lstatSync(path);
273
- }
274
- if (entry.isSymbolicLink() || !entry.isFile() || (platform !== "win32" && (entry.mode & 0o777) !== 0o400))
275
- throw new Error("Managed restore config permissions are unsafe.");
276
- if (readFileSync(path, "utf8") !== MANAGED_SESSION_RESTORE_EMPTY_CONFIG_CONTENT)
277
- throw new Error("Managed restore config content changed.");
278
- return path;
279
- }
280
- catch {
281
- cleanupManagedSessionRestoreConfig();
282
- }
283
- }
284
- return undefined;
285
- }
286
- export async function getManagedSessionRestoreConfigEnv(restoreEnv, pinForOwnedClose = false) {
287
- if (restoreEnv[AGENT_BROWSER_RESTORE_ENV] === undefined && !pinForOwnedClose)
288
- return {};
289
- const path = await ensureManagedSessionRestoreEmptyConfig(process.platform);
290
- return path ? { [AGENT_BROWSER_CONFIG_ENV]: path } : undefined;
291
- }
292
200
  export function getManagedSessionRestoreProtectedEnv(options, restoreEnv) {
293
201
  const { ownedContext } = resolveManagedSessionRestorePolicy(options);
294
202
  if (restoreEnv[AGENT_BROWSER_RESTORE_ENV] === undefined)
295
203
  return {};
296
204
  if (ownedContext?.protectedStorageEnv)
297
205
  return { ...ownedContext.protectedStorageEnv };
298
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
206
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
299
207
  return getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv);
300
208
  }
301
209
  export function validateManagedSessionRestoreContextForSpawn(options) {
@@ -303,7 +211,7 @@ export function validateManagedSessionRestoreContextForSpawn(options) {
303
211
  if (closesBrowserSession(options.args) || ownedContext?.restoreDecision !== "enabled")
304
212
  return true;
305
213
  const ownedCwd = ownedContext.cwd ?? options.cwd;
306
- if (!ownedContext.restoreKey || createManagedSessionRestoreKey(ownedCwd) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
214
+ if (!ownedContext.restoreKey || !ownedContext.restoreScope || createManagedSessionRestoreKey(ownedCwd, ownedContext.restoreScope) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
307
215
  return false;
308
216
  const effectiveEnv = { ...parentEnv, ...options.env };
309
217
  if (isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]))
@@ -330,7 +238,7 @@ export function getManagedSessionRestoreEnv(options) {
330
238
  return {};
331
239
  if (restoreState.isDisabled(sessionName, namespace) || !sessionName)
332
240
  return {};
333
- return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd) };
241
+ return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd, getManagedSessionRestoreScope(sessionName)) };
334
242
  }
335
243
  /** Commit sticky suppression only after an owned-context subprocess has actually started. */
336
244
  export function commitManagedSessionRestoreSuppression(options) {
@@ -375,8 +283,9 @@ export function buildOwnedManagedSessionRestoreContext(options) {
375
283
  const projectIdentityAvailable = !optedOut && hasManagedSessionRestoreProjectIdentity(ownedCwd);
376
284
  const incompatible = !optedOut && isManagedSessionRestoreIncompatible(policyOptions);
377
285
  const enabled = !optedOut && !incompatible;
378
- const effectiveEnv = { ...(options.parentEnv ?? process.env), ...options.env };
379
- const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd) : undefined;
286
+ const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
287
+ const restoreScope = getManagedSessionRestoreScope(owned.sessionName);
288
+ const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd, restoreScope) : undefined;
380
289
  return {
381
290
  ...owned,
382
291
  compatibilityUserAgent: options.compatibilityUserAgent,
@@ -386,6 +295,7 @@ export function buildOwnedManagedSessionRestoreContext(options) {
386
295
  protectedStorageEnv: enabled ? getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv) : undefined,
387
296
  restoreDecision: optedOut ? "opted-out" : incompatible ? "incompatible" : "enabled",
388
297
  restoreKey,
298
+ restoreScope,
389
299
  restoreSuppressed: optedOut || incompatible,
390
300
  };
391
301
  }
@@ -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({