pi-agent-browser-native 0.9.0 → 0.9.2

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 (47) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +19 -3
  3. package/dist/extensions/agent-browser/index.js +3 -107
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
  8. package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
  9. package/dist/extensions/agent-browser/lib/config.js +3 -48
  10. package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
  13. package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +4 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
  16. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +4 -3
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +6 -2
  23. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
  24. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +4 -3
  25. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +1 -1
  26. package/dist/extensions/agent-browser/lib/playbook.js +5 -4
  27. package/dist/extensions/agent-browser/lib/process.js +0 -3
  28. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
  29. package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
  30. package/dist/extensions/agent-browser/lib/results/network.js +1 -1
  31. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +7 -1
  32. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
  33. package/dist/extensions/agent-browser/lib/results/presentation/common.js +1 -0
  34. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
  35. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
  36. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
  37. package/dist/extensions/agent-browser/lib/runtime.js +4 -2
  38. package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
  39. package/dist/extensions/agent-browser/lib/web-search.js +15 -15
  40. package/docs/ARCHITECTURE.md +9 -8
  41. package/docs/COMMAND_REFERENCE.md +26 -4
  42. package/docs/RELEASE.md +1 -1
  43. package/docs/REQUIREMENTS.md +2 -1
  44. package/docs/SUPPORT_MATRIX.md +6 -3
  45. package/docs/TOOL_CONTRACT.md +12 -9
  46. package/package.json +3 -3
  47. package/scripts/doctor.mjs +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.9.2 - 2026-10-04
