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,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,7 +8,7 @@ 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)
@@ -38,7 +39,7 @@ export function applyBrowserRunStatePatch(state, patch) {
38
39
  export const getSessionContextKey = getSessionPageStateKey;
39
40
  export function buildSessionDetailFields(sessionName, usedImplicitSession, namespace, managedSessionRestoreDisabled = false) {
40
41
  return {
41
- ...(namespace ? { namespace } : {}),
42
+ ...(namespace !== undefined ? { namespace } : {}),
42
43
  ...(sessionName
43
44
  ? {
44
45
  sessionName,
@@ -213,10 +214,15 @@ export function getTraceOwnerGuardMessage(options) {
213
214
  : `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
214
215
  }
215
216
  export function updateTraceOwnerState(options) {
216
- const owner = getTraceOwner(options.command);
217
- if (!owner || !options.sessionName || !options.succeeded) {
217
+ if (!options.sessionName || !options.succeeded)
218
+ return;
219
+ if (isCloseCommand(options.command)) {
220
+ options.traceOwners.delete(options.sessionName);
218
221
  return;
219
222
  }
223
+ const owner = getTraceOwner(options.command);
224
+ if (!owner)
225
+ return;
220
226
  if (options.subcommand === "start") {
221
227
  options.traceOwners.set(options.sessionName, owner);
222
228
  }
@@ -248,8 +254,6 @@ export function extractNavigationSummaryFromData(data) {
248
254
  export function shouldCaptureNavigationSummary(command, data) {
249
255
  if (command === "eval")
250
256
  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
257
  return (isNavigationObservableCommandName(command) &&
254
258
  (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
255
259
  }
@@ -269,29 +273,42 @@ function extractBatchResultCommand(item) {
269
273
  return Array.isArray(item.command) ? item.command.filter((token) => typeof token === "string") : [];
270
274
  }
271
275
  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) {
276
+ if (commandTokens[0] !== "batch") {
277
277
  return commandTokens;
278
278
  }
279
- return parsed.steps.flatMap((step) => step);
279
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
280
+ return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
281
+ }
282
+ // Selector-carrying flags whose values remain page-scoped refs; values of other value-taking flags
283
+ // (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
284
+ // per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
285
+ // so a ref after them is a genuine positional selector and must stay guarded.
286
+ const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
287
+ const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
288
+ function isNonSelectorValueFlagToken(token) {
289
+ if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
290
+ return false;
291
+ return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
280
292
  }
281
293
  function collectRefsFromTokens(tokens) {
282
- return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
294
+ const refs = [];
295
+ for (const [index, token] of tokens.entries()) {
296
+ if (!/^@e\d+\b/.test(token))
297
+ continue;
298
+ if (isNonSelectorValueFlagToken(index > 0 ? tokens[index - 1] : undefined))
299
+ continue;
300
+ refs.push(token.slice(1));
301
+ }
302
+ return refs;
283
303
  }
284
304
  export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
285
305
  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) {
306
+ if (commandTokens[0] !== "batch") {
291
307
  return collectFromStep(commandTokens);
292
308
  }
309
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
293
310
  const refsBeforeInBatchSnapshot = [];
294
- for (const step of parsed.steps) {
311
+ for (const step of steps) {
295
312
  if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
296
313
  break;
297
314
  refsBeforeInBatchSnapshot.push(...collectFromStep(step));
@@ -318,13 +335,10 @@ function isSafeSameSnapshotFormBatchStep(step, refSnapshot) {
318
335
  return false;
319
336
  }
320
337
  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)
338
+ if (commandTokens[0] !== "batch")
325
339
  return undefined;
326
340
  let priorStepInvalidatesRefs = false;
327
- for (const step of parsed.steps) {
341
+ for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
328
342
  if ((step[0] ?? "") === "snapshot") {
329
343
  priorStepInvalidatesRefs = false;
330
344
  }
@@ -332,7 +346,7 @@ function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
332
346
  if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
333
347
  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
348
  }
335
- if (isRefInvalidatingBatchCommand(step[0]) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
349
+ if (isRefInvalidatingBatchCommand(step) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
336
350
  priorStepInvalidatesRefs = true;
337
351
  }
338
352
  }
@@ -355,7 +369,9 @@ export function buildStaleRefPreflight(options) {
355
369
  return undefined;
356
370
  if (options.refSnapshotInvalidation) {
357
371
  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.`,
372
+ message: options.refSnapshotInvalidation.reason === "page-transition"
373
+ ? `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used yet. ${options.refSnapshotInvalidation.summary}`
374
+ : `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
375
  refIds: usedRefIds,
360
376
  snapshotInvalidation: options.refSnapshotInvalidation,
361
377
  };
@@ -401,11 +417,22 @@ export function shouldPinSessionTabForCommand(options) {
401
417
  }
402
418
  export function buildPinnedBatchPlan(options) {
403
419
  if (options.command === "batch") {
420
+ const tabSelectionStep = ["tab", options.selectedTab];
421
+ // Upstream executes raw argument steps exclusively when any exist, so the
422
+ // pinned rewrite must dispatch those same steps rather than resurrecting
423
+ // caller stdin the guard never scanned.
424
+ const argumentSteps = getUpstreamEffectiveBatchSteps(options.commandTokens, undefined);
425
+ if (argumentSteps.length > 0) {
426
+ return {
427
+ includeNavigationSummary: false,
428
+ steps: [tabSelectionStep, ...argumentSteps],
429
+ unwrapMode: "user-batch",
430
+ };
431
+ }
404
432
  const parsed = parseUserBatchStdin(options.stdin);
405
433
  if (parsed.error) {
406
434
  return { error: parsed.error };
407
435
  }
408
- const tabSelectionStep = ["tab", options.selectedTab];
409
436
  return {
410
437
  includeNavigationSummary: false,
411
438
  steps: [tabSelectionStep, ...(parsed.steps ?? [])],
@@ -613,10 +640,11 @@ export function electronTargetLabel(target) {
613
640
  export function getActiveElectronRecords(records) {
614
641
  return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
615
642
  }
616
- export function findElectronLaunchRecordForSession(sessionName, records) {
643
+ export function findElectronLaunchRecordForSession(sessionName, records, namespace) {
617
644
  if (!sessionName)
618
645
  return undefined;
619
- return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
646
+ const sessionKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
647
+ return getActiveElectronRecords(records).find((record) => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === sessionKey);
620
648
  }
621
649
  function buildElectronReattachNextAction(record, liveTarget) {
622
650
  const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
@@ -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,7 +430,7 @@ 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({
433
+ const target = await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
432
434
  allowManagedSessionTarget: true,
433
435
  cwd: options.cwd,
434
436
  namespace: options.namespace,
@@ -436,9 +438,10 @@ async function collectOwnedElectronManagedSessionTarget(options) {
436
438
  signal: options.signal,
437
439
  timeoutMs: options.timeoutMs,
438
440
  })) ?? { sessionName: options.sessionName };
441
+ return { ...target, namespace: options.namespace };
439
442
  }
440
443
  catch (error) {
441
- return { error: error instanceof Error ? error.message : String(error), sessionName: options.sessionName };
444
+ return { error: error instanceof Error ? error.message : String(error), namespace: options.namespace, sessionName: options.sessionName };
442
445
  }
443
446
  }
444
447
  async function runElectronProbeCommandData(options) {
@@ -600,10 +603,10 @@ function buildElectronProbeResult(options) {
600
603
  export async function cleanupTrackedElectronHostLaunches(options) {
601
604
  const results = [];
602
605
  for (const record of options.records) {
603
- const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
606
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
604
607
  const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
605
608
  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 })
609
+ ? 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
610
  : undefined;
608
611
  const managedSessionStep = record.sessionName
609
612
  ? managedSessionCloseError
@@ -648,7 +651,7 @@ export async function cleanupActiveElectronHostLaunches(options) {
648
651
  export async function handleElectronHostInput(options) {
649
652
  const currentSessionKey = getSessionPageStateKey(options.managedSessionName, options.managedSessionNamespace) ?? options.managedSessionName;
650
653
  const preserveAttachedBrowserSession = options.attachedSessionKeys.has(currentSessionKey)
651
- || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName));
654
+ || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName));
652
655
  return await withAttachedBrowserSessionContext(preserveAttachedBrowserSession, () => handleElectronHostInputInContext(options));
653
656
  }
654
657
  async function handleElectronHostInputInContext(options) {
@@ -671,11 +674,12 @@ async function handleElectronHostInputInContext(options) {
671
674
  const managedSessions = await Promise.all(records
672
675
  .filter((record) => typeof record.sessionName === "string")
673
676
  .map((record) => {
674
- const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
677
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
675
678
  return collectOwnedElectronManagedSessionTarget({
676
679
  cwd,
677
680
  headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
678
681
  headedManagedAutosaveInterval: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveInterval,
682
+ namespace: record.namespace,
679
683
  restoreState: managedSessionRestoreState,
680
684
  sessionName: record.sessionName,
681
685
  signal,
@@ -684,7 +688,7 @@ async function handleElectronHostInputInContext(options) {
684
688
  }));
685
689
  const mismatches = managedSessions
686
690
  .map((managedSession) => {
687
- const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName);
691
+ const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName && candidate.namespace === managedSession.namespace);
688
692
  const status = record ? statuses.find((candidate) => candidate.launchId === record.launchId) : undefined;
689
693
  return record && status ? buildElectronSessionMismatch({ managedSession, record, statusTargets: status.targets }) : undefined;
690
694
  })
@@ -700,7 +704,7 @@ async function handleElectronHostInputInContext(options) {
700
704
  if (compiledElectron?.action === "probe") {
701
705
  const launchRecord = compiledElectron.launchId
702
706
  ? electronLaunchRecords.get(compiledElectron.launchId)
703
- : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords);
707
+ : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords, managedSessionNamespace);
704
708
  if (compiledElectron.launchId && !launchRecord) {
705
709
  return buildElectronHostFailureResult({
706
710
  compiledElectron: redactedCompiledElectron ?? compiledElectron,
@@ -732,7 +736,7 @@ async function handleElectronHostInputInContext(options) {
732
736
  }
733
737
  try {
734
738
  const status = launchRecord ? await inspectElectronLaunchStatus(launchRecord) : undefined;
735
- const probeNamespace = compiledElectron.launchId ? undefined : managedSessionNamespace;
739
+ const probeNamespace = compiledElectron.launchId ? launchRecord?.namespace : managedSessionNamespace;
736
740
  const pageStateKey = getSessionPageStateKey(probeSessionName, probeNamespace) ?? probeSessionName;
737
741
  const currentPageState = sessionPageState.get(pageStateKey);
738
742
  const fileAccessError = getManagedSessionStateAccessValidationError({
@@ -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,