pi-agent-browser-native 0.9.0 → 0.9.1

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 (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  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/managed-session-restore.js +1 -4
  15. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
  20. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
  21. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +1 -1
  22. package/dist/extensions/agent-browser/lib/process.js +0 -3
  23. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
  24. package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
  25. package/dist/extensions/agent-browser/lib/results/network.js +1 -1
  26. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
  27. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
  28. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
  29. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
  30. package/dist/extensions/agent-browser/lib/runtime.js +4 -2
  31. package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
  32. package/dist/extensions/agent-browser/lib/web-search.js +15 -15
  33. package/docs/ARCHITECTURE.md +4 -4
  34. package/docs/COMMAND_REFERENCE.md +2 -0
  35. package/docs/RELEASE.md +1 -1
  36. package/docs/SUPPORT_MATRIX.md +3 -3
  37. package/docs/TOOL_CONTRACT.md +3 -3
  38. package/package.json +3 -3
  39. package/scripts/doctor.mjs +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.9.1 - 2026-10-02
6
+
7
+ ### Removed
8
+
9
+ - 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).
10
+
11
+ ### Changed
12
+
13
+ - 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.
14
+ - 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`.
15
+ - 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.
16
+ - 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.
17
+
18
+ ### Fixed
19
+
20
+ - Execute maintainer command-reference and startup-profile scripts correctly through symlink and space/percent-encoded paths using Node's native main-module detection.
21
+ - 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.
22
+ - 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).
23
+ - 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).
24
+
25
+ - 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.
26
+
3
27
  ## 0.9.0 - 2026-10-01
4
28
 
5
29
  ### Breaking changes
package/README.md CHANGED
@@ -570,6 +570,7 @@ Use these rules:
570
570
  - Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
571
571
  - 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
572
  - 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.
573
+ - `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
574
  - 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
575
  - 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
576
 
@@ -729,7 +730,7 @@ Install upstream `agent-browser`, then install dependencies:
729
730
  npm install
730
731
  ```
731
732
 
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`.
733
+ 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
734
 
734
735
  Checkout-only extension smoke test:
735
736
 
@@ -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"];
@@ -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 [];
@@ -333,7 +333,7 @@ function resolveSemanticActionVisibleRefArgsFromSnapshot(compiled, snapshotData)
333
333
  return undefined;
334
334
  return { args: [...getCompiledSemanticActionSessionPrefix(compiled), ...resolution.args], snapshot: resolution.snapshot };
335
335
  }
336
- export async function resolveSemanticActionVisibleRefArgs(options) {
336
+ async function resolveSemanticActionVisibleRefArgs(options) {
337
337
  if (!options.compiled || !options.sessionName)
338
338
  return undefined;
339
339
  const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
@@ -0,0 +1,86 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../argv-grammar.js";
3
+ // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
4
+ export class AsyncExecutionQueue {
5
+ tail = Promise.resolve();
6
+ active = new AsyncLocalStorage();
7
+ isCurrent() { return this.active.getStore()?.active === true; }
8
+ run(work, signal, barrier = Promise.resolve()) {
9
+ if (this.isCurrent()) {
10
+ signal?.throwIfAborted();
11
+ return work();
12
+ }
13
+ const previous = this.tail;
14
+ let release;
15
+ this.tail = new Promise((resolve) => {
16
+ release = resolve;
17
+ });
18
+ let rejectWaiting;
19
+ const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
20
+ const abortWaiting = () => rejectWaiting(signal?.reason);
21
+ signal?.addEventListener("abort", abortWaiting, { once: true });
22
+ if (signal?.aborted)
23
+ abortWaiting();
24
+ const execution = (async () => {
25
+ await Promise.all([previous, barrier]);
26
+ signal?.removeEventListener("abort", abortWaiting);
27
+ try {
28
+ signal?.throwIfAborted();
29
+ const scope = { active: true };
30
+ try {
31
+ return await this.active.run(scope, work);
32
+ }
33
+ finally {
34
+ scope.active = false;
35
+ }
36
+ }
37
+ finally {
38
+ release();
39
+ }
40
+ })();
41
+ return signal ? Promise.race([execution, cancelled]) : execution;
42
+ }
43
+ }
44
+ export class KeyedAsyncExecutionQueue {
45
+ barriers = new Map();
46
+ entries = new Map();
47
+ async run(key, namespace, work, signal) {
48
+ const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
49
+ if (entry.queue.isCurrent())
50
+ return work();
51
+ const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
52
+ entry.users += 1;
53
+ this.entries.set(key, entry);
54
+ try {
55
+ return await entry.queue.run(work, signal, barrier);
56
+ }
57
+ finally {
58
+ entry.users -= 1;
59
+ if (entry.users === 0 && this.entries.get(key) === entry)
60
+ this.entries.delete(key);
61
+ }
62
+ }
63
+ async runExclusive(namespace, work) {
64
+ const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
65
+ const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
66
+ let release;
67
+ const blocked = new Promise((resolve) => {
68
+ release = resolve;
69
+ });
70
+ const barrier = previous.then(() => blocked);
71
+ this.barriers.set(namespaceKey, barrier);
72
+ const drains = [...this.entries]
73
+ .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
74
+ .map(([, { queue }]) => queue.run(async () => undefined));
75
+ await previous;
76
+ await Promise.all(drains);
77
+ try {
78
+ return await work();
79
+ }
80
+ finally {
81
+ release();
82
+ if (this.barriers.get(namespaceKey) === barrier)
83
+ this.barriers.delete(namespaceKey);
84
+ }
85
+ }
86
+ }
@@ -157,7 +157,7 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
157
157
  }
158
158
  return text;
159
159
  }
