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,4 +1,5 @@
1
1
  import { rm } from "node:fs/promises";
2
+ import { getAgentBrowserSessionIdentityKey, VALUE_FLAGS } from "../../argv-grammar.js";
2
3
  import { runAgentBrowserProcess } from "../../process.js";
3
4
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
4
5
  import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
@@ -7,13 +8,11 @@ import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, normal
7
8
  import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, } from "../../command-taxonomy.js";
8
9
  import { chooseOpenResultTabCorrection } from "../../runtime.js";
9
10
  import { isRecord } from "../../parsing.js";
10
- import { parseUserBatchStdin } from "../batch-stdin.js";
11
+ import { getUpstreamEffectiveBatchSteps, parseUserBatchStdin } from "../batch-stdin.js";
11
12
  export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
12
13
  export function applyBrowserRunStatePatch(state, patch) {
13
14
  if (!patch)
14
15
  return;
15
- if (patch.allowedDomainsBySession)
16
- state.allowedDomainsBySession = patch.allowedDomainsBySession;
17
16
  if ("artifactManifest" in patch)
18
17
  state.artifactManifest = patch.artifactManifest;
19
18
  if (patch.freshSessionOrdinal !== undefined)
@@ -38,7 +37,7 @@ export function applyBrowserRunStatePatch(state, patch) {
38
37
  export const getSessionContextKey = getSessionPageStateKey;
39
38
  export function buildSessionDetailFields(sessionName, usedImplicitSession, namespace, managedSessionRestoreDisabled = false) {
40
39
  return {
41
- ...(namespace ? { namespace } : {}),
40
+ ...(namespace !== undefined ? { namespace } : {}),
42
41
  ...(sessionName
43
42
  ? {
44
43
  sessionName,
@@ -213,10 +212,15 @@ export function getTraceOwnerGuardMessage(options) {
213
212
  : `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
214
213
  }
215
214
  export function updateTraceOwnerState(options) {
216
- const owner = getTraceOwner(options.command);
217
- if (!owner || !options.sessionName || !options.succeeded) {
215
+ if (!options.sessionName || !options.succeeded)
216
+ return;
217
+ if (isCloseCommand(options.command)) {
218
+ options.traceOwners.delete(options.sessionName);
218
219
  return;
219
220
  }
221
+ const owner = getTraceOwner(options.command);
222
+ if (!owner)
223
+ return;
220
224
  if (options.subcommand === "start") {
221
225
  options.traceOwners.set(options.sessionName, owner);
222
226
  }
@@ -243,14 +247,13 @@ export function extractNavigationSummaryFromData(data) {
243
247
  const result = isRecord(data) && isRecord(data.result) ? data.result : data;
244
248
  const title = extractStringResultField(result, "title");
245
249
  const url = extractStringResultField(result, "url");
246
- return title || url ? { title, url } : undefined;
250
+ const urlChanged = isRecord(result) && typeof result.urlChanged === "boolean" ? result.urlChanged : undefined;
251
+ return title || url ? { title, url, urlChanged } : undefined;
247
252
  }
248
- export function shouldCaptureNavigationSummary(command, data) {
253
+ export function shouldCaptureNavigationSummary(command, data, subcommand) {
249
254
  if (command === "eval")
250
255
  return true;
251
- if (isRecord(data) && typeof data.clicked === "string" && !data.clicked.startsWith("@") && !data.clicked.startsWith("ref=") && typeof data.href !== "string")
252
- return false;
253
- return (isNavigationObservableCommandName(command) &&
256
+ return (isNavigationObservableCommandName(command, subcommand) &&
254
257
  (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
255
258
  }
256
259
  export function mergeNavigationSummaryIntoData(data, navigationSummary) {
@@ -269,29 +272,42 @@ function extractBatchResultCommand(item) {
269
272
  return Array.isArray(item.command) ? item.command.filter((token) => typeof token === "string") : [];
270
273
  }
271
274
  export function getStaleRefArgs(commandTokens, stdin) {
272
- if (commandTokens[0] !== "batch" || stdin === undefined) {
273
- return commandTokens;
274
- }
275
- const parsed = parseUserBatchStdin(stdin);
276
- if (parsed.error || parsed.steps === undefined) {
275
+ if (commandTokens[0] !== "batch") {
277
276
  return commandTokens;
278
277
  }
279
- return parsed.steps.flatMap((step) => step);
278
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
279
+ return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
280
+ }
281
+ // Selector-carrying flags whose values remain page-scoped refs; values of other value-taking flags
282
+ // (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
283
+ // per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
284
+ // so a ref after them is a genuine positional selector and must stay guarded.
285
+ const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
286
+ const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
287
+ function isNonSelectorValueFlagToken(token) {
288
+ if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
289
+ return false;
290
+ return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
280
291
  }
281
292
  function collectRefsFromTokens(tokens) {
282
- return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
293
+ const refs = [];
294
+ for (const [index, token] of tokens.entries()) {
295
+ if (!/^@e\d+\b/.test(token))
296
+ continue;
297
+ if (isNonSelectorValueFlagToken(index > 0 ? tokens[index - 1] : undefined))
298
+ continue;
299
+ refs.push(token.slice(1));
300
+ }
301
+ return refs;
283
302
  }
284
303
  export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
285
304
  const collectFromStep = (step) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
286
- if (commandTokens[0] !== "batch" || stdin === undefined) {
287
- return collectFromStep(commandTokens);
288
- }
289
- const parsed = parseUserBatchStdin(stdin);
290
- if (parsed.error || parsed.steps === undefined) {
305
+ if (commandTokens[0] !== "batch") {
291
306
  return collectFromStep(commandTokens);
292
307
  }
308
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
293
309
  const refsBeforeInBatchSnapshot = [];
294
- for (const step of parsed.steps) {
310
+ for (const step of steps) {
295
311
  if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
296
312
  break;
297
313
  refsBeforeInBatchSnapshot.push(...collectFromStep(step));
@@ -318,13 +334,10 @@ function isSafeSameSnapshotFormBatchStep(step, refSnapshot) {
318
334
  return false;
319
335
  }
320
336
  function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
321
- if (commandTokens[0] !== "batch" || stdin === undefined)
322
- return undefined;
323
- const parsed = parseUserBatchStdin(stdin);
324
- if (parsed.error || parsed.steps === undefined)
337
+ if (commandTokens[0] !== "batch")
325
338
  return undefined;
326
339
  let priorStepInvalidatesRefs = false;
327
- for (const step of parsed.steps) {
340
+ for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
328
341
  if ((step[0] ?? "") === "snapshot") {
329
342
  priorStepInvalidatesRefs = false;
330
343
  }
@@ -332,7 +345,7 @@ function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
332
345
  if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
333
346
  return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
334
347
  }
335
- if (isRefInvalidatingBatchCommand(step[0]) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
348
+ if (isRefInvalidatingBatchCommand(step) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
336
349
  priorStepInvalidatesRefs = true;
337
350
  }
338
351
  }
@@ -355,7 +368,9 @@ export function buildStaleRefPreflight(options) {
355
368
  return undefined;
356
369
  if (options.refSnapshotInvalidation) {
357
370
  return {
358
- message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
371
+ message: options.refSnapshotInvalidation.reason === "page-transition"
372
+ ? `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used yet. ${options.refSnapshotInvalidation.summary}`
373
+ : `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
359
374
  refIds: usedRefIds,
360
375
  snapshotInvalidation: options.refSnapshotInvalidation,
361
376
  };
@@ -401,11 +416,22 @@ export function shouldPinSessionTabForCommand(options) {
401
416
  }
402
417
  export function buildPinnedBatchPlan(options) {
403
418
  if (options.command === "batch") {
419
+ const tabSelectionStep = ["tab", options.selectedTab];
420
+ // Upstream executes raw argument steps exclusively when any exist, so the
421
+ // pinned rewrite must dispatch those same steps rather than resurrecting
422
+ // caller stdin the guard never scanned.
423
+ const argumentSteps = getUpstreamEffectiveBatchSteps(options.commandTokens, undefined);
424
+ if (argumentSteps.length > 0) {
425
+ return {
426
+ includeNavigationSummary: false,
427
+ steps: [tabSelectionStep, ...argumentSteps],
428
+ unwrapMode: "user-batch",
429
+ };
430
+ }
404
431
  const parsed = parseUserBatchStdin(options.stdin);
405
432
  if (parsed.error) {
406
433
  return { error: parsed.error };
407
434
  }
408
- const tabSelectionStep = ["tab", options.selectedTab];
409
435
  return {
410
436
  includeNavigationSummary: false,
411
437
  steps: [tabSelectionStep, ...(parsed.steps ?? [])],
@@ -415,7 +441,7 @@ export function buildPinnedBatchPlan(options) {
415
441
  if (options.commandTokens.length === 0) {
416
442
  return undefined;
417
443
  }
418
- const includeNavigationSummary = isNavigationObservableCommandName(options.command);
444
+ const includeNavigationSummary = isNavigationObservableCommandName(options.command, options.commandTokens[1]);
419
445
  const tabSelectionStep = ["tab", options.selectedTab];
420
446
  const commandStep = options.commandTokens;
421
447
  const navigationSummarySteps = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
@@ -505,11 +531,10 @@ export function unwrapPinnedSessionBatchEnvelope(options) {
505
531
  };
506
532
  }
507
533
  export async function runSessionCommandData(options) {
508
- const { allowManagedSessionTarget, args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
534
+ const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
509
535
  if (!sessionName)
510
536
  return undefined;
511
537
  const processResult = await runAgentBrowserProcess({
512
- allowManagedSessionTarget,
513
538
  args: ["--json", ...(namespace !== undefined || pinNamespace ? ["--namespace", namespace ?? ""] : []), "--session", sessionName, ...args],
514
539
  cwd,
515
540
  signal,
@@ -613,10 +638,11 @@ export function electronTargetLabel(target) {
613
638
  export function getActiveElectronRecords(records) {
614
639
  return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
615
640
  }
616
- export function findElectronLaunchRecordForSession(sessionName, records) {
641
+ export function findElectronLaunchRecordForSession(sessionName, records, namespace) {
617
642
  if (!sessionName)
618
643
  return undefined;
619
- return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
644
+ const sessionKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
645
+ return getActiveElectronRecords(records).find((record) => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === sessionKey);
620
646
  }
621
647
  function buildElectronReattachNextAction(record, liveTarget) {
622
648
  const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
@@ -2,7 +2,7 @@ import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../
2
2
  import { discoverElectronApps } from "../../electron/discovery.js";
3
3
  import { boundElectronProbeString } from "../../electron/cdp.js";
4
4
  import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
5
- import { getManagedSessionStateAccessValidationError } from "../../managed-session-state-policy.js";
5
+ import { getPageTargetValidationError } from "../../page-target-validation.js";
6
6
  import { isRecord } from "../../parsing.js";
7
7
  import { withAttachedBrowserSessionContext } from "../../process.js";
8
8
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
@@ -114,6 +114,7 @@ function isElectronLaunchRecord(value) {
114
114
  return false;
115
115
  return value.version === 1 &&
116
116
  value.launchedByWrapper === true &&
117
+ (value.namespace === undefined || typeof value.namespace === "string") &&
117
118
  typeof value.launchId === "string" &&
118
119
  typeof value.appName === "string" &&
119
120
  typeof value.executablePath === "string" &&
@@ -133,13 +134,14 @@ export function restoreElectronLaunchRecordsFromBranch(branch) {
133
134
  const electron = isRecord(details?.electron) ? details.electron : undefined;
134
135
  if (!electron)
135
136
  continue;
137
+ const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
136
138
  const launch = isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
137
139
  if (launch)
138
- records.set(launch.launchId, launch);
140
+ records.set(launch.launchId, { ...launch, namespace: launch.namespace ?? namespace });
139
141
  const cleanupRecords = isRecord(electron.cleanup) && Array.isArray(electron.cleanup.records) ? electron.cleanup.records : [];
140
142
  for (const cleanupRecord of cleanupRecords) {
141
143
  if (isElectronLaunchRecord(cleanupRecord))
142
- records.set(cleanupRecord.launchId, cleanupRecord);
144
+ records.set(cleanupRecord.launchId, { ...cleanupRecord, namespace: cleanupRecord.namespace ?? namespace });
143
145
  }
144
146
  }
145
147
  return records;
@@ -428,22 +430,22 @@ async function withOwnedElectronManagedSessionPolicy(options, run) {
428
430
  }
429
431
  async function collectOwnedElectronManagedSessionTarget(options) {
430
432
  try {
431
- return await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
432
- allowManagedSessionTarget: true,
433
+ const target = await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
433
434
  cwd: options.cwd,
434
435
  namespace: options.namespace,
435
436
  sessionName: options.sessionName,
436
437
  signal: options.signal,
437
438
  timeoutMs: options.timeoutMs,
438
439
  })) ?? { sessionName: options.sessionName };
440
+ return { ...target, namespace: options.namespace };
439
441
  }
440
442
  catch (error) {
441
- return { error: error instanceof Error ? error.message : String(error), sessionName: options.sessionName };
443
+ return { error: error instanceof Error ? error.message : String(error), namespace: options.namespace, sessionName: options.sessionName };
442
444
  }
443
445
  }
444
446
  async function runElectronProbeCommandData(options) {
445
447
  try {
446
- return { data: await runSessionCommandData({ ...options, allowManagedSessionTarget: true, pinNamespace: true, throwOnFailure: true }) };
448
+ return { data: await runSessionCommandData({ ...options, pinNamespace: true, throwOnFailure: true }) };
447
449
  }
448
450
  catch (error) {
449
451
  return { error: error instanceof Error ? error.message : String(error) };
@@ -457,9 +459,6 @@ async function collectElectronProbe(options) {
457
459
  const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
458
460
  if (!url)
459
461
  throw new Error("get url returned no active page URL.");
460
- const fileAccessError = getManagedSessionStateAccessValidationError({ args: ["snapshot", "-i"], currentPageUrl: url, cwd: options.cwd });
461
- if (fileAccessError)
462
- throw new ElectronManagedSessionPolicyError(fileAccessError);
463
462
  const titleResult = await runElectronProbeCommandData({ ...commandContext, args: ["get", "title"] });
464
463
  const focusedResult = await runElectronProbeCommandData({ ...commandContext, args: ["eval", "--stdin"], stdin: ELECTRON_FOCUSED_ELEMENT_EVAL });
465
464
  const tabsResult = await runElectronProbeCommandData({ ...commandContext, args: ["tab", "list"] });
@@ -600,10 +599,10 @@ function buildElectronProbeResult(options) {
600
599
  export async function cleanupTrackedElectronHostLaunches(options) {
601
600
  const results = [];
602
601
  for (const record of options.records) {
603
- const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
602
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
604
603
  const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
605
604
  const managedSessionCloseError = record.sessionName
606
- ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
605
+ ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
607
606
  : undefined;
608
607
  const managedSessionStep = record.sessionName
609
608
  ? managedSessionCloseError
@@ -648,7 +647,7 @@ export async function cleanupActiveElectronHostLaunches(options) {
648
647
  export async function handleElectronHostInput(options) {
649
648
  const currentSessionKey = getSessionPageStateKey(options.managedSessionName, options.managedSessionNamespace) ?? options.managedSessionName;
650
649
  const preserveAttachedBrowserSession = options.attachedSessionKeys.has(currentSessionKey)
651
- || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName));
650
+ || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName));
652
651
  return await withAttachedBrowserSessionContext(preserveAttachedBrowserSession, () => handleElectronHostInputInContext(options));
653
652
  }
654
653
  async function handleElectronHostInputInContext(options) {
@@ -671,11 +670,12 @@ async function handleElectronHostInputInContext(options) {
671
670
  const managedSessions = await Promise.all(records
672
671
  .filter((record) => typeof record.sessionName === "string")
673
672
  .map((record) => {
674
- const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
673
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
675
674
  return collectOwnedElectronManagedSessionTarget({
676
675
  cwd,
677
676
  headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
678
677
  headedManagedAutosaveInterval: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveInterval,
678
+ namespace: record.namespace,
679
679
  restoreState: managedSessionRestoreState,
680
680
  sessionName: record.sessionName,
681
681
  signal,
@@ -684,7 +684,7 @@ async function handleElectronHostInputInContext(options) {
684
684
  }));
685
685
  const mismatches = managedSessions
686
686
  .map((managedSession) => {
687
- const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName);
687
+ const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName && candidate.namespace === managedSession.namespace);
688
688
  const status = record ? statuses.find((candidate) => candidate.launchId === record.launchId) : undefined;
689
689
  return record && status ? buildElectronSessionMismatch({ managedSession, record, statusTargets: status.targets }) : undefined;
690
690
  })
@@ -700,7 +700,7 @@ async function handleElectronHostInputInContext(options) {
700
700
  if (compiledElectron?.action === "probe") {
701
701
  const launchRecord = compiledElectron.launchId
702
702
  ? electronLaunchRecords.get(compiledElectron.launchId)
703
- : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords);
703
+ : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords, managedSessionNamespace);
704
704
  if (compiledElectron.launchId && !launchRecord) {
705
705
  return buildElectronHostFailureResult({
706
706
  compiledElectron: redactedCompiledElectron ?? compiledElectron,
@@ -732,17 +732,16 @@ async function handleElectronHostInputInContext(options) {
732
732
  }
733
733
  try {
734
734
  const status = launchRecord ? await inspectElectronLaunchStatus(launchRecord) : undefined;
735
- const probeNamespace = compiledElectron.launchId ? undefined : managedSessionNamespace;
735
+ const probeNamespace = compiledElectron.launchId ? launchRecord?.namespace : managedSessionNamespace;
736
736
  const pageStateKey = getSessionPageStateKey(probeSessionName, probeNamespace) ?? probeSessionName;
737
737
  const currentPageState = sessionPageState.get(pageStateKey);
738
- const fileAccessError = getManagedSessionStateAccessValidationError({
738
+ const pageTargetError = getPageTargetValidationError({
739
739
  args: ["snapshot", "-i"],
740
740
  currentPageUrl: currentPageState.tabTarget?.url,
741
- cwd,
742
741
  pageUrlUnknown: currentPageState.tabTargetUnknown === true,
743
742
  });
744
- if (fileAccessError)
745
- throw new ElectronManagedSessionPolicyError(fileAccessError);
743
+ if (pageTargetError)
744
+ throw new ElectronManagedSessionPolicyError(pageTargetError);
746
745
  const managedSessionOwner = ownedManagedSessions.get(pageStateKey);
747
746
  const headedManagedAutosaveDisabled = managedSessionOwner?.headedManagedAutosaveDisabled === true;
748
747
  const headedManagedAutosaveInterval = managedSessionOwner?.headedManagedAutosaveInterval;
@@ -5,6 +5,7 @@ import { compileAgentBrowserElectron } from "../input-modes/electron.js";
5
5
  import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "../input-modes/job.js";
6
6
  import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl } from "../input-modes/lookups.js";
7
7
  import { compileAgentBrowserSemanticAction } from "../input-modes/semantic-action.js";
8
+ import { AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS, compileAgentBrowserScript } from "../input-modes/script.js";
8
9
  function redactCompiledElectron(compiled) {
9
10
  if (!compiled)
10
11
  return undefined;
@@ -71,6 +72,7 @@ export function resolveAgentBrowserInput(options) {
71
72
  const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
72
73
  const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
73
74
  const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
75
+ const scriptResult = params.script === undefined ? {} : compileAgentBrowserScript(params.script);
74
76
  const hasExplicitArgs = Array.isArray(params.args);
75
77
  const explicitInputModes = [
76
78
  hasExplicitArgs,
@@ -80,19 +82,21 @@ export function resolveAgentBrowserInput(options) {
80
82
  Boolean(sourceLookupResult.compiled),
81
83
  Boolean(networkSourceLookupResult.compiled),
82
84
  Boolean(electronResult.compiled),
85
+ Boolean(scriptResult.compiled),
83
86
  ].filter(Boolean).length;
84
87
  const inputModeError = explicitInputModes !== 1
85
- ? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
88
+ ? "Provide exactly one of script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
86
89
  : undefined;
87
90
  const compiledSemanticAction = semanticActionResult.compiled;
88
91
  const compiledQaPreset = qaResult.compiled;
89
92
  const compiledSourceLookup = sourceLookupResult.compiled;
90
93
  const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
91
94
  const compiledElectron = electronResult.compiled;
95
+ const compiledScript = scriptResult.compiled;
92
96
  const compiledJob = jobResult.compiled ?? compiledQaPreset;
93
97
  const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
94
98
  const normalizedExplicitArgs = normalizeExplicitEvalStdinArgs(params.args ?? [], params.stdin);
95
- const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
99
+ const toolArgs = compiledElectron || compiledScript ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
96
100
  const toolStdin = compiledGeneratedBatch?.stdin ?? normalizedExplicitArgs.stdin;
97
101
  const redactedArgs = redactInvocationArgs(toolArgs);
98
102
  const generatedStdinError = params.stdin !== undefined
@@ -100,7 +104,9 @@ export function resolveAgentBrowserInput(options) {
100
104
  ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
101
105
  : compiledElectron
102
106
  ? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
103
- : undefined
107
+ : compiledScript
108
+ ? "Do not provide stdin with script; browser call stdin belongs inside browser({ args, stdin })."
109
+ : undefined
104
110
  : undefined;
105
111
  const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
106
112
  ? "outputPath must be a non-empty string when provided."
@@ -109,7 +115,12 @@ export function resolveAgentBrowserInput(options) {
109
115
  ? "timeoutMs must be a positive integer when provided."
110
116
  : compiledElectron && params.timeoutMs !== undefined
111
117
  ? "Use electron.timeoutMs for electron actions; top-level timeoutMs applies only to browser CLI subprocess calls."
112
- : undefined;
118
+ : compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
119
+ ? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
120
+ : undefined;
121
+ const scriptSessionModeError = compiledScript && params.sessionMode !== undefined
122
+ ? "Do not provide sessionMode with script; script always uses its own isolated session."
123
+ : undefined;
113
124
  const attachedQaSessionError = compiledQaPreset?.checks.attached
114
125
  ? params.sessionMode === "fresh"
115
126
  ? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
@@ -123,31 +134,35 @@ export function resolveAgentBrowserInput(options) {
123
134
  ?? sourceLookupResult.error
124
135
  ?? networkSourceLookupResult.error
125
136
  ?? electronResult.error
137
+ ?? scriptResult.error
126
138
  ?? inputModeError
127
139
  ?? generatedStdinError
128
140
  ?? outputPathError
129
141
  ?? timeoutMsError
142
+ ?? scriptSessionModeError
130
143
  ?? attachedQaSessionError
131
- ?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
144
+ ?? (compiledElectron || compiledScript ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
132
145
  const redactedCompiledJob = redactCompiledJob(compiledJob);
133
146
  const redactedCompiledSemanticAction = compiledSemanticAction
134
147
  ? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
135
148
  : undefined;
136
149
  const attemptedKind = compiledElectron
137
150
  ? "electron"
138
- : compiledNetworkSourceLookup
139
- ? "networkSourceLookup"
140
- : compiledSourceLookup
141
- ? "sourceLookup"
142
- : compiledQaPreset
143
- ? "qa"
144
- : jobResult.compiled
145
- ? "job"
146
- : compiledSemanticAction
147
- ? "semanticAction"
148
- : hasExplicitArgs
149
- ? "args"
150
- : undefined;
151
+ : compiledScript
152
+ ? "script"
153
+ : compiledNetworkSourceLookup
154
+ ? "networkSourceLookup"
155
+ : compiledSourceLookup
156
+ ? "sourceLookup"
157
+ : compiledQaPreset
158
+ ? "qa"
159
+ : jobResult.compiled
160
+ ? "job"
161
+ : compiledSemanticAction
162
+ ? "semanticAction"
163
+ : hasExplicitArgs
164
+ ? "args"
165
+ : undefined;
151
166
  const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
152
167
  const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
153
168
  const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
@@ -178,6 +193,9 @@ export function resolveAgentBrowserInput(options) {
178
193
  if (compiledElectron && redactedCompiledElectron) {
179
194
  return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
180
195
  }
196
+ if (compiledScript) {
197
+ return { ...resolvedBase, compiledScript, kind: "script", status: "valid" };
198
+ }
181
199
  if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
182
200
  return {
183
201
  ...resolvedBase,
@@ -1,7 +1,7 @@
1
- import { mkdir, realpath, stat, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { getAgentBrowserStoragePathValidationError } from "../managed-session-state-policy.js";
1
+ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, isAbsolute, resolve } from "node:path";
4
3
  import { isRecord } from "../parsing.js";
4
+ import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
5
5
  export function normalizeRequestedOutputPath(path) {
6
6
  return path.startsWith("@") ? path.slice(1) : path;
7
7
  }
@@ -11,11 +11,41 @@ function getTextContent(result) {
11
11
  .map((item) => item.text)
12
12
  .join("\n\n") ?? "";
13
13
  }
14
- function getOutputPayload(result) {
14
+ function getOutputSource(result) {
15
+ return isRecord(result.details) && result.details.data !== undefined ? "details.data" : "content.text";
16
+ }
17
+ async function readCompactedSpill(path, manifest) {
18
+ if (!path || !manifest?.entries.some((entry) => entry.kind === "spill"
19
+ && (entry.path === path || entry.absolutePath === path)
20
+ && (entry.storageScope === "persistent-session" || entry.storageScope === "process-temp")
21
+ && (entry.retentionState === "live" || entry.retentionState === "ephemeral")))
22
+ throw new Error("Full compacted output is unavailable from the wrapper-managed spill; outputPath was not written.");
23
+ const text = await readFile(path, "utf8");
24
+ return extname(path) === ".json" ? JSON.parse(text) : text;
25
+ }
26
+ async function rehydrateCompactedData(data, details, manifest) {
27
+ if (isRecord(data) && data.compacted === true) {
28
+ return readCompactedSpill(typeof details.fullOutputPath === "string" ? details.fullOutputPath : undefined, manifest);
29
+ }
30
+ if (!Array.isArray(data))
31
+ return data;
32
+ const batchSteps = Array.isArray(details.batchSteps) ? details.batchSteps : [];
33
+ return Promise.all(data.map(async (row, index) => {
34
+ if (!isRecord(row) || !isRecord(row.result) || row.result.compacted !== true)
35
+ return row;
36
+ const step = isRecord(batchSteps[index]) ? batchSteps[index] : undefined;
37
+ const path = typeof step?.fullOutputPath === "string"
38
+ ? step.fullOutputPath
39
+ : typeof row.result.fullOutputPath === "string" ? row.result.fullOutputPath : undefined;
40
+ return { ...row, result: await readCompactedSpill(path, manifest) };
41
+ }));
42
+ }
43
+ async function getOutputPayload(result) {
15
44
  const details = isRecord(result.details) ? result.details : undefined;
16
- if (details && details.data !== undefined)
17
- return { source: "details.data", value: details.data };
18
- return { source: "content.text", value: getTextContent(result) };
45
+ if (details?.data === undefined)
46
+ return { source: "content.text", value: getTextContent(result) };
47
+ const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
48
+ return { source: "details.data", value: await rehydrateCompactedData(details.data, details, manifest) };
19
49
  }
20
50
  function serializeOutputPayload(value) {
21
51
  return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
@@ -28,9 +58,6 @@ function appendOutputFileNotice(result, message) {
28
58
  }
29
59
  return [{ type: "text", text: message }, ...content];
30
60
  }
31
- export function getAgentBrowserOutputPathValidationError(outputPath, cwd) {
32
- return outputPath ? getAgentBrowserStoragePathValidationError(normalizeRequestedOutputPath(outputPath), cwd) : undefined;
33
- }
34
61
  function getArtifactPaths(result, cwd) {
35
62
  const details = isRecord(result.details) ? result.details : undefined;
36
63
  if (!details || !Array.isArray(details.artifacts))
@@ -61,24 +88,16 @@ async function pathsReferToSameFile(left, right) {
61
88
  export async function applyAgentBrowserOutputPath(options) {
62
89
  if (!options.outputPath)
63
90
  return options.result;
64
- const validationError = getAgentBrowserOutputPathValidationError(options.outputPath, options.cwd);
65
- if (validationError) {
66
- return {
67
- content: [{ type: "text", text: validationError }],
68
- details: { failureCategory: "validation-error", resultCategory: "failure", validationError },
69
- isError: true,
70
- };
71
- }
72
91
  if (options.result.isError || (isRecord(options.result.details) && options.result.details.resultCategory === "failure"))
73
92
  return options.result;
74
93
  const requestedPath = normalizeRequestedOutputPath(options.outputPath);
75
94
  const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
76
- const payload = getOutputPayload(options.result);
95
+ const source = getOutputSource(options.result);
77
96
  for (const artifactPath of getArtifactPaths(options.result, options.cwd)) {
78
97
  if (!await pathsReferToSameFile(absolutePath, artifactPath))
79
98
  continue;
80
99
  const message = "outputPath resolves to the same file as a browser artifact destination; choose a separate outputPath or omit it. The browser artifact was preserved.";
81
- const outputFile = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
100
+ const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
82
101
  const details = isRecord(options.result.details) ? { ...options.result.details } : {};
83
102
  delete details.successCategory;
84
103
  return {
@@ -89,6 +108,7 @@ export async function applyAgentBrowserOutputPath(options) {
89
108
  };
90
109
  }
91
110
  try {
111
+ const payload = await getOutputPayload(options.result);
92
112
  const serialized = serializeOutputPayload(payload.value);
93
113
  await mkdir(dirname(absolutePath), { recursive: true });
94
114
  await writeFile(absolutePath, serialized, "utf8");
@@ -103,7 +123,7 @@ export async function applyAgentBrowserOutputPath(options) {
103
123
  }
104
124
  catch (error) {
105
125
  const message = error instanceof Error ? error.message : String(error);
106
- const outputFile = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
126
+ const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
107
127
  const details = isRecord(options.result.details)
108
128
  ? (() => {
109
129
  const rest = { ...options.result.details };