6
+
7
+ ### Fixed
8
+
9
+ - Treat `--engine` as launch-scoped so switching an active managed browser uses the existing fresh-session recovery instead of replacing its page in place.
10
+ - Label positively identified Lightpanda images as text-rendered in direct, native batch and selected browser-code output; retain image bytes while refusing CSS/mouse-coordinate geometry even when synthetic dimensions match.
11
+ - Omit wrapper-supplied Chrome profile/executable bootstrap defaults when CLI, environment or native config selects Lightpanda, without changing caller-selected launch settings.
12
+
13
+ ### Documentation
14
+
15
+ - Keep Chrome default and Lightpanda opt-in. Document separate installation, single-target/visual/authentication limits, native unsupported commands, CORS and telemetry; add no driver, browser bundle or automatic fallback.
16
+
17
+ ## 0.9.1 - 2026-10-02
18
+
19
+ ### Removed
20
+
21
+ - Deleted dead production code surfaced by the test audit: `getOwnedManagedSessionRestoreKey`, `parseValidBatchStepEntries`, `normalizeCommandName` (alias resolution is owned by the capability-table predicates), `AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS`, `isProjectSafeCredentialValueForProvider`, `isPlaintextCredentialValue`, the unused async `loadAgentBrowserConfig` duplicate of the sync loader, the unused `ensureAgentBrowserSocketDir` wrapper, and 46 export keywords (22 files) with no cross-module consumers. No behavior changed; owning tests now assert through the exported boundaries (`loadAgentBrowserConfigSync`, `getAgentBrowserSocketDirValidationError`, `runAgentBrowserScript`, capability predicates).
22
+
23
+ ### Changed
24
+
25
+ - Share the unchanged 300000ms code timeout ceiling between its schema and runner, and the unchanged startup measurement/250ms budget between the regression test and safe profiler (retaining the Android test's 1000ms exception). Remove the unreachable platform-smoke passthrough entry without changing facade gates.
26
+ - Move retained regressions to focused queue, artifact, input-validation, Electron host/branch, and Pi-pipeline owners. Relocate queue and concurrent artifact-merge helpers out of the extension entrypoint without changing their behavior. Consolidate required platform paths and private-file/tarball leak checks into the canonical package verifier and its existing real-tarball test. Allocate the macOS artifact alias fixture under `/tmp` so its path-alias contract does not depend on `TMPDIR`.
27
+ - Consolidate duplicate test invocations into their owning tables and delivery boundaries, preserving distinct lifecycle, redaction, recovery, and native activation checks. Split recording dependency, reservation, and batch-lifecycle assertions into named contracts. Test-only; no production behavior changed.
28
+ - Repaired 23 weak test assertions found by the test audit: literal prompt-byte expectations instead of source-derived constants, per-fixture bound expectations replacing pass-for-every-row alternations, real execution and credential-laziness probes for web-search/config, exact root-session `get url` probe matrices in passthrough coverage, complete guarded-set and key-set assertions, and anchored literal launcher/argv-delivery checks. Test-only; no production behavior changed.
29
+
30
+ ### Fixed
31
+
32
+ - Execute maintainer command-reference and startup-profile scripts correctly through symlink and space/percent-encoded paths using Node's native main-module detection.
33
+ - Keep ordinary `cookies set --curl` import paths visible in invocation echoes and failed batch diagnostics without exposing inline cookie values, secret-bearing flags or file contents.
34
+ - Host the permissioned `agent_browser_code` child with a real Node runtime from `PATH` when Pi runs as a Bun-compiled binary, and fail as `missing-binary` with a clear remediation when no Node runtime is available instead of hanging the worker handshake (#238).
35
+ - Resolve npm `.cmd` shims on Windows in `pi-agent-browser-doctor` PATH checks so installed `agent-browser` and `pi` binaries stop producing false negatives (#219).
36
+
37
+ - Host cold-boundaries, cold-resume, and destination-cancel fixture browser sockets in a short private `0700` directory (`/tmp/piab-s-…`, via `createShortPrivateSocketDir` in the test harness) so realpath-canonicalized CI temp roots cannot exceed the macOS 103-byte Unix socket path cap. Test-only; production length and privacy guards are unchanged, and the fallback keeps the previous fixture-root path on Windows or when `/tmp` is unusable.
38
+
3
39
  ## 0.9.0 - 2026-10-01
4
40
 
5
41
  ### Breaking changes
package/README.md CHANGED
@@ -321,7 +321,7 @@ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-conf
321
321
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable set "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
322
322
  ```
323
323
 
324
- Profile hints with `authenticated-only` remain advisory. Global/override profile names with `policy: "always"` and executable defaults bootstrap inactive automatic root browsers; active roots and unrelated explicit sessions retain their native settings. Configure profile/executable guidance globally, in trusted project config, or through `PI_AGENT_BROWSER_CONFIG`. Ask the agent to run `agent_browser` with `args: ["profiles"]` and `args: ["doctor"]` when profile resolution fails. The upstream `profiles` command lists Chrome profiles from Chrome's user data directory; `Default` is not canonical on every machine. Use the displayed profile directory name, a full profile/user-data directory path when upstream accepts one, or a configured `browser.executablePath` plus `sessionMode: "fresh"` for a different Chromium-compatible browser.
324
+ Profile hints with `authenticated-only` remain advisory. Global/override profile names with `policy: "always"` and executable defaults bootstrap inactive automatic root browsers only when the effective engine is Chrome; active roots and unrelated explicit sessions retain their native settings. Configure profile/executable guidance globally, in trusted project config, or through `PI_AGENT_BROWSER_CONFIG`. Ask the agent to run `agent_browser` with `args: ["profiles"]` and `args: ["doctor"]` when profile resolution fails. The upstream `profiles` command lists Chrome profiles from Chrome's user data directory; `Default` is not canonical on every machine. Use the displayed profile directory name, a full profile/user-data directory path when upstream accepts one, or a configured `browser.executablePath` plus `sessionMode: "fresh"` for a different Chromium-compatible browser.
325
325
 
326
326
  ## Common agent calls
327
327
 
@@ -338,6 +338,21 @@ Chromium launch switches belong in the value of `--args`, for example `{ "args":
338
338
 
339
339
  Local Chrome startup uses the stock binary's `--args --no-startup-window` capability to suppress Chrome's extra startup window. Headless remains the default. Caller launch arguments keep native CLI → environment → config precedence and follow this default; browsers started before this change with custom arguments may restart once when their native launch configuration changes. Ordinary follow-ups do not resend bootstrap settings. CDP, providers, Electron, and Lightpanda remain native-owned; no tabs are deleted or profiles edited by this behavior.
340
340
 
341
+ ### Opt-in Lightpanda
342
+
343
+ Chrome remains the default. Install [Lightpanda](https://github.com/lightpanda-io/browser/releases/tag/1.0.0) separately on supported macOS/Linux hosts, put `lightpanda` on `PATH`, then select it explicitly:
344
+
345
+ ```json
346
+ { "args": ["--engine", "lightpanda", "open", "https://example.com"], "sessionMode": "fresh" }
347
+ { "args": ["snapshot", "-i"] }
348
+ ```
349
+
350
+ `--engine` is launch-scoped; changing an active managed browser uses the existing fresh-session recovery. `AGENT_BROWSER_ENGINE` and native config also select the engine. Wrapper-supplied Chrome profile/executable defaults are omitted for Lightpanda; caller arguments, environment, and native config remain unchanged.
351
+
352
+ Lightpanda screenshots render page text, not graphical layout. Positively identified captures carry `imageObservations[].rendering: "text"` and unknown coordinate geometry; keep using refs, not image-derived mouse coordinates. Lightpanda is not Chrome-parity visual QA, profile login, multi-tab, or WebMCP support, and this wrapper never falls back automatically. See [capabilities and limits](docs/COMMAND_REFERENCE.md#lightpanda-engine).
353
+
354
+ ### Native navigation and interaction
355
+
341
356
  `open` without a URL uses native `get url`: it launches when needed and otherwise preserves the current page. This also applies to effective batch rows and code calls. `details.args` preserves the request, while `effectiveArgs`, command, URL data, and lifecycle describe the actual native operation. Use `open about:blank` only when you intend to navigate there. Help/version requests remain unchanged.
342
357
 
343
358
  Watch a browser window during a demo, QA run, or user-completed login by adding upstream's global `--headed` flag on the first launch. Use `sessionMode: "fresh"` if a managed session may already exist, because headed/headless state is launch-scoped. A successful first/fresh local wrapper-managed headed launch, including a launch inside `batch`, returns `details.browserWindow = { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` and one visible handoff sentence; CDP, auto-connect, provider, and Electron attachments do not. This proves that the wrapper requested and upstream launched headed mode, not that the OS window is visible on the user's display; remote, container, or virtual-display setups can still hide it. After the user finishes in the window, continue with `sessionMode: "auto"`.
@@ -505,7 +520,7 @@ Use the exact returned recovery payload after errors. Timeout partial progress r
505
520
 
506
521
  Both JSON and prose results expose a bounded canonical observation with recovery actions and artifact receipts. Oversized observations name an `observationPath` for the complete redacted result; audit-only `details` is not the model's sole recovery channel. Inner code calls avoid full model rendering, spills, and image encoding until output is selected.
507
522
 
508
- Screenshots expose measured image dimensions and capture geometry when supported by matching before/after samples. A full-page or element image is not a viewport screenshot. Native mouse coordinates are viewport CSS pixels; use the [geometry contract](docs/TOOL_CONTRACT.md#image-observations) and current scroll/frame state before converting image pixels. Unknown geometry stays unknown, including ambiguous or scrolled element captures and unmeasured batch captures. Pi image-resize notes only map sent pixels to original image pixels.
523
+ Screenshots expose measured image dimensions and capture geometry when supported by matching before/after samples. A full-page or element image is not a viewport screenshot. Native mouse coordinates are viewport CSS pixels; use the [geometry contract](docs/TOOL_CONTRACT.md#image-observations) and current scroll/frame state before converting image pixels. Unknown geometry stays unknown, including ambiguous or scrolled element captures, unmeasured batch captures, and Lightpanda text-rendered images (`rendering: "text"`). Pi image-resize notes only map sent pixels to original image pixels.
509
524
 
510
525
  Save artifacts to the user's exact paths and check `artifactVerification` before claiming they exist or are fresh. Conditional `changed: false` screenshots produce no new attachment. `outputPath` is a separate result-data destination and must not alias a screenshot, download, or recording. Close ends the browser session; explicit files remain host-owned. Recording starts remain pending until stop and receipt/file verification.
511
526
 
@@ -570,6 +585,7 @@ Use these rules:
570
585
  - Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
571
586
  - Prefer `auth save --password-stdin` over putting passwords in `args`; the wrapper only accepts caller `stdin` for `batch`, `eval --stdin`, and `auth save --password-stdin` (advanced QA and source tools generate their own batch stdin).
572
587
  - Use `state save <path>` / `state load <path>` for portable test state. `state save` is reported as a file artifact with verification metadata; if an upstream-successful artifact command reports a non-pending file path that the wrapper cannot find or did not update during this command, the tool fails with `failureCategory: "artifact-missing"` instead of treating missing/stale evidence as durable. `state load` may mention a path but is not treated as a newly saved artifact.
588
+ - `cookies set --curl <file>` treats its operand as a file path, not a cookie value: invocation echoes and failed batch diagnostics keep ordinary import paths visible. Inline cookie values and secret-bearing flags or path text still redact.
573
589
  - Treat `cookies get`, `storage local|session`, `state show`, and `auth show` output as sensitive. `state show` is presented as saved-state metadata only, and cookie/localStorage/sessionStorage values are redacted from structured details. The native presentation summarizes and redacts credential-like values while allowing benign primitive storage values to aid local QA, but avoid requesting broad dumps unless the task needs them.
574
590
  - Use `dialog status`, `dialog accept [text]`, `dialog dismiss`, and `frame <selector|main>` through native `args`; dialog commands use a shorter wrapper timeout and timed-out interactions add `inspect-dialog-after-timeout` / `dismiss-dialog-after-timeout` / fresh-session recovery actions so a blocking alert/prompt does not burn the full default watchdog. Use exact `confirm <id>` / `deny <id>` next actions for guarded-action confirmations.
575
591
 
@@ -729,7 +745,7 @@ Install upstream `agent-browser`, then install dependencies:
729
745
  npm install
730
746
  ```
731
747
 
732
- Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.1.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 1.0.0 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
748
+ Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.2.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 1.0.0 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
733
749
 
734
750
  Checkout-only extension smoke test:
735
751
 
@@ -1,4 +1,3 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { dirname, isAbsolute, join } from "node:path";
@@ -21,6 +20,8 @@ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment,
21
20
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
22
21
  import { getBrowserCwdError, resolveExecutionCwd } from "./lib/execution-cwd.js";
23
22
  import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
23
+ import { AsyncExecutionQueue, KeyedAsyncExecutionQueue } from "./lib/orchestration/execution-queue.js";
24
+ import { mergeBrowserRunArtifactManifest } from "./lib/orchestration/browser-run/artifact-merge.js";
24
25
  import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
25
26
  import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
26
27
  import { buildPromptPolicy, getLatestUserMessage, getMessageText } from "./lib/prompt-policy.js";
@@ -37,7 +38,7 @@ import { applyAgentBrowserOutputPath, canWriteAgentBrowserOutput, normalizeReque
37
38
  import { appendScriptSessionLease, createBrowserCodeOutput, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
38
39
  import { resolveBrowserExecutionIdentity, withBrowserExecutionLock, withBrowserExecutionLocks } from "./lib/managed-session-policy-lock.js";
39
40
  import { AGENT_BROWSER_INSTRUCTION_GROUP, AGENT_BROWSER_TOOL_INVENTORY, registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
40
- import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
41
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
41
42
  import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
42
43
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
43
44
  import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
@@ -647,90 +648,6 @@ function shouldSerializeBrowserCommand(options) {
647
648
  return true;
648
649
  return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
649
650
  }
650
- // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
651
- class AsyncExecutionQueue {
652
- tail = Promise.resolve();
653
- active = new AsyncLocalStorage();
654
- isCurrent() { return this.active.getStore()?.active === true; }
655
- run(work, signal, barrier = Promise.resolve()) {
656
- if (this.isCurrent()) {
657
- signal?.throwIfAborted();
658
- return work();
659
- }
660
- const previous = this.tail;
661
- let release;
662
- this.tail = new Promise((resolve) => {
663
- release = resolve;
664
- });
665
- let rejectWaiting;
666
- const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
667
- const abortWaiting = () => rejectWaiting(signal?.reason);
668
- signal?.addEventListener("abort", abortWaiting, { once: true });
669
- if (signal?.aborted)
670
- abortWaiting();
671
- const execution = (async () => {
672
- await Promise.all([previous, barrier]);
673
- signal?.removeEventListener("abort", abortWaiting);
674
- try {
675
- signal?.throwIfAborted();
676
- const scope = { active: true };
677
- try {
678
- return await this.active.run(scope, work);
679
- }
680
- finally {
681
- scope.active = false;
682
- }
683
- }
684
- finally {
685
- release();
686
- }
687
- })();
688
- return signal ? Promise.race([execution, cancelled]) : execution;
689
- }
690
- }
691
- export class KeyedAsyncExecutionQueue {
692
- barriers = new Map();
693
- entries = new Map();
694
- async run(key, namespace, work, signal) {
695
- const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
696
- if (entry.queue.isCurrent())
697
- return work();
698
- const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
699
- entry.users += 1;
700
- this.entries.set(key, entry);
701
- try {
702
- return await entry.queue.run(work, signal, barrier);
703
- }
704
- finally {
705
- entry.users -= 1;
706
- if (entry.users === 0 && this.entries.get(key) === entry)
707
- this.entries.delete(key);
708
- }
709
- }
710
- async runExclusive(namespace, work) {
711
- const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
712
- const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
713
- let release;
714
- const blocked = new Promise((resolve) => {
715
- release = resolve;
716
- });
717
- const barrier = previous.then(() => blocked);
718
- this.barriers.set(namespaceKey, barrier);
719
- const drains = [...this.entries]
720
- .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
721
- .map(([, { queue }]) => queue.run(async () => undefined));
722
- await previous;
723
- await Promise.all(drains);
724
- try {
725
- return await work();
726
- }
727
- finally {
728
- release();
729
- if (this.barriers.get(namespaceKey) === barrier)
730
- this.barriers.delete(namespaceKey);
731
- }
732
- }
733
- }
734
651
  function mergeBrowserRunMap(current, initial, updated) {
735
652
  if (updated === initial)
736
653
  return current;
@@ -745,27 +662,6 @@ function mergeBrowserRunMap(current, initial, updated) {
745
662
  }
746
663
  return merged;
747
664
  }
748
- export function mergeBrowserRunArtifactManifest(current, initial, updated) {
749
- if (!updated || updated === initial)
750
- return current;
751
- if (current === initial)
752
- return updated;
753
- const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
754
- const changedEntries = updated.entries
755
- .map((entry, index) => ({ entry, index }))
756
- .filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
757
- .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
758
- || Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
759
- || left.index - right.index)
760
- .map(({ entry }) => entry);
761
- return changedEntries.length === 0
762
- ? current
763
- : mergeSessionArtifactManifest({
764
- base: current,
765
- entries: changedEntries,
766
- nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
767
- });
768
- }
769
665
  function findPackageRoot(startDir) {
770
666
  let currentDir = startDir;
771
667
  while (true) {
@@ -76,7 +76,7 @@ function getOpenCommandTarget(commandTokens) {
76
76
  }
77
77
  return undefined;
78
78
  }
79
- export function parseCommandInfoFromTokens(commandTokens) {
79
+ function parseCommandInfoFromTokens(commandTokens) {
80
80
  const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
81
81
  const command = upstreamCommandTokens[0];
82
82
  return {
@@ -38,7 +38,7 @@ export const GLOBAL_VALUE_FLAGS = [
38
38
  "--idle-timeout",
39
39
  "--input-mode",
40
40
  ];
41
- export const COMMAND_VALUE_FLAGS = [
41
+ const COMMAND_VALUE_FLAGS = [
42
42
  "--allowed-origins",
43
43
  "--baseline",
44
44
  "--body",
@@ -79,7 +79,7 @@ export const COMMAND_VALUE_FLAGS = [
79
79
  "--password",
80
80
  "--wait-until",
81
81
  ];
82
- export const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
82
+ const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
83
83
  export const VALUE_FLAGS = new Set([...GLOBAL_VALUE_FLAGS, ...COMMAND_VALUE_FLAGS]);
84
84
  export const PREVALIDATED_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
85
85
  export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args", "--session"]);
@@ -246,7 +246,7 @@ export function extractRequestedRestoreKey(args, sessionName, envValue) {
246
246
  }
247
247
  return restoreKey;
248
248
  }
249
- export function getFlagName(token) {
249
+ function getFlagName(token) {
250
250
  return token.split("=", 1)[0] ?? token;
251
251
  }
252
252
  export function isNonFlagToken(token) {
@@ -4,7 +4,7 @@ import { buildPageTransitionRefSnapshotInvalidation, normalizeSessionTabTarget,
4
4
  import { getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isSessionArtifactManifest } from "./results/artifact-manifest.js";
5
5
  import { extractAgentBrowserLifecycle } from "./results/presentation/common.js";
6
6
  export const BROWSER_TRANSITION_ENTRY = "agent-browser-transition";
7
- export const BROWSER_EVENT_VERSION = 1;
7
+ const BROWSER_EVENT_VERSION = 1;
8
8
  export const BROWSER_RESULT_TOOLS = new Set([
9
9
  "agent_browser", "agent_browser_code", "agent_browser_action", "agent_browser_qa", "agent_browser_electron",
10
10
  "agent_browser_source", "agent_browser_network_source",
@@ -295,9 +295,6 @@ function getCommandCapability(command) {
295
295
  function hasCommandCapability(command, capability) {
296
296
  return getCommandCapability(command)?.[capability] === true;
297
297
  }
298
- export function normalizeCommandName(command) {
299
- return getCommandCapability(command)?.command ?? command;
300
- }
301
298
  export function isCloseCommand(command) {
302
299
  return hasCommandCapability(command, "closesSession");
303
300
  }
@@ -229,19 +229,6 @@ function validateBrowserDefaultProfile(value, path, errors) {
229
229
  }
230
230
  return { name, policy: /** @type {BrowserDefaultProfilePolicy} */ (policy) };
231
231
  }
232
- /** @param {string} rawValue */
233
- export function isPlaintextCredentialValue(rawValue) {
234
- const trimmed = rawValue.trim();
235
- return Boolean(trimmed) && !trimmed.startsWith("!") && !trimmed.startsWith("$");
236
- }
237
- /**
238
- * @param {string} rawValue
239
- * @param {WebSearchProvider} provider
240
- */
241
- export function isProjectSafeCredentialValueForProvider(rawValue, provider) {
242
- void provider;
243
- return rawValue.trim().length > 0;
244
- }
245
232
  /**
246
233
  * @param {unknown} value
247
234
  * @param {string} path
@@ -1,53 +1,8 @@
1
1
  import { exec as execCallback } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
3
2
  import { promisify } from "node:util";
4
- import { SECRET_COMMAND_TIMEOUT_MS, buildAgentBrowserConfigState, getAgentBrowserConfigPaths, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, } from "./config-policy.js";
5
- export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isPlaintextCredentialValue, isProjectSafeCredentialValueForProvider, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
3
+ import { SECRET_COMMAND_TIMEOUT_MS, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, resolveEnvInterpolations, } from "./config-policy.js";
4
+ export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
6
5
  const exec = promisify(execCallback);
7
- async function readConfigLayer(path, scope, errors, warnings) {
8
- let raw;
9
- try {
10
- raw = await readFile(path, "utf8");
11
- }
12
- catch (error) {
13
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
14
- return undefined;
15
- }
16
- errors.push(`Could not read ${scope} config ${path}: ${error instanceof Error ? error.message : String(error)}`);
17
- return undefined;
18
- }
19
- return parseAgentBrowserConfigLayer(raw, path, scope, errors, warnings);
20
- }
21
- export async function loadAgentBrowserConfig(options = {}) {
22
- const env = options.env ?? process.env;
23
- const paths = getAgentBrowserConfigPaths({ cwd: options.cwd, env });
24
- const includeProjectConfig = options.includeProjectConfig !== false;
25
- const errors = [];
26
- const warnings = [];
27
- const layerCandidates = [
28
- { path: paths.global, scope: "global" },
29
- ...(includeProjectConfig ? [{ path: paths.project, scope: "project" }] : []),
30
- ...(paths.override ? [{ path: paths.override, scope: "override" }] : []),
31
- ];
32
- const layers = [];
33
- let mergedConfig = {};
34
- for (const candidate of layerCandidates) {
35
- const layer = await readConfigLayer(candidate.path, candidate.scope, errors, warnings);
36
- if (!layer)
37
- continue;
38
- layers.push(layer);
39
- mergedConfig = mergeAgentBrowserConfig(mergedConfig, layer.config);
40
- }
41
- return buildAgentBrowserConfigState({
42
- env,
43
- errors,
44
- layers,
45
- mergedConfig,
46
- paths,
47
- projectConfigIncluded: includeProjectConfig,
48
- warnings,
49
- });
50
- }
51
6
  export function loadAgentBrowserConfigSync(options = {}) {
52
7
  return loadAgentBrowserConfigStateSync(options);
53
8
  }
@@ -70,7 +25,7 @@ async function resolveCommandCredential(rawValue, signal) {
70
25
  throw new Error("Credential command failed without exposing command output. Check pi-agent-browser-config web-search status and the configured secret manager command.");
71
26
  }
72
27
  }
73
- export async function resolveCredentialSource(source, options = {}) {
28
+ async function resolveCredentialSource(source, options = {}) {
74
29
  if (!source)
75
30
  return undefined;
76
31
  let value;
@@ -6,8 +6,8 @@ import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
6
6
  import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
7
7
  import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
8
8
  export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
9
- export const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
10
- export const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
9
+ const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
10
+ const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
11
11
  const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
12
12
  export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
13
13
  const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"];
@@ -46,7 +46,7 @@ function policyEntryMatchesApp(entry, app) {
46
46
  return false;
47
47
  return appIdentifiers(app).some((identifier) => identifier.toLowerCase().includes(normalizedEntry));
48
48
  }
49
- export function evaluateElectronLaunchPolicy(options) {
49
+ function evaluateElectronLaunchPolicy(options) {
50
50
  const denyEntry = options.deny?.find((entry) => policyEntryMatchesApp(entry, options.target));
51
51
  if (denyEntry) {
52
52
  return {
@@ -66,7 +66,7 @@ export function evaluateElectronLaunchPolicy(options) {
66
66
  }
67
67
  return undefined;
68
68
  }
69
- export async function resolveElectronLaunchTarget(options) {
69
+ async function resolveElectronLaunchTarget(options) {
70
70
  if (options.appPath)
71
71
  return inspectElectronAppPath(options.appPath);
72
72
  if (options.executablePath)
@@ -1,9 +1,8 @@
1
1
  import { JsonSchema } from "../json-schema.js";
2
2
  import { StringEnum as localStringEnum } from "../string-enum-schema.js";
3
3
  import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS } from "../electron/discovery.js";
4
- import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
5
- export const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
6
- export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
4
+ import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
5
+ const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
7
6
  function outputProperties(Type) {
8
7
  return {
9
8
  outputPath: Type.Optional(Type.String({ description: "Result-data path, separate from browser artifact paths.", minLength: 1 })),
@@ -2,20 +2,34 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import which from "which";
5
6
  import { isRecord } from "../parsing.js";
6
7
  import { extractExplicitNamespace, extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
7
8
  import { getUpstreamEffectiveBatchSteps } from "../orchestration/batch-stdin.js";
8
9
  import { extractUpstreamCommandTokens } from "../argv-descriptor.js";
9
10
  import { isCloseAllCommand } from "../command-taxonomy.js";
10
11
  import { redactSensitiveText, validateToolArgs } from "../runtime.js";
12
+ import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS } from "./types.js";
11
13
  export const AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES = 64 * 1_024;
12
14
  export const AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS = 120_000;
13
15
  export const AGENT_BROWSER_SCRIPT_NAMESPACE = "";
14
- export const AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS = 300_000;
15
16
  export const AGENT_BROWSER_SCRIPT_MAX_CALLS = 25;
16
17
  export const AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES = 64 * 1_024;
17
18
  export const AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES = 1 * 1_024 * 1_024;
18
- export const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
19
+ const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
20
+ /**
21
+ * Resolve the Node runtime that hosts the permissioned code child. A Bun-compiled Pi binary
22
+ * points `process.execPath` at the pi executable, which rejects Node-only flags like
23
+ * `--permission`, so those hosts must spawn a real `node` from PATH instead.
24
+ */
25
+ export function resolveScriptChildNodePath(options) {
26
+ if (options.runtime.bun === undefined)
27
+ return options.execPath;
28
+ const nodePath = options.whichNode();
29
+ if (nodePath === null)
30
+ throw new Error("agent_browser_code requires a `node` runtime on PATH when pi runs on a Bun binary.");
31
+ return nodePath;
32
+ }
19
33
  function findPackageRoot(startDir) {
20
34
  let currentDir = startDir;
21
35
  for (;;) {
@@ -34,7 +48,7 @@ function resolveScriptWorkerPath() {
34
48
  return workerPath;
35
49
  }
36
50
  const SCRIPT_SESSION_NAME_PATTERN = /^piab-script-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
37
- export function validateAgentBrowserScriptSource(input) {
51
+ function validateAgentBrowserScriptSource(input) {
38
52
  if (typeof input !== "string")
39
53
  return { error: "script must be a string." };
40
54
  const bytes = Buffer.byteLength(input, "utf8");
@@ -175,8 +189,8 @@ export async function runAgentBrowserScript(options) {
175
189
  return buildFailedRun({ callCount: 0, emitCount: 0, error: compiled.error, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
176
190
  }
177
191
  const timeoutMs = options.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
178
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS) {
179
- return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS}.`, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
192
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > AGENT_BROWSER_CODE_MAX_TIMEOUT_MS) {
193
+ return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${AGENT_BROWSER_CODE_MAX_TIMEOUT_MS}.`, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
180
194
  }
181
195
  if (options.signal?.aborted) {
182
196
  return buildFailedRun({ aborted: true, callCount: 0, emitCount: 0, error: "Script execution was aborted.", failureCategory: "aborted", rejectedCallCount: 0, steps: [] });
@@ -189,7 +203,15 @@ export async function runAgentBrowserScript(options) {
189
203
  const message = error instanceof Error ? error.message : "Compiled script worker is missing.";
190
204
  return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
191
205
  }
192
- const child = spawn(process.execPath, [
206
+ let childNodePath;
207
+ try {
208
+ childNodePath = resolveScriptChildNodePath({ runtime: { bun: process.versions.bun }, execPath: process.execPath, whichNode: () => which.sync("node", { nothrow: true }) });
209
+ }
210
+ catch (error) {
211
+ const message = error instanceof Error ? error.message : "No Node runtime is available for the code child.";
212
+ return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
213
+ }
214
+ const child = spawn(childNodePath, [
193
215
  "--permission",
194
216
  "--max-old-space-size=64",
195
217
  workerPath,
@@ -1,8 +1,8 @@
1
1
  export const DEFAULT_SESSION_MODE = "auto";
2
+ export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
2
3
  export const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select"];
3
4
  export const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"];
4
- export const AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS = 200;
5
- export const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
5
+ const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
6
6
  export const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"];
7
7
  export const AGENT_BROWSER_ELECTRON_ACTIONS = ["list", "launch", "status", "cleanup", "probe"];
8
8
  export const AGENT_BROWSER_ELECTRON_HANDOFFS = ["connect", "tabs", "snapshot"];
@@ -29,6 +29,10 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
29
29
  flag: "--enable",
30
30
  reason: "selects built-in page init scripts before the upstream browser session is launched",
31
31
  },
32
+ {
33
+ flag: "--engine",
34
+ reason: "selects the browser engine used for the upstream launch",
35
+ },
32
36
  {
33
37
  flag: "--executable-path",
34
38
  reason: "selects the browser executable used for the upstream launch",
@@ -82,9 +82,6 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
82
82
  export async function withOwnedManagedSessionContext(context, run) {
83
83
  return await ownedManagedSessionStorage.run(context, run);
84
84
  }
85
- export function getOwnedManagedSessionRestoreKey() {
86
- return ownedManagedSessionStorage.getStore()?.restoreKey;
87
- }
88
85
  export function resolveOwnedManagedSessionContext(options) {
89
86
  const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
90
87
  const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
@@ -115,7 +112,7 @@ function hasExplicitConfigArg(args) {
115
112
  function closesBrowserSession(args) {
116
113
  return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
117
114
  }
118
- export function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
115
+ function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
119
116
  return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
120
117
  }
121
118
  /** Caller-selected upstream config disables the wrapper's automatic restore injection without blocking that config. */
@@ -139,12 +139,3 @@ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
139
139
  return argumentSteps;
140
140
  return parseUserBatchStdin(stdin).steps ?? [];
141
141
  }
142
- export function parseValidBatchStepEntries(stdin) {
143
- const parsed = parseBatchStdinJsonArray(stdin);
144
- if (parsed.error || parsed.steps === undefined)
145
- return [];
146
- return parsed.steps.flatMap((step, index) => {
147
- const validated = validateUserBatchStep(step, index);
148
- return validated.ok ? [{ index, step: validated.step }] : [];
149
- });
150
- }
@@ -0,0 +1,22 @@
1
+ import { getSessionArtifactManifestEntryKey, isPendingRecordingCommand, mergeSessionArtifactManifest } from "../../results/artifact-manifest.js";
2
+ export function mergeBrowserRunArtifactManifest(current, initial, updated) {
3
+ if (!updated || updated === initial)
4
+ return current;
5
+ if (current === initial)
6
+ return updated;
7
+ const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
8
+ const changedEntries = updated.entries
9
+ .map((entry, index) => ({ entry, index }))
10
+ .filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
11
+ .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
12
+ || Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
13
+ || left.index - right.index)
14
+ .map(({ entry }) => entry);
15
+ return changedEntries.length === 0
16
+ ? current
17
+ : mergeSessionArtifactManifest({
18
+ base: current,
19
+ entries: changedEntries,
20
+ nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
21
+ });
22
+ }
@@ -566,7 +566,7 @@ export function formatQaAttachedTargetText(target) {
566
566
  return undefined;
567
567
  return ["QA attached target:", target.sessionName, target.title, target.url].filter((part) => typeof part === "string" && part.length > 0).join(" — ");
568
568
  }
569
- export function buildQaAttachedRecoveryNextActions(sessionName) {
569
+ function buildQaAttachedRecoveryNextActions(sessionName) {
570
570
  const sessionArgs = (args) => withOptionalSessionArgs(sessionName, args);
571
571
  return [
572
572
  buildNextToolAction({
@@ -26,13 +26,13 @@ export function buildMissingBinaryMessage() {
26
26
  ].join("\n");
27
27
  }
28
28
  const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set(["try-button-name-candidate", "try-link-name-candidate"]);
29
- export function formatSemanticActionCandidateText(actions) {
29
+ function formatSemanticActionCandidateText(actions) {
30
30
  const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
31
31
  if (candidateActions.length === 0)
32
32
  return undefined;
33
33
  return ["Agent-browser candidate fallbacks:", ...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`)].join("\n");
34
34
  }
35
- export function buildSemanticActionCandidateActions(compiled) {
35
+ function buildSemanticActionCandidateActions(compiled) {
36
36
  const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
37
37
  if (commandIndex < 0 || compiled.args[commandIndex] !== "find")
38
38
  return [];