160
- export function formatAgentBrowserRenderResult(result, options, theme, isError) {
160
+ function formatAgentBrowserRenderResult(result, options, theme, isError) {
161
161
  if (options.isPartial) {
162
162
  return theme.fg("warning", "Running agent-browser...");
163
163
  }
@@ -290,9 +290,6 @@ export async function getAgentBrowserSocketDirValidationError(socketDir, uid = t
290
290
  return `the directory could not be inspected (${error.code ?? "unknown error"})`;
291
291
  }
292
292
  }
293
- export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
294
- return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
295
- }
296
293
  export function getAgentBrowserSocketPathValidationError(options) {
297
294
  if ((options.platform ?? processPlatform) === "win32")
298
295
  return undefined;
@@ -7,8 +7,8 @@ export function isPendingRecordingArtifact(artifact) {
7
7
  return artifact.recordingState === "openRecording" || artifact.status === "pending"
8
8
  || (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
9
9
  }
10
- export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
- export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
10
+ const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
+ const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
12
12
  export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
13
13
  function parsePositiveSafeInteger(value) {
14
14
  if (value === undefined)
@@ -31,7 +31,7 @@ function getRouteDiagnosticReason(item, route) {
31
31
  return "unfulfilled-routed-request";
32
32
  return undefined;
33
33
  }
34
- export function getNetworkRouteMode(args) {
34
+ function getNetworkRouteMode(args) {
35
35
  if (args.includes("--abort"))
36
36
  return "abort";
37
37
  if (args.includes("--body"))
@@ -3,7 +3,7 @@ export function getStringRecordField(value, key) {
3
3
  const field = value[key];
4
4
  return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
5
5
  }
6
- export function getNetworkRequestUrlPath(url) {
6
+ function getNetworkRequestUrlPath(url) {
7
7
  if (!url)
8
8
  return undefined;
9
9
  try {
@@ -68,7 +68,7 @@ function getStatefulCommandSensitiveValues(command) {
68
68
  return [];
69
69
  const tokens = extractUpstreamCommandTokens(command);
70
70
  const values = [];
71
- if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
71
+ if (tokens[0] === "cookies" && tokens[1] === "set" && !tokens.slice(2).includes("--curl") && tokens[3])
72
72
  values.push(tokens[3]);
73
73
  if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4])
74
74
  values.push(tokens[4]);
@@ -507,7 +507,7 @@ export function formatNetworkRouteDiagnosticsText(diagnostics) {
507
507
  lines.push("If this route is intended as a mock, inspect the request/headers and treat failed, pending, or CORS-looking rows as unfulfilled until a mocked response is observed.");
508
508
  return lines.join("\n");
509
509
  }
510
- export function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
510
+ function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
511
511
  const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
512
512
  if (!diagnostic)
513
513
  return undefined;
@@ -19,7 +19,7 @@ function formatSemanticActionTarget(compiled) {
19
19
  const target = `${locator} ${quotedValue}`;
20
20
  return name ? `${target} (name ${JSON.stringify(name)})` : target;
21
21
  }
22
- export function formatSemanticActionCompactLine(compiled) {
22
+ function formatSemanticActionCompactLine(compiled) {
23
23
  const target = formatSemanticActionTarget(compiled);
24
24
  switch (compiled.action) {
25
25
  case "click":
@@ -34,7 +34,7 @@ export function formatSemanticActionCompactLine(compiled) {
34
34
  return `${compiled.action}: ${target}`;
35
35
  }
36
36
  }
37
- export function resolveSemanticPresentationCommand(compiled) {
37
+ function resolveSemanticPresentationCommand(compiled) {
38
38
  if (!compiled || !SEMANTIC_PRESENTATION_ACTIONS.has(compiled.action))
39
39
  return undefined;
40
40
  if (compiled.action === "select")
@@ -256,6 +256,6 @@ export function formatRichInputRecoveryText(diagnostic) {
256
256
  `- ${diagnostic.inputMethodHint}`,
257
257
  ].join("\n");
258
258
  }
259
- export function normalizeSemanticActionAccessibleName(name) {
259
+ function normalizeSemanticActionAccessibleName(name) {
260
260
  return name.replace(/\s+/g, " ").trim().toLowerCase();
261
261
  }
@@ -254,7 +254,7 @@ function redactBearerCredentials(text) {
254
254
  return formatRedactedCredential(label, credential, trailing);
255
255
  });
256
256
  }
257
- export function isSensitiveFieldName(key) {
257
+ function isSensitiveFieldName(key) {
258
258
  SENSITIVE_FIELD_NAME_PATTERN.lastIndex = 0;
259
259
  return SENSITIVE_FIELD_NAME_PATTERN.test(key);
260
260
  }
@@ -347,7 +347,9 @@ export function redactInvocationArgs(args) {
347
347
  }
348
348
  }
349
349
  }
350
- if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set" && redacted[commandStartIndex + 3] !== undefined) {
350
+ if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set"
351
+ && !extractUpstreamCommandTokens(args).slice(2).includes("--curl")
352
+ && redacted[commandStartIndex + 3] !== undefined) {
351
353
  redacted[commandStartIndex + 3] = "[REDACTED]";
352
354
  }
353
355
  if (commandStartIndex !== undefined
@@ -62,7 +62,7 @@ function extractStringResultField(data, fieldName) {
62
62
  const text = data[fieldName].trim();
63
63
  return text.length > 0 ? text : undefined;
64
64
  }
65
- export function extractSessionTabTargetFromData(data) {
65
+ function extractSessionTabTargetFromData(data) {
66
66
  const directTarget = normalizeSessionTabTarget({
67
67
  targetId: isRecord(data) && typeof data.targetId === "string" ? data.targetId : undefined,
68
68
  title: extractStringResultField(data, "title"),
@@ -5,20 +5,20 @@ import { redactSensitiveText } from "./runtime.js";
5
5
  import { StringEnum as localStringEnum } from "./string-enum-schema.js";
6
6
  import { DEFAULT_WEB_SEARCH_PROVIDER, EXA_SEARCH_TYPES, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
7
7
  export const AGENT_BROWSER_WEB_SEARCH_TOOL_NAME = "agent_browser_web_search";
8
- export const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
9
- export const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
10
- export const DEFAULT_SEARCH_RESULT_COUNT = 5;
11
- export const MAX_SEARCH_RESULT_COUNT = 10;
12
- export const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
13
- export const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
14
- export const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
15
- export const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
16
- export const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
8
+ const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
9
+ const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
10
+ const DEFAULT_SEARCH_RESULT_COUNT = 5;
11
+ const MAX_SEARCH_RESULT_COUNT = 10;
12
+ const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
13
+ const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
14
+ const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
15
+ const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
16
+ const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
17
17
  export const WEB_SEARCH_MIN_REQUEST_INTERVAL_MS = 1_100;
18
18
  export const EXA_SEARCH_SYSTEM_PROMPT = "Prefer primary, official sources. Respect any requested version or date. Avoid duplicate or equivalent results.";
19
19
  export { EXA_SEARCH_TYPES };
20
- export const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
21
- export const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
20
+ const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
21
+ const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
22
22
  const MAX_EXA_DOMAIN_FILTERS = 20;
23
23
  const MAX_EXA_ADDITIONAL_QUERIES = 10;
24
24
  export function createAgentBrowserWebSearchParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
@@ -193,7 +193,7 @@ export function cleanSearchText(value, maxLength = 500) {
193
193
  return cleaned;
194
194
  return `${cleaned.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
195
195
  }
196
- export function normalizeSearchUrl(value) {
196
+ function normalizeSearchUrl(value) {
197
197
  if (typeof value !== "string")
198
198
  return undefined;
199
199
  try {
@@ -258,7 +258,7 @@ export function normalizeExaSearchResult(result) {
258
258
  function getProviderLabel(provider) {
259
259
  return provider === "exa" ? "Exa" : "Brave";
260
260
  }
261
- export function formatSearchResults(provider, query, results) {
261
+ function formatSearchResults(provider, query, results) {
262
262
  const providerLabel = getProviderLabel(provider);
263
263
  if (results.length === 0) {
264
264
  return `No ${providerLabel} web results found for: ${query}`;
@@ -538,14 +538,14 @@ const EXA_WEB_SEARCH_ADAPTER = {
538
538
  };
539
539
  },
540
540
  };
541
- export const WEB_SEARCH_PROVIDER_ADAPTERS = {
541
+ const WEB_SEARCH_PROVIDER_ADAPTERS = {
542
542
  exa: EXA_WEB_SEARCH_ADAPTER,
543
543
  brave: BRAVE_WEB_SEARCH_ADAPTER,
544
544
  };
545
545
  export function getWebSearchProviderAdapter(provider) {
546
546
  return WEB_SEARCH_PROVIDER_ADAPTERS[provider];
547
547
  }
548
- export function dedupeSearchResults(results) {
548
+ function dedupeSearchResults(results) {
549
549
  const seen = new Set();
550
550
  return results.filter((result) => {
551
551
  if (seen.has(result.url))
@@ -46,15 +46,15 @@ The extension should:
46
46
 
47
47
  The child has an empty environment, Node permission mode without host capability allowlists, a 64 MiB heap ceiling, and a VM with string/WebAssembly code generation disabled. Null-prototype `browser`, `emit`, and `emitImage` functions use JSON-only IPC; parent promises/objects/functions are not exposed. There are no imports or host filesystem/network/process/timer APIs. Node's VM alone is not a security boundary. The retained child isolation is distinct from the selected browser's persistent authentication and native capabilities; no QuickJS, broker, second driver, or provider payload injection is involved.
48
48
 
49
- Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
49
+ Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `lib/input-modes/types.ts` owns the maximum shared by the public code schema and runner. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
50
50
 
51
51
  ### Ordered transitions and shared execution
52
52
 
53
- `lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier. Existing local queues and branch-generation checks still protect in-process ownership/replay. Independent identities remain concurrent. Direct calls bound coordination waits using the existing configured/command-aware timeout, then retain their native subprocess and Electron action watchdogs. Code owns a whole-cell deadline, including local queue waits. Independently bounded cleanup can borrow an already-held covered claim after cancellation, but cannot acquire a new identity or release the outer claim.
53
+ `lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier. `lib/orchestration/execution-queue.ts` owns the existing local managed/artifact queues and keyed namespace barriers; branch-generation checks still protect in-process ownership/replay. Independent identities remain concurrent. Direct calls bound coordination waits using the existing configured/command-aware timeout, then retain their native subprocess and Electron action watchdogs. Code owns a whole-cell deadline, including local queue waits. Independently bounded cleanup can borrow an already-held covered claim after cancellation, but cannot acquire a new identity or release the outer claim.
54
54
 
55
55
  All participating Pi processes must restart with the updated package. The lock is not a browser transaction, rollback, or a guarantee against humans, third-party native clients, older extensions, or separately named sessions attached to one external Chrome. Separate tool calls still need workflow coordination.
56
56
 
57
- `lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
57
+ `lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. `lib/orchestration/browser-run/artifact-merge.ts` preserves concurrent three-way manifest updates and recording lifecycle order. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
58
58
 
59
59
  `lib/browser-journal.ts` token-projects published envelopes and ancestry through public Pi file/header/leaf APIs, loading only winning snapshot definitions. It captures the selected leaf before asynchronous reads and validates that same branch through public `getEntry()` ancestry on both hosts. Capture-local admission reuses a validated append-only prefix while checking endpoint identity/parent and new suffixes; publication/durability checks remain independent. Startup/tree restoration uses one queued captured replay for branch state and cleanup leases, with one per-session launch-configuration fold. Stock official 1.0.0 still eagerly loads native history before extension startup. Legacy adapters live only in `lib/browser-session-conversion.ts`, which publishes a validated stopped separate copy while retaining the source. Native generation checks use PID/start and socket identity, never saved URL or delta revision alone. Filtered/helper captures share this check. Small owner-qualified launch receipts retain observed restore-key provenance; restart verifies the current native generation and key before treating that provenance as live. Full storage, migration and consumer-memory boundaries are documented in [`SESSION_STORAGE.md`](SESSION_STORAGE.md).
60
60
 
@@ -266,7 +266,7 @@ Upstream restore-state persistence remains upstream-owned. The wrapper passes an
266
266
  - subprocess execution and JSON parsing through `buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`: copies the parent process environment so user-approved provider credentials and other runtime variables reach upstream, then applies wrapper overrides such as the managed socket directory and clamped default operation timeout
267
267
  - clear missing-binary errors
268
268
  - filtered `network requests --current-page` / `--current-origin` / `--current-url` preparation results use the same `redactPresentationData` credential redactor as ordinary diagnostics before populating `details.data` or writing `outputPath`
269
- - compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set`, and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors. Shared URL redaction covers SAMLRequest/SAMLResponse/RelayState and auth-context-only state/nonce at model/persisted-artifact boundaries while retaining exact internal page-target URLs; snapshot spills are redacted before disk writes
269
+ - compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set` while distinguishing native `--curl` file-import operands from inline cookie values (including failed batch diagnostics), and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors. Shared URL redaction covers SAMLRequest/SAMLResponse/RelayState and auth-context-only state/nonce at model/persisted-artifact boundaries while retaining exact internal page-target URLs; snapshot spills are redacted before disk writes
270
270
  - bounded machine-readable outcome metadata on tool `details` (`resultCategory`, `successCategory`, `failureCategory`, optional `nextActions`, optional `pageChangeSummary` with observed-vs-dispatched evidence and per-step summaries on `batch`, optional `artifactVerification` with the same shape on each successful `batchSteps[]` row) so agents can branch without parsing prose; enums, classifier precedence, and generic follow-up payloads are implemented under `extensions/agent-browser/lib/results/` in focused modules (`contracts.ts` for shared types, `categories.ts` for `classifyAgentBrowserSuccessCategory` / `classifyAgentBrowserFailureCategory` / `buildAgentBrowserResultCategoryDetails`, `action-recommendations.ts` for `buildAgentBrowserNextActions`, `next-actions.ts` for the `AgentBrowserNextAction` shape and merge helpers, `recovery-actions.ts` for recovery id registries and `buildRecoveryNextActions`, `network.ts` for `classifyNetworkRequestFailure` / `summarizeNetworkFailures`, and related helpers). Per-session tab target, `refSnapshot` alignment, invalidation, and tab pinning observations flow through `extensions/agent-browser/lib/session-page-state.ts` from `extensions/agent-browser/index.ts`. Compact page-change summaries and artifact verification rollups are built in `extensions/agent-browser/lib/results/presentation.ts` (`buildPageChangeSummary`, `buildArtifactVerificationSummary`), and the human contract lives in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). Final public results return native `isError` after all wrapper reclassifications, without a `tool_result` repair hook. Prose retains the visible category notice; explicit parseable JSON stays unchanged. `lib/native-output.ts` and `finalizeAgentBrowserFailure` in `lib/pi-tool-rendering.ts` keep structured observations, transcript status and TUI affordances aligned (transcript semantics in the same contract doc)
271
271
  - inline screenshots/images for the plain `screenshot` command; other image-like saves (for example `diff screenshot`) still appear in `details.artifacts` and summaries but are not auto-inlined as Pi image attachments (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details))
272
272
  - lightweight session convenience
@@ -759,6 +759,8 @@ These calls return plain text and stay stateless: the extension does not inject
759
759
  | `cookies [get|set|clear]` | Manage cookies. Full set form: `cookies set <name> <value> --url <url> --domain <domain> --path <path> --httpOnly --secure --sameSite <Strict|Lax|None> --expires <timestamp>`; also supports `cookies set --curl <file>` for JSON, cURL, or bare Cookie-header bulk imports. |
760
760
  | `storage <local|session>` | Manage web storage. |
761
761
 
762
+ `cookies set --curl <file>` selects native file-import mode even when `--curl` follows other set operands. Ordinary import paths remain visible in invocation echoes and failed batch diagnostics; inline cookie values, sensitive flags, and secret-bearing path text still redact. File contents are not echoed by this exception.
763
+
762
764
  Privacy note: `cookies get` can expose real profile cookies. Do not run it against `--profile Default` or other authenticated profiles unless the user explicitly needs cookie inspection; prefer task-specific page actions and storage checks.
763
765
 
764
766
  ### WebMCP page tools
package/docs/RELEASE.md CHANGED
@@ -274,7 +274,7 @@ These show up often in cloud dev boxes and scripted smokes; they are maintainer
274
274
  | Topic | What to watch for | Mitigation |
275
275
  | --- | --- | --- |
276
276
  | **Pi CLI vs repo devDependencies** | Global `pi` older than the minimum Pi runtime floor for the release can change TUI behavior, `/reload`, package installs, and tool routing during lifecycle or checkout smokes. | Run `npm run doctor` and align `pi` with the current audited baseline before release gates (`pi update` or install the matching version). The published peer range stays wildcard per Pi package docs, and the doctor enforces the minimum Pi runtime floor before package validation. |
277
- | **npm lockfile (`packageManager`)** | `package.json` pins **npm@12.1.0** for the Node 24.21.0 runtime baseline. Older npm versions can rewrite optional-platform metadata in `package-lock.json`; private registry proxies can also leak organization URLs into `resolved` entries. | Prefer `npx -y npm@12.1.0 install` when refreshing the lockfile; do not commit unrelated lockfile churn or WorkOS/private-registry URLs. |
277
+ | **npm lockfile (`packageManager`)** | `package.json` pins **npm@12.2.0** for the Node 24.21.0 runtime baseline. Older npm versions can rewrite optional-platform metadata in `package-lock.json`; private registry proxies can also leak organization URLs into `resolved` entries. | Prefer `npx -y npm@12.2.0 install` when refreshing the lockfile; do not commit unrelated lockfile churn or WorkOS/private-registry URLs. |
278
278
  | **`pi -p` / print mode** | Non-interactive `pi -p` may hang or emit no stdout for long real-browser smokes without a TTY. | Use **tmux**-driven interactive `pi` for release evidence and checkout smokes; reserve `-p` for short, non-browser checks. |
279
279
  | **Real-browser cleanup** | `real-upstream`, Sauce Demo, and live-site runs can leave defunct Chrome/`agent-browser` children if a session aborts mid-flow. | Close via `agent_browser` / `agent-browser` `close`, kill stray tmux sessions, and remove temp screenshots/HARs under `/tmp` or your chosen artifact dirs. |
280
280
  | **Automated prompt driving** | Grepping tmux pane text for words that also appear in the **user** prompt (`PASS`, `FAIL`, `checkout overview`, `Smoke result:`) can false-complete before the agent finishes. | Wait for pane idle (no `Working…`), `agent_browser close` / `Artifact lifecycle`, or JSONL tool results—not instruction phrases copied from the prompt. |
@@ -99,7 +99,7 @@ Browser evidence (RQ-0152) is covered by `test/agent-browser.browser-evidence.te
99
99
 
100
100
  Contributor fixes #133/#152 remove unused prompt suffix entries without changing runtime guidance and diagnose bare `--no-sandbox` only in the command slot or navigation option positions. Native `--args` values and literal operands remain intact; batch checks use raw effective rows without treating row-local `--args` as a launch setting. `test/agent-browser.chromium-args.test.ts` covers pre-dispatch rejection, literal/flag-value controls, inspection, raw/stdin batch precedence and dispatcher outcomes. Existing prompt/grammar checks remain; source checks do not replace native-product gates.
101
101
 
102
- Electron diagnostics (RQ-0096, #128) keep list timeout rejection truthful, label explicit-ID cleaned records as historical without changing active selection or actions, and measure the tracked profile path with native `lstat` (`present` / ENOENT-only `absent` / `unknown`). Existing Electron discovery/lifecycle tests cover current liveness independently of cleanup history, native path errors and dangling symlinks, transcript replay, and unchanged cleanup ownership. Failed-startup diagnostics include redacted last-4096-byte stdout/stderr tails in visible text and structured details, using private profile-local regular files. Regression tests cover exact tails, empty output, capture/spawn errors and closed native file handles; a real spawned fixture with injected kill denial proves live output through temp cleanup, retained logs through native host-exit behavior, and persistence failure reporting. Tracked-child cleanup regressions require actual exit before profile removal and immediate executable deletion. Restored-PID cleanup uses native process command-line evidence on Windows and POSIX; real-process regressions verify matching profiles with spaces/Unicode, refusal of unrelated or mismatched profiles, and shutdown after transcript restoration. The shared daemon policy verifies a restored Electron attachment's live browser endpoint and named upstream `get cdp-url` before ordinary calls, status, or probe reuse; `test/agent-browser.extension-ref-guards.test.ts` covers successful reuse and repeated rejection of replaced app, connection, and namespace identities without weakening generic restore-disabled rules. These checks do not replace native-app, Pi lifecycle, package, or live-site gates.
102
+ Electron diagnostics (RQ-0096, #128) keep list timeout rejection truthful, label explicit-ID cleaned records as historical without changing active selection or actions, and measure the tracked profile path with native `lstat` (`present` / ENOENT-only `absent` / `unknown`). Focused Electron discovery, host-lifecycle, and branch-ownership tests cover current liveness independently of cleanup history, native path errors and dangling symlinks, transcript replay, and unchanged cleanup ownership. Failed-startup diagnostics include redacted last-4096-byte stdout/stderr tails in visible text and structured details, using private profile-local regular files. Regression tests cover exact tails, empty output, capture/spawn errors and closed native file handles; a real spawned fixture with injected kill denial proves live output through temp cleanup, retained logs through native host-exit behavior, and persistence failure reporting. Tracked-child cleanup regressions require actual exit before profile removal and immediate executable deletion. Restored-PID cleanup uses native process command-line evidence on Windows and POSIX; real-process regressions verify matching profiles with spaces/Unicode, refusal of unrelated or mismatched profiles, and shutdown after transcript restoration. The shared daemon policy verifies a restored Electron attachment's live browser endpoint and named upstream `get cdp-url` before ordinary calls, status, or probe reuse; `test/agent-browser.extension-electron-branch-ownership.test.ts` covers successful reuse and repeated rejection of replaced app, connection, and namespace identities without weakening generic restore-disabled rules. These checks do not replace native-app, Pi lifecycle, package, or live-site gates.
103
103
 
104
104
  Artifact diagnostics (#124/#127) use a shared pre-dispatch mkdir-error boundary, preserve raw batch argv/precedence, recommend absolute raw artifact paths, recognize image headers rather than filename MIME guesses, retain known requested/reported paths, and warn once about conservative ref invalidation after dispatched recording starts and URL-bearing restarts, on success or failure, without claiming an observed page change. `test/agent-browser.artifact-diagnostics.test.ts` covers registered filesystem failures, real image bytes and misleading suffixes, the inline bound, native macOS path aliases, recording/ref continuity and unreached-row negatives; `test/agent-browser.presentation-artifacts-batch.test.ts` retains artifact/persistence coverage. These source regressions do not qualify daemon-cwd differences or affected-filesystem timestamp behavior (#118), or replace native/Pi/package/live-site gates.
105
105
 
@@ -143,7 +143,7 @@ Re-run the gates below before each release. Dated outcomes in this table are his
143
143
  | Windows argument transport (#102/#131/#141) | `test/agent-browser.windows-argv.test.ts` exercises actual host argv, empty values, quotes, Unicode/spaces, stdin, custom shims and failure outcomes. `test/agent-browser.windows-stock-launcher.test.ts` covers the recognized global npm CMD template's direct-native path, command selection, fallback, literal LF/CRLF and cancellation. Custom or unrecognized batch launchers retain their existing newline limitation. POSIX keeps Node `spawn`. | Native Windows focused qualification on 2026-09-21 passed all seven stock-launcher controls, all 29 Chromium cases and 33 process/argv cases (two existing platform skips). Actual upstream Rust received all 12 expected operands; the original CMD path lost all four newline-bearing operands. These focused results do not replace full-suite, managed-browser, package or release gates. |
144
144
  | Real upstream contract | `npm run verify -- real-upstream` runs the localhost fixture matrix against a real stable `agent-browser` at or above the configured floor. | **Earlier recommended 0.36.0:** pass on 2026-09-01 (2/2 tests), including WebMCP and `--no-webmcp`; the 0.35.0 floor last passed on 2026-08-26 (2/2 tests). |
145
145
  | Packaged Pi smoke | `npm run verify -- package-pi` validates package contents, installs the extracted tarball's runtime dependencies without lifecycle scripts or host peers, loads the packaged `agent_browser` tool without requiring optional Brave config, and executes fake-upstream `--version`. | **Earlier package 0.6.2 / Pi 0.84.4:** pass on 2026-09-01; the 127-file tarball loaded exactly one packaged `agent_browser` and executed `--version`. |
146
- | Startup profile | `npm run verify -- startup-profile --samples <n>` clean-builds generated `dist/`, records direct package entrypoint import/factory timing in fresh Node processes, and writes `.artifacts/startup-profile/latest.json`. It must not launch Pi, tmux, mise, npm, browsers, or `agent-browser`; full Pi TUI ready-prompt profiling is intentionally excluded after it proved too invasive for routine verification. Run this opt-in evidence when package layout, the compiled entrypoint, top-level imports, schema registration, or prompt/config startup logic changes. | **Earlier compiled 0.6.2 entrypoint:** pass on 2026-09-01 with three samples; median 104.9 ms and max 114.5 ms, below the 250 ms budget. |
146
+ | Startup profile | `npm run verify -- startup-profile --samples <n>` clean-builds generated `dist/`, records direct package entrypoint import/factory timing in fresh Node processes using the same `scripts/startup-measurement.mjs` measurement and 250ms budget as the startup owner test (the test retains its Android-only 1000ms exception), and writes `.artifacts/startup-profile/latest.json`. It must not launch Pi, tmux, mise, npm, browsers, or `agent-browser`; full Pi TUI ready-prompt profiling is intentionally excluded after it proved too invasive for routine verification. Run this opt-in evidence when package layout, the compiled entrypoint, top-level imports, schema registration, or prompt/config startup logic changes. | **Earlier compiled 0.6.2 entrypoint:** pass on 2026-09-01 with three samples; median 104.9 ms and max 114.5 ms, below the 250 ms budget. |
147
147
  | Deterministic dogfood smoke | `npm run verify -- dogfood` clean-builds, then `scripts/verify-agent-browser-dogfood.ts` drives the native wrapper against a loopback HTTP fixture through persistent code aggregation, advanced QA/actions, native batch, screenshot artifact verification, and session close with the real `agent-browser` on `PATH`. | **Earlier 0.36.0 / package 0.6.2 / Pi 0.84.4:** pass on 2026-09-01; QA, script branching/cleanup, fresh/current opens, semantic clicks, job screenshot verification, and close all passed. |
148
148
  | WebGPU and restore autosave | Live 0.31.2 feature probes validate the new upstream paths independently from ordinary browser dogfood. | **macOS:** `doctor --webgpu` passed Apple Metal render/readback and headless red-pixel screenshot checks; the official Hello Triangle sample exposed the Metal adapter and produced a verified non-black screenshot. **Ubuntu image:** `doctor --webgpu --headed --json` passed SwiftShader render/readback and red-pixel screenshot checks with the baked Vulkan/Mesa/Xvfb dependencies. **Restore autosave:** an idle headless page timer changed localStorage and the namespaced restore file contained the new value before close; test state was removed. Wrapper-owned headed launches default periodic autosave off to avoid upstream 0.33.2 visible temporary collector tabs; native close still saves, direct window close can lose newer state because headed browsers are exempt from idle shutdown, and explicit `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` opts in at daemon launch; its effective value persists across resume, and changing a running wrapper-owned headed daemon in either direction requires close plus a fresh launch. On 2026-08-06, a headed checkout dogfood run crossed `react.dev` → `example.com` → `react.dev`, idled past the autosave interval while recording, showed no temporary page switch in frame/contact-sheet review, and closed both sessions successfully; disposable evidence was removed. **Windows interactive desktop:** post-release validation on 2026-07-15 used a disposable clone of snapshot `57cc3c0d-7d7c-4a4c-9c57-8270d4174679`, a logged-in console session, `agent-browser 0.31.2`, and Edge 150. The headed doctor WebGPU render/readback subcheck passed in 0.99s and its decoded screenshot subcheck passed with `rgb(255,0,0)`; a separate `--webgpu --headed` launch opened the proof page and the Parallels console capture visibly recorded the red triangle (553,500 bright-red pixels; SHA-256 `f5a28f5336cbdfeb0ff557af9425458bddc6b266d3cc6b946de7b101a0b43288`). The full doctor remained nonzero only for the unrelated absence of a separately installed Chrome binary; the explicit Edge executable completed the launch and both WebGPU probes. Local evidence is under `.artifacts/windows-webgpu-interactive/20260715T150903Z/summary.json`; the disposable clone and temporary account were deleted afterward. |
149
149
  | Android/Termux local bring-up | Non-release-blocking source validation uses Termux system Chromium through upstream's Linux `which chromium` discovery, the packaged Linux-musl arm64 CLI, real Pi managed-session smoke, deterministic dogfood, and the default unit/package gate. Electron desktop lifecycle is not applicable. | **Historical 0.6 local evidence (not 0.7 code qualification):** Android 17 / arm64 Termux passed implicit, explicit, and namespaced fresh sessions, managed restore, script, QA, semantic action, job, click, eval/batch stdin, screenshot attachment, recording dependencies, and cleanup; Android is not yet a Crabbox release target. |
@@ -160,7 +160,7 @@ Runtime floor note: package metadata requires Node 24.21.0 or newer, keeps optio
160
160
  | --- | --- | --- | --- | --- | --- |
161
161
  | Built-in skills | 19 canonical tokens from baseline section `skills`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills), generated baseline block, README proof section, release docs. | `needsManagedSession` keeps read-only skills inspection sessionless while preserving thin upstream passthrough; upstream `@agent-browser/sandbox` remains external package guidance, not a bundled wrapper dependency. | Runtime and extension-validation skills/provider matrix; real-upstream inspection/skills group. | Supported. |
162
162
  | Core page, element, navigation, and extraction commands | Canonical tokens from baseline section `core-commands`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands), [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md), README quick start. | Thin passthrough with wrapper-owned JSON/session planning, ref guidance, artifact verification, page-change summaries, click-dispatch diagnostics only after native temporary-marker target identity readback (otherwise abstaining), inspection-only recovery for upstream overlay-blocked clicks, deterministic document scroll before wheel/no-op diagnostics and instant CSS-container movement, shorthand compilers, and redaction. | Real-upstream core matrix (including snapshot-ref select) plus fake core matrix for passthrough, ordering, diagnostics, and compiler validation. | Supported. Upstream semantics remain upstream-owned. |
163
- | Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers and every session/state list row; explicit targets and paths pass through. Runtime retains state artifact verification, sessionless local command planning, automatic managed restore, native-CDP-ID tab recovery across duplicate URLs and transcript replay, reached failed-batch target invalidation/live verification, active-target refresh after tab close, and close alias cleanup. | Extension tab/ref tests, real-upstream stable-id/label tab lifecycle, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
163
+ | Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers, every session/state list row, and ordinary `cookies set --curl` file operands in invocation echoes and failed batch diagnostics; explicit targets and paths pass through. Runtime retains state artifact verification, sessionless local command planning, automatic managed restore, native-CDP-ID tab recovery across duplicate URLs and transcript replay, reached failed-batch target invalidation/live verification, active-target refresh after tab close, and close alias cleanup. | Extension tab/ref tests, real-upstream stable-id/label tab lifecycle, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
164
164
  | Network, storage, artifacts, diagnostics, and performance | Canonical tokens from baseline section `network-storage-artifacts-diagnostics`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage), diagnostic sections, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Thin passthrough plus native-owned click/download execution for all downloads (including loopback, Blob exports, and redirects), compact diagnostics with identical credential redaction for page/origin/URL-filtered details and exports, route-mock warnings, useful-but-redacted storage output, stream idempotency normalization, FPS/cursor/contact-sheet-aware recording destinations/pinning and contact-sheet image verification, truthful ref-refresh and missing-ffmpeg warnings, positive WebMCP navigation hints and catalog updates, sensitive-data redaction, timeout bounds, and cleanup-pair guidance. | Fake non-core matrix and safe real-upstream coverage for network/HAR, diff, trace/profiler, console/errors/highlight, stream, vitals, React missing-renderer, native FPS recording path/page checks and WebMCP navigation hints. | Supported. Environment-sensitive operations need suitable local/browser state. |
165
165
  | Batch, auth, confirmations, setup, dashboard, devices, and AI commands | Canonical tokens from baseline section `batch-auth-setup-ai`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup), README security notes, release docs. | Native-tool batch stdin, generated QA/lookup batch plans, auth/confirmation redaction, sessionless local auth/setup/dashboard/doctor/plugin planning, upgrade-only native text normalization with error precedence and explicit-JSON results, plugin list/show JSON envelope normalization, bare-`mcp` validation with `mcp --help` preserved, timeout/cleanup guidance. | Parser/runtime plugin and MCP unit coverage; fake-upstream plugin list/show and MCP help/blocking coverage; registered upgrade text/success/nonzero/timeout/abort/JSON controls in `test/agent-browser.upgrade-output.test.ts`; real-upstream plugin list shape probe; structured input-mode tests. | Supported. Interactive side-effecting setup/auth/chat remains upstream-owned. `plugin` is local/sessionless; `mcp` is external-client-only except help; `auth login --credential-provider` resolves credentials via a plugin; `install --with-deps` failures remain upstream-owned. |
166
166
  | Global flags, config, providers, policy, and environment | Canonical tokens from baseline section `options-and-env`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment), README provider/setup notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sessionmode), architecture/runtime docs. | Runtime preserves caller-requested JSON for early filters, scroll results, and preparation failures without output-file prose (help/version remains native text), and handles command discovery, value-flag prevalidation, launch-scoped flags, redacted echoes, fresh-session recovery hints, explicit sessions, provider/device launch-scoping, parent env forwarding with consistent managed idle-timeout/socket/operation-timeout overrides on top-level and helper subprocesses, subprocess completion, and package-owned Pi-scoped config for optional companion features. | Runtime tests for flags/planning/redaction/session behavior; process/ref tests for env consistency and stdio-linger completion; config/web-search/CLI tests; fake provider/specialized-skill matrix; package doctor. | Supported. Provider clouds, iOS/Appium, proxies, profiles, and credentials require external setup. |
@@ -321,7 +321,7 @@ The browser persists between code and direct calls. JavaScript variables do not.
321
321
 
322
322
  Inner calls are serialized, including calls issued with `Promise.all`. The maximum is 25 attempted calls, 64 KiB emitted JSON, 1 MiB per IPC message, and 8 MiB cumulative IPC. Inner execution keeps full redacted data without full model rendering/spilling/image encoding; only selected outer output is rendered. An observation that cannot fit the bridge limit fails rather than silently discarding recovery or fabricating data.
323
323
 
324
- Source runs in a fresh permissioned Node child with an empty child environment, 64 MiB V8 heap ceiling, and a VM context whose string/WebAssembly code generation is disabled. Task globals have null prototypes and JSON-only IPC; no parent promise/object/function is exposed. There are no host filesystem, network, process, import, or timer APIs. This language boundary is separate from native browser capabilities: parent-side browser subprocesses still receive ordinary auth/config/environment. `node:vm` alone is not a security boundary, and this contract is not a universal sandbox certification.
324
+ Source runs in a fresh permissioned Node child with an empty child environment, 64 MiB V8 heap ceiling, and a VM context whose string/WebAssembly code generation is disabled. Task globals have null prototypes and JSON-only IPC; no parent promise/object/function is exposed. There are no host filesystem, network, process, import, or timer APIs. When Pi itself runs as a Bun-compiled binary, the child is hosted by a real `node` runtime from `PATH`; if none is available the call fails as `missing-binary` with that exact remediation instead of hanging the handshake. This language boundary is separate from native browser capabilities: parent-side browser subprocesses still receive ordinary auth/config/environment. `node:vm` alone is not a security boundary, and this contract is not a universal sandbox certification.
325
325
 
326
326
  #### Failure and persistence
327
327
 
@@ -618,7 +618,7 @@ Ref preflight details (command taxonomy in `extensions/agent-browser/lib/command
618
618
  - **URL alignment:** `refSnapshot.target.url` and the session’s current tab URL are compared via `targetsMatch` / `normalizeComparableUrl` in `extensions/agent-browser/index.ts`: values are trimmed, parsed as URLs when possible, compared **after dropping the `#fragment`**, and the query string remains significant. If either side lacks a `url`, `targetsMatch` treats the pair as matching so early-session calls are not blocked.
619
619
  - **Batch stdin ordering:** user `batch` JSON is scanned in order. Any step whose first token satisfies `isRefInvalidatingBatchCommand` sets a latch that blocks later steps whose first token satisfies `isRefGuardedCommand` and that mention `@e…` refs, except for same-snapshot native form-control steps whose current snapshot role metadata identifies all refs as safe controls (`check`/`uncheck` or direct `click`/`tap` on checkbox or radio refs, and `select` on combobox refs). A step whose first token is `snapshot` clears that latch for subsequent steps (pre-spawn intent only; it does not wait for upstream success). These predicates read explicit command capability flags from `command-taxonomy.ts`: navigation/mutation verbs such as `open` / `goto`, `reload`, non-form `click`, and related upstream commands have `invalidatesBatchRefs`, and `record start` steps (any outcome), `record restart` steps with a URL operand, plus WebMCP `invoke` / `result` / `cancel` steps also set the latch because upstream swaps or navigates the active page; same-snapshot `fill` rows and the role-checked native form-control rows stay guarded against missing/stale refs but do not set the latch, allowing ordinary form batches before a final click/submit step. Direct `click`/`tap @e…` is only treated as a safe form-control row when every ref in that step is a latest-snapshot checkbox or radio; other click/tap refs remain invalidating. Ref-guarded commands accept page-scoped refs for interaction (`click`, `fill`, `download`, `scrollintoview` / `scrollinto`, and others centralized in the command taxonomy). Changing either capability requires updating this contract, [`docs/SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md) `RQ-0072`/`RQ-0087` notes, README and command-reference pitfalls, and `test/agent-browser.extension-validation.test.ts`.
620
620
 
621
- **Presentation redaction (implementation map):** Successful non-`batch` tool calls and each successful `batchSteps[]` row run upstream `data` through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation/diagnostics.ts`: `cookies` still walk objects/arrays and replace case-insensitive `value` keys with `"[REDACTED]"`; `storage` redacts values when the key or value looks credential-like (token, cookie, auth, secret, JWT, bearer/basic credential, high-entropy token-like string, or nested sensitive JSON) but keeps low-risk primitive QA values such as booleans, numbers, and short strings visible. Redacted storage entries add `valueRedacted` plus `valueRedactionReason` in `details.data`; diagnostic formatters mirror the same decision. Every other command’s payload is recursively scrubbed with the shared `redactSensitiveValue`, which redacts known sensitive key names and applies string-level sensitivity heuristics so network, diff, trace/profiler, stream, dashboard, chat, and other structured results do not echo bearer tokens, proxy credentials, or similar fields verbatim into `details.data`. Echoed `command` arrays in `details` and in batch roll-ups use `redactInvocationArgs` from `extensions/agent-browser/lib/runtime.ts` to mask trailing values for sensitive global flags (including `--body`, `--headers`, `--password`, and `--proxy`), preserve the special positional rules for `cookies set`, `storage local|session set`, and `set credentials`, and scrub other argv tokens for URLs and inline secrets. Raw batch row strings are parsed and command-redacted for presentation and ordered intent/completion journals; execution argv remains unchanged. Failed batch steps additionally run `redactExactValues` on structured step errors so literals taken from that step’s argv (cookie value, storage set value, `--password` / `--password=` tokens) cannot reappear inside formatted error blobs. When the full batch is large enough to need its own aggregate spill, that spill reapplies these per-command data and argv redactors before persistence rather than using generic batch redaction.
621
+ **Presentation redaction (implementation map):** Successful non-`batch` tool calls and each successful `batchSteps[]` row run upstream `data` through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation/diagnostics.ts`: `cookies` still walk objects/arrays and replace case-insensitive `value` keys with `"[REDACTED]"`; `storage` redacts values when the key or value looks credential-like (token, cookie, auth, secret, JWT, bearer/basic credential, high-entropy token-like string, or nested sensitive JSON) but keeps low-risk primitive QA values such as booleans, numbers, and short strings visible. Redacted storage entries add `valueRedacted` plus `valueRedactionReason` in `details.data`; diagnostic formatters mirror the same decision. Every other command’s payload is recursively scrubbed with the shared `redactSensitiveValue`, which redacts known sensitive key names and applies string-level sensitivity heuristics so network, diff, trace/profiler, stream, dashboard, chat, and other structured results do not echo bearer tokens, proxy credentials, or similar fields verbatim into `details.data`. Echoed `command` arrays in `details` and in batch roll-ups use `redactInvocationArgs` from `extensions/agent-browser/lib/runtime.ts` to mask trailing values for sensitive global flags (including `--body`, `--headers`, `--password`, and `--proxy`), preserve the special positional rules for `cookies set`, `storage local|session set`, and `set credentials` (native `cookies set --curl <file>` imports are file operands, so ordinary import paths remain visible in echoes and failed batch diagnostics), and scrub other argv tokens for URLs and inline secrets. Raw batch row strings are parsed and command-redacted for presentation and ordered intent/completion journals; execution argv remains unchanged. Failed batch steps additionally run `redactExactValues` on structured step errors so literals taken from that step’s argv (cookie value, storage set value, `--password` / `--password=` tokens) cannot reappear inside formatted error blobs. When the full batch is large enough to need its own aggregate spill, that spill reapplies these per-command data and argv redactors before persistence rather than using generic batch redaction.
622
622
 
623
623
  `nextActions` is an optional list of exact follow-ups. Each entry has a `tool`, `id`, `reason`, optional `safety`, and executable `params` or an `artifactPath`. Native actions use `agent_browser` with argv/stdin; Electron and network-source actions use their actual advanced tool name and flat params. Enable the corresponding capability with `agent_browser_tools` if needed. Both prose and JSON expose recovery; oversized observations retain complete payloads in the named observation spill rather than inventing shortened calls.
624
624
 
@@ -699,7 +699,7 @@ Additional structured fields can appear when relevant:
699
699
  - `attachedBrowserSession: true` on successful calls that establish or reuse a wrapper-tracked CDP/auto-connect/Electron attachment, and on a failed fresh attachment only when `managedSessionOutcome.activeAfter` proves its daemon remained active for cleanup. The marker restores attachment continuity from the active transcript branch, including that active-after-failure case; live state and transcript replay remove it after a terminal successful close/cleanup even when aggregate verification failed; a close followed by a later step whose lifecycle reports a browser launch preserves it, while a successful non-launching diagnostic leaves the close terminal. Caller config, environment, paths, and file-access settings remain upstream-owned; the marker only adds live-URL verification and lifecycle continuity.
700
700
  - `lifecycle: { effectiveLaunch: { browserLaunched } }` when upstream returned that boolean. It separates starting the requested `agent-browser` CLI process from the effective Chrome session context. `readSource` exposes upstream's string `data.source` for direct `read` calls and identifies the raw HTTP fetch path; its lifecycle boolean can be `false` before a browser launch or `true` when the same managed session already has an active browser. Direct reads also append one visible `Read execution` line with the source, CLI-start result, managed browser lifecycle, and managed-session outcome so Pi models do not have to infer model-invisible details.
701
701
  - `browserWindow: { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` only after a successful first/fresh local wrapper-managed headed call (including `batch`) that is not an attachment and whose lifecycle proves a browser launched and whose managed-session outcome is `created` or `replaced`. One visible handoff sentence tells the user to complete the login in that window if they can see it, then continue with `sessionMode: "auto"`; the field never claims OS desktop visibility.
702
- - `sessionTabTarget` retains the observed `url`, optional `title`, and native CDP `targetId` when available. When an observed page lacks a native ID, a read-only `tab list` observes current identity; earlier commands and equal URLs cannot supply it. If another tab matches the observed URL while the active tab does not, existing URL-based drift recovery selects the matching tab instead of attaching the unrelated active ID. Resume and branch recovery select that exact target, even when several tabs share an address/title; a missing known ID never falls back to another same-address tab. Older transcripts without an ID retain URL-based recovery. An existing cold-managed-browser reopen records its new native ID. `sessionTabCorrection.selectionKind` can be `targetId`, `tabId`, `label`, or `index`.
702
+ - `sessionTabTarget` retains the observed `url`, optional `title`, and native CDP `targetId` when available. When an observed page lacks a native ID, a read-only `tab list` observes current identity; earlier commands and equal URLs cannot supply it. If another tab matches the observed URL while the active tab does not, existing URL-based drift recovery selects the matching tab instead of attaching the unrelated active ID. Resume and branch recovery select that exact target, even when several tabs share an address/title; a missing known ID never falls back to another same-address tab. Older transcripts without an ID retain URL-based recovery. An existing cold-managed-browser reopen records its new native ID. `sessionTabCorrection.selectionKind` can be `targetId`, `tabId`, `label`, or `index`. `sessionTabCorrection` records an applied tab selection (before the command or during post-command recovery), not proof of the final page or successful later recovery. Use `sessionTabTarget` for the final observed position and `aboutBlankSessionMismatch.recoveryApplied` for the recovery outcome; the historical correction may remain when that recovery fails.
703
703
  - `sessionTabTargetUnknown: true` after a spawned `connect`, `state load`, history navigation, tab-selection/close, `window new`, or `diff url` call changes the active page without a trustworthy observed target. Direct `window new` / `diff url` and reached native batch rows retire the old target and refs; an intentional new blank window or an observed blank URL-diff destination never triggers old-tab recovery. URL diff inputs do not prove the final URL after redirects. Successful tab creation/selection/close, including dispatched batch rows, live-probes URL and a fresh non-blank title before state is committed, even when the new tab shares the prior URL; new blank tabs, explicitly selected `about:blank` tabs, and post-close blank targets are retained. It is persisted and restored across branch/reload replay, clears stale refs and tab pinning, and blocks page inspection until `get url` or explicit navigation observes a target; `tab list`, tab selection, close, and blocking-dialog `status` / `accept` / `dismiss` remain available. A timeout against an unknown target removes standalone snapshot actions and returns `verify-page-target-after-timeout`, a session-scoped `batch --bail` whose stdin runs `get url` before `snapshot -i`.
704
704
  - `compiledSemanticAction` when the call used `agent_browser_action` and the result includes the unified `details` merge: `{ action, locator, args }` for `find` actions or `{ action: "select", selector?, locator?, values, args }` for `select`, with the same redaction rules as `args` / `effectiveArgs`; omitted for plain direct native calls and omitted on some early error returns that omit this field (see the `agent_browser_action` section above)
705
705
  - `compiledJob` is internal QA batch-plan metadata: `{ args: ["batch", "--bail"], failFast: true, stdin, steps }`, with redacted step argv. It does not expose a public job input or compiler.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
@@ -72,7 +72,7 @@
72
72
  "@earendil-works/pi-coding-agent": "1.0.0",
73
73
  "@earendil-works/pi-tui": "1.0.0",
74
74
  "@types/cross-spawn": "6.0.6",
75
- "@types/node": "24.19.0",
75
+ "@types/node": "24.19.1",
76
76
  "@types/which": "2.0.2",
77
77
  "tsx": "4.23.15",
78
78
  "typebox": "1.3.27",
@@ -98,7 +98,7 @@
98
98
  "startup-profile": "node ./scripts/profile-startup.mjs",
99
99
  "prepare": "node ./scripts/prepare.mjs"
100
100
  },
101
- "packageManager": "npm@12.1.0",
101
+ "packageManager": "npm@12.2.0",
102
102
  "peerDependenciesMeta": {
103
103
  "@earendil-works/pi-ai": {
104
104
  "optional": true
@@ -124,15 +124,23 @@ export function parseCliArgs(argv = process.argv.slice(2)) {
124
124
  }
125
125
 
126
126
  async function defaultRunAgentBrowser(args) {
127
- const { stdout, stderr } = await execFile("agent-browser", args, { maxBuffer: 1024 * 1024 });
127
+ const { stdout, stderr } = await execFile("agent-browser", args, { maxBuffer: 1024 * 1024, ...buildNpmShimExecOptions(process.platform) });
128
128
  return `${stdout}${stderr}`;
129
129
  }
130
130
 
131
131
  async function defaultRunPi(args) {
132
- const { stdout, stderr } = await execFile("pi", args, { maxBuffer: 1024 * 1024 });
132
+ const { stdout, stderr } = await execFile("pi", args, { maxBuffer: 1024 * 1024, ...buildNpmShimExecOptions(process.platform) });
133
133
  return `${stdout}${stderr}`;
134
134
  }
135
135
 
136
+ /**
137
+ * npm's global bin shims on Windows are `.cmd` files that `execFile` cannot launch without a
138
+ * shell. Doctor only passes fixed flag arrays here, so shell composition stays safe.
139
+ */
140
+ export function buildNpmShimExecOptions(platform = process.platform) {
141
+ return platform === "win32" ? { shell: true } : {};
142
+ }
143
+
136
144
  async function defaultPathExists(path) {
137
145
  try {
138
146
  await access(path);