pi-agent-browser-native 0.2.72 → 0.2.75

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 (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +14 -12
  3. package/dist/extensions/agent-browser/index.js +148 -16
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +122 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +11 -0
  6. package/dist/extensions/agent-browser/lib/electron/cdp.js +2 -2
  7. package/dist/extensions/agent-browser/lib/electron/launch.js +48 -12
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +96 -98
  9. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +88 -2
  10. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +22 -0
  11. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +432 -0
  12. package/dist/extensions/agent-browser/lib/managed-session-restore.js +374 -0
  13. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +367 -0
  14. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +589 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +299 -0
  16. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +35 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +9 -2
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +40 -22
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +15 -6
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +54 -33
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +182 -0
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +629 -430
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +148 -57
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +30 -40
  29. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +102 -19
  30. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +13 -1
  31. package/dist/extensions/agent-browser/lib/playbook.js +9 -8
  32. package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
  33. package/dist/extensions/agent-browser/lib/process.js +271 -34
  34. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
  35. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
  36. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +35 -12
  37. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
  38. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
  39. package/dist/extensions/agent-browser/lib/runtime.js +114 -107
  40. package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
  41. package/dist/extensions/agent-browser/lib/temp.js +13 -25
  42. package/docs/ARCHITECTURE.md +10 -9
  43. package/docs/COMMAND_REFERENCE.md +43 -23
  44. package/docs/ELECTRON.md +10 -10
  45. package/docs/RELEASE.md +3 -2
  46. package/docs/REQUIREMENTS.md +1 -1
  47. package/docs/SUPPORT_MATRIX.md +19 -18
  48. package/docs/TOOL_CONTRACT.md +29 -26
  49. package/docs/platform-smoke.md +2 -2
  50. package/package.json +1 -1
  51. package/platform-smoke.config.mjs +1 -1
  52. package/scripts/agent-browser-capability-baseline.mjs +11 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.75 - 2026-08-04
4
+
5
+ ### Fixed
6
+
7
+ - Headless `dash.cloudflare.com` now uses the same normal-Chrome user-agent compatibility path as OpenAI web properties, bypassing the Turnstile loop caused by `HeadlessChrome`. Wrapper-managed sessions retain that wrapper-owned user agent across follow-up calls and Pi reload/resume while preserving checkout-managed authentication restore.
8
+
9
+ ## 0.2.74 - 2026-08-03
10
+
11
+ ### Changed
12
+
13
+ - Rebaselined the command/help inventory and package docs to `agent-browser 0.33.2` / vercel-labs/agent-browser@93cdda5709e8861c0c26b0b955d8d746e9fda0d7 (0.33.1 daemon idle timeout + tab recovery; 0.33.2 stream quality/size envs and latest-wins streaming).
14
+ - Wrapper-owned managed sessions now set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key so SSO cookies/localStorage/sessionStorage survive browser relaunches across Pi chats in the same checkout generation. The wrapper combines checkout-root and Git-admin filesystem identities with a UUID in the Git admin directory, keeps the key across checkout renames, changes it on copied/replaced checkouts, and fails closed outside Git instead of adopting older cwd-only keys. Restore is ownership-gated, and `piab-*` live-session names are reserved for the extension instance that owns them, sticky-disabled after incompatible launches/config (profiles, CDP/providers, extensions/init scripts/raw args/plugins, and related browser mutation), and opt-out with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. Any upstream config discovered while planning blocks browser-backed native calls without being read by the Pi host, while accepted browser-backed subprocesses, including wrapper-owned closes, pin a process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle to prevent later config creation from changing the receiving browser while preserving PID/start-identity abnormal-exit stale cleanup on POSIX and native Windows while treating legacy Windows identity formats conservatively; ownership marker schema v2 makes older readers ignore new Windows identity records. A user-private immutable ticket-claim lock with PID/start-identity dead-claim/artifact recovery and a fail-closed pre-update v2 bridge serializes cross-process daemon inspection through spawn (its post-v0.2.74 removal is tracked in [#93](https://github.com/fitchmultz/pi-agent-browser-native/issues/93)), failed fresh starts are probed and retained for shutdown cleanup when live or uninspectable, and the wrapper canonicalizes and pins namespace identity (including default-namespace closes and replayable Electron probe state), canonicalizes wrapper-owned close argv so caller config/restore globals cannot redirect saved auth, keeps close from injecting a replacement checkout's restore key into a live daemon while recording returned old-generation snapshots against the observed wrapper key, rejects nested batch attachment, inspects live same-name daemons before incompatible reuse, prevents already-aborted calls from spawning, canonicalizes and pins trusted home roots after caller env merging, rejects writable/unowned POSIX ancestry without silently chmod-tightening it, rejects symlinks through the POSIX state path and `.tmp` write area, enforces owner-only mode `0700`, persists close-proven snapshot ownership as atomic per-key records across Pi restarts, converges concurrent close records without a blocking pruning lock, self-heals malformed regular records, expires wrapper-created snapshots older than 30 days while retaining two fallbacks, and caps young churn at 256 records per restore key, redacts `state show` cookie/storage values, and fails closed when storage is unsafe or an encryption key is malformed; Windows requires a 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY`. An already-live daemon using an older cwd-only restore key must be closed before reuse; the wrapper now refuses to attach when the live same-name daemon's key does not match the checkout generation.
15
+ - Closed final review gaps by re-inspecting every same-identity daemon under the policy lock and requiring current-process provenance before reusing a restore-disabled daemon, cleaning Electron processes/profiles after any post-launch prepare failure, using a strict native-Windows command-first global scanner that preserves valued `--restore` semantics through `--restore=<name>`, exact lowercase optional booleans, and invalid-input failure behavior, falling back from `/bin/ps` to `/usr/bin/ps`, repeating checkout/storage/state-access validation after all async setup immediately before spawn, splitting real-upstream verification into force-exiting fail-fast phases, keeping npm's local dependency bin directory from shadowing the host Pi in lifecycle verification, applying managed restore policy to every Electron status/probe subprocess, retaining restore-disabled daemon provenance across same-process branch changes, and making filtered state-list summaries count only caller-visible rows.
16
+ - Final merge review also made Electron host launch cancellation no-spawn/cleanup-safe, decoupled managed daemon inspection from shorter caller watchdog overrides, recorded null daemon policy for owned restore-disabled helper starts, classified all-failed Electron probes as upstream errors, reserved managed session names case-insensitively, split daemon policy and managed-list filtering into focused modules, and hardened local boundaries. POSIX daemon socket storage now rejects rather than repairs pre-existing unsafe modes, validates trusted ancestry and planted entries, and uses the canonical macOS temp path. Browser access to `.agent-browser` state is blocked through command-specific input/output operands (including dash-prefixed global and positional paths), every path-bearing upstream environment mirror (state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded/nested-file-scheme/Windows-aliased/symlinked paths (including nonexistent descendants), protected top-level `outputPath`, content-returning local URLs, local-page follow-ups, recursively inspected raw batch command strings, and persisted unverified top-level or batch tab/attachment/script/state-load transitions; Electron snapshot/tabs handoff, probes, and later capture share the boundary; handoff/probes verify the live URL before tab/title/content helpers, and handoff failure or cancellation cleans the managed session plus host process/profile. Raw artifact destinations are checked before directory creation with the same screenshot-path parser used by preparation. Enabled file-access argv/env and file-access-enabling or protected-path raw Chrome values are rejected, while every upstream spawn clears raw-args env, strips caller file-access occurrences, and adds canonical `--args "" --allow-file-access false` defaults so project/user config cannot re-enable local access. Post-transition navigation summaries, including forced live probes after arbitrary `eval`, and timeout diagnostics verify `get url` before reading title and fail when an implicit transition lands on a local file page. Failed or unexecuted navigation stays unverified, stale concurrent completions serialize authoritative state only, and replay gives unknown state precedence over inconsistent stale fields. While a target is unverified, `tab list` and non-content `tab <id>` selection remain available, but page reads still require `get url` to verify the selected target.
17
+ - Final security review now live-verifies the active URL before content-bearing calls against caller-owned explicit sessions, including sessions restored from stale transcript page state, and fails closed when that probe cannot prove a safe target. Protected-path detection treats Windows drive-relative forms such as `C:.agent-browser\\state\\...` as filesystem paths, nested `batch` steps are rejected instead of being interpreted recursively, and raw batch command strings mirror upstream's ASCII-space tokenizer, including its quote/backslash handling, rather than splitting on other Unicode whitespace.
18
+ - Final reviewer remediation models continued execution after failed non-bail batch navigation and blocks later content when any retained page could be local or unverified; exact `batch --bail` and already-safe diagnostic continuation remain available. Non-bail state exploration is capped and fails closed to `--bail` guidance instead of growing without bound. Caller-owned explicit-session calls are serialized per effective canonical namespace/session inside one extension instance, including CLI/environment namespace aliases from live URL verification through semantic snapshot resolution and the main command; macOS and Windows identity keys also case-fold namespace and session components to match case-insensitive daemon paths. Different identities remain concurrent, with policy, route, and artifact deltas merged across unrelated managed-state commits while branch restores still discard stale work. Concurrent artifact results carry the aggregate manifest with monotonic revisions so transcript restore retains all bounded entries. Semantic-action snapshots now run only after the live URL gate succeeds, and cancellation during the live probe propagates instead of becoming a page-verification error.
19
+ - Managed `piab-r2-*` restore capabilities, legacy `piab-r-*` capabilities, and capability-bearing paths are now redacted from model-visible text, structured details, JSON-mode content, and persisted tool results. `session list` and `state list` omit wrapper-managed rows; malformed oversized upstream output is discarded instead of being persisted as a secret-bearing parse-failure spill; foreign managed `--restore` / `--state` / `state show` / `state load` references, broad `state clear`, `state clean`, and managed save/rename targets fail before spawn. Retention removes stale ownership-proven snapshots and empty manifests from superseded restore-key generations after 30 days only when a private lineage record proves the same canonical checkout path, while preserving independent checkouts, unrecorded files, and the current checkout key.
20
+
21
+ ### Validation
22
+
23
+ - Passed `npm run verify -- pre-pr` (690 tests passed, 2 opt-in skips; 125 packed files), real-upstream contract, dogfood, packaged Pi, startup-profile, configured-source lifecycle, isolated Pi explicit-session smoke, and local platform-target verification. The remote Crabbox macOS/Ubuntu/native-Windows matrix was unavailable and explicitly waived for this release.
24
+
25
+ ## 0.2.73 - 2026-08-02
26
+
27
+ ### Changed
28
+
29
+ - Shrunk the model-facing `agent_browser` parameter schema by trimming redundant field descriptions while keeping every input mode (`args`, `semanticAction`, `job`, `qa`, `sourceLookup`, `networkSourceLookup`, `electron`) and the same validation constraints.
30
+ - Updated schema/extension validation coverage, including a compact schema size budget check.
31
+
32
+ ### Validation
33
+
34
+ - Passed `npm run verify` (591 tests passed, 2 opt-in skips) and live command-reference verification against `agent-browser 0.33.0`. Platform/cloud release smoke was not run for this GitHub-only prep.
35
+
3
36
  ## 0.2.72 - 2026-07-23
4
37
 
5
38
  ### Changed
package/README.md CHANGED
@@ -75,7 +75,7 @@ The result is optimized for agent work:
75
75
  | Page snapshots are too large or viewport-blind | Shows compact, main-content-first summaries, surfaces an `Omitted high-value controls` section (plus `details.data.highValueControlRefIds`) when dense pages or desktop host screens hide editables, named surfaces/tabs, primary action buttons, and high-signal named links such as repository results from the trimmed ref lists, supports wrapper-side `snapshot -i --search <text>` / `--filter role=<role>` to trim dense pages while preserving full `details.refSnapshot`, supports `snapshot --viewport` for scroll/viewport metadata, supports `snapshot --diff` for quick ref-map deltas versus the prior tracked snapshot, and stores full raw output in spill files when needed | `extensions/agent-browser/lib/results/snapshot.ts`, `extensions/agent-browser/lib/orchestration/browser-run/prepare.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-validation.test.ts` |
76
76
  | Screenshots/downloads get lost in text | Normalizes artifact paths, creates missing parent directories, saves simple loopback anchor downloads to the requested path when possible, and reports existence, size, cwd, session, and repair status | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#download-screenshot-and-pdf-files) |
77
77
  | Profile restores and tab drift confuse agents | Tracks managed sessions, keeps every upstream helper probe on the same idle-timeout launch configuration so the background browser is not restarted between a snapshot and action, re-selects target tabs after observed drift, refreshes the active target after `tab close`, rehydrates branch-backed session state on Pi session-tree changes, and pins later commands only for sessions with drift/restored-session risk | generated tab-recovery notes below; `test/agent-browser.extension-tab-recovery.test.ts` (drift and about:blank recovery), `test/agent-browser.extension-tabs.test.ts` (post-close target), `test/agent-browser.extension-ref-guards.test.ts` (snapshot/action environment and session-tree rehydration), `test/agent-browser.resume-state.test.ts` (persisted session / resume planning) |
78
- | Auth/profile workflows can leak secrets | Supports `auth save --password-stdin` and redacts sensitive args, URLs, stdout/stderr, details, and parse-failure spills | `test/agent-browser.extension-security-redaction.test.ts` |
78
+ | Auth/profile workflows can leak secrets | Supports `auth save --password-stdin`, redacts sensitive args, URLs, stdout/stderr, and details, and discards malformed oversized stdout instead of persisting a parse-failure spill | `test/agent-browser.extension-security-redaction.test.ts` |
79
79
  | Stateful cookies/storage/auth output bloats or leaks context | Presentation layer redacts `details.data` for cookies and credential-like storage values while keeping low-risk local QA values such as `theme: dark` readable; recursively scrubs other structured upstream JSON (network, diff, trace/profiler, stream, dashboard, chat, auth, dialog, frame, state, and similar) using sensitive key names plus string heuristics; masks sensitive argv flags and positionals; scrubs secrets from failed batch step errors; and exposes a compact redacted `batch` matrix on top-level `details.data` | `extensions/agent-browser/lib/results/presentation.ts`, `extensions/agent-browser/lib/results/presentation/diagnostics.ts`, `extensions/agent-browser/lib/runtime.ts`, `test/agent-browser.presentation-diagnostics.test.ts` |
80
80
  | Stale `@eN` refs fail mysteriously | Records per-session `details.refSnapshot`, rejects mismatched URLs / unknown refs / unsafe `batch` stdin ordering before spawn, adds recovery guidance to rerun `snapshot -i` or use stable `find` locators | `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/session-page-state.ts`, `test/agent-browser.session-page-state.test.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.extension-semantic-recovery.test.ts` |
81
81
  | Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; a `tool_result` hook also aligns real Pi `isError` semantics, naming `Pi tool isError: true` in prose output while preserving parseable caller-requested `--json` output | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/results/categories.ts`, `extensions/agent-browser/index.ts`, `extensions/agent-browser/lib/pi-tool-rendering.ts`, `test/agent-browser.results.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.pi-pipeline.test.ts` |
@@ -85,7 +85,7 @@ The result is optimized for agent work:
85
85
  | Recording workflows fail late when `ffmpeg` is missing | After successful `record start` / `record restart`, warns when `ffmpeg` is not on `PATH` so agents can install or fix PATH before `record stop` | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#diff-debug-and-streaming), `test/agent-browser.extension-validation.test.ts` |
86
86
  | Direct binary help may be blocked in agent sessions | Publishes a repo-readable command reference and verifies it against the target upstream version | `npm run verify` |
87
87
  | Desktop Electron apps need discovery, CDP attach, and safe teardown | Top-level `electron` runs host `list` / isolated `launch` (temp profile, OS-chosen debug port) / `status` / `probe` / `cleanup`, merges `launchId` plus managed `sessionName`, supports `handoff` `snapshot` / `tabs` / `connect`, and surfaces mismatch and post-command health guidance; wrapper cleanup applies only to launches it created | `extensions/agent-browser/lib/electron/discovery.ts`, `launch.ts`, `cleanup.ts`, [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#electron), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#electron-desktop-apps) |
88
- | Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list`, `skills get …`, `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), `profiles`, `dashboard`, `device list`, `doctor`, `install`, `upgrade`, `session list`, `session id`, `session info`, `plugin add/list/show/run`, `mcp --help`, and targeted/all saved-state maintenance (`state clear --all`, `state clear -a`, named clear, or `state clean --older-than <days>`) as sessionless reads/actions: no implicit managed `--session` under default `sessionMode: "auto"` (same session-ownership goal as plain-text `--help` / `--version`), while bare `mcp` server calls are blocked and provider/browser-backed workflows stay thin passthroughs that require upstream setup and credentials | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
88
+ | Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list`, `skills get …`, `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), `profiles`, `dashboard`, `device list`, `doctor`, `install`, `upgrade`, `session list` (with wrapper-managed rows hidden), `session id`, `session info`, `plugin add/list/show/run`, `mcp --help`, and caller-owned saved-state inspection/targeted maintenance (`state list/show/rename` or named clear) as sessionless reads/actions: no implicit managed `--session` under default `sessionMode: "auto"`; broad clear/clean and managed-state targets are rejected before spawn (same session-ownership goal as plain-text `--help` / `--version`), while bare `mcp` server calls are blocked and provider/browser-backed workflows stay thin passthroughs that require upstream setup and credentials | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
89
89
 
90
90
  ## Fastest way to try it
91
91
 
@@ -275,7 +275,7 @@ Render a WebGPU page by enabling upstream's WebGPU launch preset on a fresh loca
275
275
  { "args": ["screenshot", "/tmp/webgpu.png"] }
276
276
  ```
277
277
 
278
- `--webgpu` is also available as `AGENT_BROWSER_WEBGPU` or `"webgpu": true` in upstream `agent-browser.json`; `--webgpu false` overrides an enabled default. It cannot be combined while enabled with `--cdp`, `--auto-connect`, or provider launches. Run `{ "args": ["doctor", "--webgpu"] }` to pixel-check rendering and capture. macOS supports headless WebGPU screenshots; upstream requires a logged-in headed desktop on Windows and `--headed` plus Vulkan loader/Mesa packages on Linux (automatic Xvfb unless `AGENT_BROWSER_NO_XVFB=1`).
278
+ `--webgpu` is also available as `AGENT_BROWSER_WEBGPU`; `--webgpu false` overrides an enabled environment default. Standalone upstream supports `"webgpu": true` in `agent-browser.json`, but browser-backed native calls reject upstream config files as described below. It cannot be combined while enabled with `--cdp`, `--auto-connect`, or provider launches. Run `{ "args": ["doctor", "--webgpu"] }` to pixel-check rendering and capture. macOS supports headless WebGPU screenshots; upstream requires a logged-in headed desktop on Windows and `--headed` plus Vulkan loader/Mesa packages on Linux (automatic Xvfb unless `AGENT_BROWSER_NO_XVFB=1`).
279
279
 
280
280
  Restrict browser and `read` traffic with upstream's domain containment on a fresh local Chrome context:
281
281
 
@@ -297,9 +297,11 @@ Click a visible ref, then refresh refs after navigation or a DOM update:
297
297
  Run a multi-step flow in one tool call:
298
298
 
299
299
  ```json
300
- { "args": ["batch"], "stdin": "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }
300
+ { "args": ["batch", "--bail"], "stdin": "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }
301
301
  ```
302
302
 
303
+ Use exact `batch --bail` when a later content step assumes an earlier navigation succeeded. Without fail-fast behavior, a failed navigation can leave the prior page active; the wrapper rejects the batch when that retained page could be local or unverified. Non-bail continuation remains available when every possible retained page is already verified safe. Splitting navigation and content into separate calls is the other safe option.
304
+
303
305
  If the same `batch` stdin later uses `@e…` on interaction commands after a step that can navigate or mutate the page (`open`, non-form `click`, `reload`, and similar), insert a `snapshot` step whose first argv token is `snapshot` (for example `["snapshot","-i"]`) between those phases. Multiple same-snapshot `fill @e…` steps and native form-control steps (`check`/`uncheck` on checkbox or radio refs, checkbox/radio `click`/`tap` refs, and `select` on combobox refs) may be batched before a final click/submit step. Dynamic or autosubmit forms should still use stable locators or split with a fresh snapshot. The wrapper rejects unsafe ordering with `failureCategory: "stale-ref"` before upstream runs; full rules are under `refSnapshot` in [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details).
304
306
 
305
307
  Read documentation or other unstructured text without launching Chrome, or omit the URL to read the rendered DOM of the current tab:
@@ -367,11 +369,11 @@ Typical pitfalls:
367
369
  - A successful upstream `click` is not proof that the web app handled the event or changed state. For top-level non-Electron direct clicks on `xpath=` targets and eligible current `@e…` refs, the wrapper may fail the tool with `details.clickDispatch` and a `Click dispatch diagnostic` line when upstream reported success but no trusted DOM event reached the resolved target. Raw `find … click` locator calls are not probed because the wrapper has no concrete element before upstream resolves the locator, and document-level probes can falsely fail frame-scoped clicks. `@e…` ref click probes are limited to current snapshot refs with accessible role `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`, using duplicate-name snapshot order when needed. Use the suggested `inspect-click-dispatch-miss` / `retry-click-after-dispatch-miss` next actions instead of assuming the click mutated the page; when `details.clickDispatch.scrollContainer` is present, use `scroll-target-into-view-after-dispatch-miss` first. When the task depends on a mutation, follow `inspect-after-mutation` / `pageChangeSummary` evidence with a wait, URL/text check, or fresh snapshot before trusting the result; if the target still did not change, retry with a current visible ref or stable selector and report the workflow issue instead of silently continuing. For static local fixtures where the user only needs to exercise app code, an explicit `eval --stdin` programmatic click such as `document.querySelector("#demo").click()` can be a diagnostic workaround, but treat it as an untrusted scripted activation rather than proof a real user click works, and never use it to bypass user instructions. Respect explicit user stop boundaries yourself: if the user says to stop before order/post/purchase/submit, gather evidence on that page and do not click the final action. The wrapper does not parse broad prompt text into business-intent action blocks; `details.promptGuard` is reserved for concrete artifact-before-close checks.
368
370
  - A successful `snapshot -i` can surface `Possible overlay blockers` immediately when refs already contain strong dialog/alertdialog evidence plus close/dismiss controls. If a **top-level** `@e…`/`ref=` click succeeds (unified command `click`, not a `batch` step), upstream reports `data.clicked`, and `details.navigationSummary.url` stays on the same tab URL under the same normalization as ref preflight (fragment-insensitive), the wrapper may take one extra `snapshot -i` and add `Possible overlay blockers` with `details.overlayBlockers` (`candidates`, `summary`, optional `snapshot` refresh for refs) plus session-aware `inspect-overlay-state` / bounded `try-overlay-blocker-candidate-*` next actions when that snapshot shows strong modal context (`dialog` / `alertdialog`) and close/dismiss-like controls. Page-wide words like privacy, sign in, or banner alone do not trigger this diagnostic. The unchanged-URL check compares the prior pinned tab target with `details.navigationSummary.url`; CSS selector clicks do not run this overlay probe. Also skipped when tab correction or about-blank recovery already ran on that result.
369
371
  - If `get text <selector>` reads a non-ref, non-simple-id CSS selector with multiple matches or a hidden first match while visible matches exist, including successful `batch` steps, the wrapper may add `Selector text visibility warning`, `details.selectorTextVisibility` (plus `selectorTextVisibilityAll` for multiple batched warnings), and `inspect-visible-text-candidates` next actions; the warning names the matching `details.nextActions` id. Prefer a visible `@ref`, a scoped selector, or a targeted `eval --stdin` over hidden tab content.
370
- - In wrapper-tracked attached Electron sessions, broad selectors such as `body`, `html`, `main`, or `[role=application]` may read the whole app shell. The wrapper may add `Broad Electron get text selector warning`, `details.electronGetTextScopeWarning`, and `snapshot-for-electron-text-scope`; ordinary browser pages, including `file://` fixtures, do not qualify without Electron launch provenance. Prefer `snapshot -i`, a current `@ref`, or a narrower panel selector.
372
+ - In wrapper-tracked attached Electron sessions, broad selectors such as `body`, `html`, `main`, or `[role=application]` may read the whole app shell. The wrapper may add `Broad Electron get text selector warning`, `details.electronGetTextScopeWarning`, and `snapshot-for-electron-text-scope`; ordinary browser pages do not qualify without Electron launch provenance, and local `file://` page follow-ups are blocked before this diagnostic. Prefer `snapshot -i`, a current `@ref`, or a narrower panel selector.
371
373
 
372
374
  ### Constrained browser jobs
373
375
 
374
- For short repeatable workflows, pass a top-level `job` instead of hand-writing `batch` stdin. Keep dynamic app jobs short around navigation, click, and rerender boundaries; avoid packing a whole checkout into one job. The wrapper only supports constrained steps (`open`, `click`, `fill`, `type`, `select`, `wait`, `assertText`, `assertUrl`, `waitForDownload`, `snapshot`, and `screenshot`), compiles them to existing upstream `batch` commands, and echoes the compiled commands as `details.compiledJob` for auditability. `open` steps can include `loadState` (`domcontentloaded`, `load`, or `networkidle`) to insert a readiness wait before the next step. `click` and `fill` steps can use either CSS `selector` or semantic locator fields (`locator`, `role`/`value`, optional `name`) so a job can express flows like role/name search without brittle selectors. `type` can use `selector`, `text`, optional `delayMs` for per-character pacing, and optional `press` for a final key such as `Enter`; paced type compiles to existing `focus`, `keyboard type`, `wait`, and `press` batch rows, is capped at 200 characters per delayed step, and compacts model-visible batch text while full rows remain in `details.batchSteps`. The same compile path backs top-level `qa`, so long `qa` runs surface the same timeout evidence shape. If a long `job`, `qa`, or `batch` hits the wrapper watchdog, `details.timeoutPartialProgress` may recover per-step status (`completed`, `failed`, `pending`, or `unknown`), current page title/URL, declared artifact paths that already exist on disk, and either a `retry-timeout-step` next action for the first incomplete read-only or idempotent step or `inspect-current-page-after-timeout` when the first incomplete step may be mutating and needs state inspection before a shorter follow-up flow (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). There is no separate catalog of reusable named browser recipes above `job`, `qa`, and raw `batch`; see [`docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet`](docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet) for the closed `RQ-0068` decision and when to revisit it.
376
+ For short repeatable workflows, pass a top-level `job` instead of hand-writing `batch` stdin. Keep dynamic app jobs short around navigation, click, and rerender boundaries; avoid packing a whole checkout into one job. The wrapper only supports constrained steps (`open`, `click`, `fill`, `type`, `select`, `wait`, `assertText`, `assertUrl`, `waitForDownload`, `snapshot`, and `screenshot`), compiles them to existing upstream `batch` commands, and echoes the compiled commands as `details.compiledJob` for auditability. `open` steps can include `loadState` (`domcontentloaded`, `load`, or `networkidle`) to insert a readiness wait before the next step. `click` and `fill` steps can use either CSS `selector` or semantic locator fields (`locator`, `role`/`value`, optional `name`) so a job can express flows like role/name search without brittle selectors. `type` can use `selector`, `text`, optional `delayMs` for per-character pacing, and optional `press` for a final key such as `Enter`; paced type compiles to existing `focus`, `keyboard type`, `wait`, and `press` batch rows, is capped at 200 characters per delayed step, and compacts model-visible batch text while full rows remain in `details.batchSteps`. The same compile path backs top-level `qa`, so long `qa` runs surface the same timeout evidence shape. If a long `job`, `qa`, or `batch` hits the wrapper watchdog, `details.timeoutPartialProgress` may recover per-step status (`completed`, `failed`, `pending`, or `unknown`), current page URL plus a title only after a non-file URL is verified, declared artifact paths that already exist on disk, and either a `retry-timeout-step` next action for the first incomplete read-only or idempotent step or `inspect-current-page-after-timeout` when the first incomplete step may be mutating and needs state inspection before a shorter follow-up flow (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). There is no separate catalog of reusable named browser recipes above `job`, `qa`, and raw `batch`; see [`docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet`](docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet) for the closed `RQ-0068` decision and when to revisit it.
375
377
 
376
378
  **Navigation inside `job` is explicit.** A successful `click` does not prove the next page loaded; add `assertUrl` and/or `assertText` after navigation-prone clicks (forms, checkout, tabs, submit buttons) before screenshots or steps that assume the new page. `assertUrl` accepts exact URLs and `*` / `**` glob-style patterns and now compiles directly to upstream `wait --url` for both forms.
377
379
 
@@ -432,7 +434,7 @@ For an app you launched yourself with remote debugging enabled, use raw upstream
432
434
  { "args": ["snapshot", "-i"] }
433
435
  ```
434
436
 
435
- `connect` success means the debug endpoint accepted the session, not that an active page is ready. If a snapshot says `No active page`, the wrapper clears prior refs for that session; choose a stable `t<N>` tab and retry a condition wait or fresh `snapshot -i` before using `@e…` refs. Close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; manually launched apps, their profiles, and explicit screenshots/downloads/HARs/traces/recordings remain host-owned.
437
+ `connect` success means the debug endpoint accepted the session, not that an active page is ready. Use the returned `verify-connected-session-url` (`get url`) action before page-content reads, then inspect/select a stable tab and verify its URL. If a snapshot says `No active page`, the wrapper clears prior refs for that session; choose a stable `t<N>` tab and retry a condition wait or fresh `snapshot -i` before using `@e…` refs. Close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; manually launched apps, their profiles, and explicit screenshots/downloads/HARs/traces/recordings remain host-owned.
436
438
 
437
439
  After either path, use `qa: { "attached": true, ... }` for a current-session smoke check without opening a URL. Attached QA preserves existing network/console/page-error buffers instead of clearing them, so it can catch errors raised before the check started; visible output and `details.compiledQaPreset.checks.diagnosticsResetAtStart` identify that scope. Prefer condition waits (`wait --text`, `wait --url`, `wait --fn`, `wait --load <state>`, `wait --download`), `qa.attached`, `electron.probe` / `electron.status`, `tab list` → `tab t<N>`, fresh snapshots, or screenshots over blind sleeps. Fixed waits are a last resort: use explicit `--timeout` or top-level `timeoutMs` for legitimately slow waits, and treat a result like `"waited":"timeout"` as elapsed time only.
438
440
 
@@ -450,7 +452,7 @@ For a quick smoke/QA pass, use top-level `qa`. It compiles to the same batch pat
450
452
  }
451
453
  ```
452
454
 
453
- Use custom `job` or raw `batch` when you need a different check sequence. `job` defaults to `batch --bail` (`failFast: true`) so later mutating steps do not run after an earlier required step fails; set `failFast: false` only when collecting later diagnostic artifacts is more important than stopping immediately. `qa` always uses fail-fast assertion behavior; omit expected text/selector when you want load-plus-diagnostics only.
455
+ Use custom `job` or raw `batch` when you need a different check sequence. `job` defaults to `batch --bail` (`failFast: true`) so later mutating steps do not run after an earlier required step fails; set `failFast: false` only when later diagnostics remain safe if an earlier navigation fails; navigation-dependent content may require fail-fast behavior or split calls. `qa` always uses fail-fast assertion behavior; omit expected text/selector when you want load-plus-diagnostics only.
454
456
 
455
457
  ### Experimental source lookup
456
458
 
@@ -509,11 +511,12 @@ Use these rules:
509
511
  - For non-Chrome Chromium browsers such as Brave, Edge, Arc, or Vivaldi, use `--executable-path <path>` when upstream can launch that executable. If you need that browser's existing login state, use the browser's real profile/user-data directory path when upstream accepts it, or attach with `--auto-connect` / `connect` to a debug-enabled running browser when appropriate.
510
512
  - Use `sessionMode: "fresh"` when switching from public browsing to `--allowed-domains`, `--profile`, `--executable-path`, `--webgpu`, `--restore`, `--restore-save`, restore check flags, `--namespace`, `--session-name`, `--cdp`, `--state`, `--auto-connect`, `--init-script`, `--enable`, `-p` / `--provider`, or iOS `--device`.
511
513
  - Use `--session` when you want to manage a live upstream session name yourself.
512
- - Do not treat `--session` alone as persisted auth or tab restore after `close`, `quit`, or `exit`; use `--session <id> --restore`, `--profile`, or `--state` for persistence. Upstream 0.31.2 periodically saves restore-enabled cookies/localStorage while the browser is open, including idle page-driven changes; `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` defaults to `30000`, `0` disables periodic saves but keeps save-on-close, and the `never` value for `--restore-save` disables automatic saves for that restore session.
514
+ - Do not treat an arbitrary `--session` name alone as persisted auth after `close`, `quit`, or `exit`. Wrapper-owned managed sessions automatically set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key so cookies/localStorage/sessionStorage survive browser relaunches across Pi chats in the same checkout; the key follows a renamed checkout but changes when that path is replaced or copied, and automatic restore fails closed outside a Git checkout. The wrapper combines the checkout-root and Git-admin filesystem identities with a generation UUID in the Git admin directory and never adopts older cwd-only keys; a bare caller `--session` name does not get that injection, and the wrapper reserves `piab-*` names case-insensitively so another Pi process cannot attach to a managed authenticated browser through a case alias. Disable with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. For explicit non-managed sessions use `--session <id> --restore`, `--profile`, or `--state`. SSO/2FA such as Okta Touch ID may still need one human approval (often `--headed` the first time); after that, managed restore should keep the session without a manual `state save` dance. Any upstream `agent-browser.json` / `AGENT_BROWSER_CONFIG` / `--config` discovered while planning blocks browser-backed native calls without reading it, while accepted browser-backed spawns pin a process-private empty config to close config-creation races. This is separate from this package's trusted Pi-scoped config; sessionless local/setup commands retain upstream config behavior. Raw batch argv, batch stdin containing nested `connect`/`batch`, browser mutation flag, or matching launch-mutation env disables automatic managed restore rather than risking restored auth in a caller-customized or attached browser. Every accepted browser-backed subprocess, including wrapper-owned close, pins `AGENT_BROWSER_CONFIG` to that process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle so a project or user config created between planning and spawn cannot change the browser. A user-private immutable ticket-claim lock serializes each same-identity daemon inspection through the receiving spawn and bridges the pre-update v2 lock path; every lock winner re-inspects the live daemon, and abandoned v2 locks fail closed rather than being reclaimed unsafely. POSIX process identity probes use absolute `/bin/ps` then `/usr/bin/ps` paths. Before an incompatible call, the wrapper inspects the actual same-identity daemon and blocks when it retains any restore key, cannot be inspected, or reports restore-disabled policy without current-process provenance, including daemons missing from transcript state and sessions launched with explicit restore keys. Same-process `session_tree` transitions retain recorded provenance; extension reload, restart, and `/resume` deliberately do not trust transcript-only provenance for a still-live restore-disabled daemon, so close it first, omit the explicit session and use `sessionMode: "fresh"`, or choose a distinct explicit session. If inspection instead proves the old daemon inactive, the next owned no-restore spawn records that null policy so subsequent follow-ups remain usable. Wrapper-owned subprocesses pin the canonical namespace, including an explicit empty default, so a parent `AGENT_BROWSER_NAMESPACE` cannot redirect close or helper calls; Electron status target reads and current-managed probes also acquire the same daemon-policy lock, verify the live URL before title/content reads, and apply the same restore decision to every underlying read. A probe whose reads all fail is an `upstream-error`, not a successful empty partial result. Current-managed probe results persist their namespace and ref state for Pi reload/branch replay. Upstream restore files live under `~/.agent-browser/` and are plaintext unless you set `AGENT_BROWSER_ENCRYPTION_KEY`; on POSIX the wrapper canonicalizes and pins `HOME` after caller env merging, requires owner-trusted non-writable ancestry, requires stable device/inode/birth-time metadata for both checkout and Git-admin directories, enforces mode `0700` without silently tightening unsafe existing directories, and rejects symlinks/non-directories along the exact restore `sessions` path and its `.tmp` write area before automatic managed restore. Windows automatic managed restore requires an absolute `USERPROFILE` and the documented 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY` because POSIX mode checks cannot verify profile ACLs. Wrapper-owned close commands discard caller config/restore globals, preserve the live daemon's existing restore key instead of injecting one derived from a possibly replaced checkout, and record a returned old-generation snapshot against that observed wrapper key. If a fresh command starts agent-browser but then fails, the wrapper probes that exact identity and retains a live or uninspectable daemon for shutdown cleanup instead of abandoning it. After a wrapper-owned managed session closes successfully, the wrapper persists the returned state path as an atomic record in a lockless convergent per-key ownership directory (`0700`, with `0600` records, on POSIX), keeps the two newest proven snapshots for its exact restore key across Pi restarts, self-heals malformed regular records, removes additional proven snapshots older than 30 days, expires ownership-proven snapshots and empty manifests from older restore-key generations only when a private lineage record proves the same canonical checkout path, after 30 days, and caps young close churn at 256 records per restore key; unrecorded matching files and the current checkout key remain untouched. Managed restore keys and key-bearing paths are redacted from tool output and transcripts. `session list` and `state list` hide wrapper-managed rows; cross-checkout managed `--restore` / `--state` / state-file access, broad `state clear`, `state clean`, and managed save/rename targets are rejected before spawn. Browser access to `.agent-browser` storage is blocked through command-specific file operands (including dash-prefixed values), every path-bearing upstream environment mirror (including state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked targets (including not-yet-created descendants of symlinked directories), content-returning local-URL commands, protected artifact destinations and top-level `outputPath`, local-page follow-ups, and persisted unverified top-level or batch tab/attachment/script/state-load transitions. Raw batch command strings are split on literal ASCII spaces exactly like upstream and inspected recursively just like batch stdin arrays; Electron launch handoffs, probes, and later capture share the same boundary: snapshot/tabs handoff and probes verify the live URL before tab/title/content helpers, and cancellation during handoff closes the managed session plus process/profile. The wrapper rejects enabled `--allow-file-access` argv/env plus file-access-enabling or protected-path `--args` / `AGENT_BROWSER_ARGS` values, removes caller file-access occurrences, clears raw-args env, and adds canonical `--args "" --allow-file-access false` defaults so project/user config cannot silently preserve local-page filesystem access; an explicit safe CLI `--args` value may still override the empty default. Post-transition summaries, including after arbitrary `eval`, verify the live URL before title and fail implicit transitions to local file pages; failed navigation attempts remain unverified, and stale concurrent completions cannot overwrite newer unknown page state. `get url`, `tab list`, non-content `tab <id>` selection, explicit safe navigation away, and session/tab close remain available for recovery; tab selection stays unverified until `get url` succeeds. The wrapper repeats checkout, storage, environment, managed-session ownership, and managed-state access validation after async config/socket setup immediately before spawn. On POSIX the selected daemon socket directory must be absolute, current-user-owned, mode `0700`, under trusted ancestry, and free of symlink, foreign-owner, or special planted entries. Pre-existing unsafe modes are rejected rather than repaired, and the check is repeated immediately before spawn. On native Windows, command-first launcher reordering moves only syntactically valid leading globals, rewrites a valued `--restore <name>` as `--restore=<name>` to preserve upstream optional-value semantics, and leaves invalid or command-scoped leading tokens untouched. Upstream periodically saves restore-enabled cookies/localStorage while the browser is open; `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` defaults to `30000`, `0` disables periodic saves but keeps save-on-close, and the `never` value for `--restore-save` disables automatic saves for that restore session.
515
+ - Caller-owned explicit sessions are live-checked with `get url` before content-bearing reads or interactions. Missing or stale transcript page state is not treated as proof of a safe target; if the live URL cannot be verified, the requested content command does not run. Calls to the same effective canonical namespace/session are serialized inside one extension instance; explicit namespace argv overrides `AGENT_BROWSER_NAMESPACE`, including an explicit empty default from that probe through any semantic-action snapshot and the requested command, while different caller-owned sessions remain independent. Raw non-bail batches are rejected when a failed navigation could expose prior local or unverified page content; use exact `batch --bail` or split navigation from content. Protected Windows paths include drive-relative forms such as `C:.agent-browser\\state\\...`. Nested `batch` steps are rejected, and raw batch command strings mirror upstream's ASCII-space tokenizer, including its single/double-quote and backslash handling, without splitting on other Unicode whitespace.
513
516
  - Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
514
517
  - 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` (top-level `job` and `qa` compile to `batch` and supply their own stdin).
515
518
  - 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 on disk, the tool fails with `failureCategory: "artifact-missing"` instead of treating the path as durable. `state load` may mention a path but is not treated as a newly saved artifact.
516
- - Treat `cookies get`, `storage local|session`, and `auth show` output as sensitive. 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.
519
+ - 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.
517
520
  - 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.
518
521
 
519
522
  Safe stateful examples:
@@ -670,8 +673,7 @@ The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/
670
673
  - Targets the current locally installed upstream `agent-browser` version only.
671
674
  - Does not bundle `agent-browser`; users install it separately.
672
675
  - Does not provide a human browser UI inside Pi; the primary UX is agent-invoked tool calls. `--headed` asks upstream to show a browser window, but the wrapper cannot yet prove that the window is visible on the user's desktop.
673
- - Localhost means the browser host's loopback, not necessarily the shell/Pi host. If `http://localhost:<port>` or `http://127.0.0.1:<port>` fails with errors such as `ERR_EMPTY_RESPONSE`, use a host-reachable address when available or a `file://` URL for static fixtures, then verify with `snapshot -i` or an explicit screenshot.
674
- - `file://` pages are useful as a static fallback, but they can behave differently from HTTP pages for MIME types, CORS, storage, and script/debugger behavior. If `eval --stdin` returns `null` or otherwise cannot verify a `file://` page, treat that as inconclusive and use screenshot/snapshot evidence or move the fixture to reachable HTTP.
676
+ - Localhost means the browser host's loopback, not necessarily the shell/Pi host. If `http://localhost:<port>` or `http://127.0.0.1:<port>` fails with errors such as `ERR_EMPTY_RESPONSE`, use an environment-specific host-reachable HTTP(S) address. Do not switch the native wrapper to a `file://` fixture: follow-up inspection and interaction on local file pages is blocked to protect authenticated `.agent-browser` state.
675
677
  - A successful upstream `click` is not proof that the app handled the event. For state-changing flows, verify with a fresh snapshot, text/URL assertion, screenshot, or `pageChangeSummary` before reporting success.
676
678
  - Real authenticated profile use is powerful but sensitive. Treat profile and cookie access as user-approved, task-specific behavior.
677
679
  - Wrapper tab/session recovery is best effort around observed upstream behavior, not a replacement for explicit profile/session design.
@@ -11,7 +11,9 @@ import { fileURLToPath } from "node:url";
11
11
  import { Text } from "@earendil-works/pi-tui";
12
12
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
13
13
  import { SessionPageState } from "./lib/session-page-state.js";
14
- import { createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, extractExplicitNamespace, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, extractExplicitSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
14
+ import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
15
+ import { extractExplicitNamespace, extractExplicitSessionName, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
16
+ import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
15
17
  import { isRecord } from "./lib/parsing.js";
16
18
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
17
19
  import { isCloseCommand } from "./lib/command-taxonomy.js";
@@ -23,8 +25,8 @@ import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./
23
25
  import { parseBatchStdinJsonArray } from "./lib/orchestration/batch-stdin.js";
24
26
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
25
27
  import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
26
- import { applyAgentBrowserOutputPath } from "./lib/orchestration/output-file.js";
27
- import { isSessionArtifactManifest } from "./lib/results/artifact-manifest.js";
28
+ import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError } from "./lib/orchestration/output-file.js";
29
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isSessionArtifactManifest, mergeSessionArtifactManifest } from "./lib/results/artifact-manifest.js";
28
30
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
29
31
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
30
32
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
@@ -67,12 +69,48 @@ function restoreArtifactManifestFromBranch(branch) {
67
69
  if (!message || message.toolName !== "agent_browser")
68
70
  continue;
69
71
  const details = isRecord(message.details) ? message.details : undefined;
70
- if (isSessionArtifactManifest(details?.artifactManifest)) {
72
+ if (isSessionArtifactManifest(details?.artifactManifest) && (!restoredManifest || details.artifactManifest.updatedAtMs >= restoredManifest.updatedAtMs)) {
71
73
  restoredManifest = details.artifactManifest;
72
74
  }
73
75
  }
74
76
  return restoredManifest;
75
77
  }
78
+ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
79
+ let restored;
80
+ const targetKey = getSessionContextKey(sessionName, namespace);
81
+ for (const entry of branch) {
82
+ if (!isRecord(entry) || entry.type !== "message")
83
+ continue;
84
+ const message = isRecord(entry.message) ? entry.message : undefined;
85
+ if (!message || message.toolName !== "agent_browser")
86
+ continue;
87
+ const details = isRecord(message.details) ? message.details : undefined;
88
+ if (!details)
89
+ continue;
90
+ const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
91
+ if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
92
+ continue;
93
+ const recognizedWorkaround = (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
94
+ ? { id: workaround.id, reason: workaround.reason }
95
+ : undefined;
96
+ const succeeded = getSuccessfulToolResult(details, message);
97
+ const outcome = getManagedSessionOutcome(details);
98
+ const activeAfterFailure = recognizedWorkaround
99
+ && outcome?.activeAfter === true
100
+ && typeof outcome.currentSessionName === "string"
101
+ && getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey
102
+ && (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
103
+ if (!succeeded && !activeAfterFailure)
104
+ continue;
105
+ if (recognizedWorkaround) {
106
+ restored = recognizedWorkaround;
107
+ }
108
+ else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details))) {
109
+ restored = undefined;
110
+ }
111
+ }
112
+ return restored;
113
+ }
76
114
  function getToolResultArgs(details) {
77
115
  if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
78
116
  return details.args;
@@ -395,18 +433,18 @@ function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
395
433
  untrackOwnedManagedSession(sessions, sessionName);
396
434
  }
397
435
  }
398
- async function closeOwnedManagedSessionsExcept(sessions, keepSessionName, timeoutMs, keepNamespace) {
436
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, keepNamespace) {
399
437
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
400
438
  for (const [key, owner] of [...sessions]) {
401
439
  if (key === keepKey)
402
440
  continue;
403
- const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, sessionName: owner.sessionName, timeoutMs });
441
+ const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, restoreState, sessionName: owner.sessionName, timeoutMs });
404
442
  if (!error)
405
443
  sessions.delete(key);
406
444
  }
407
445
  }
408
- async function closeOwnedManagedSessions(sessions, timeoutMs) {
409
- await closeOwnedManagedSessionsExcept(sessions, undefined, timeoutMs);
446
+ async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs) {
447
+ await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs);
410
448
  }
411
449
  function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
412
450
  const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
@@ -446,6 +484,49 @@ class AsyncExecutionQueue {
446
484
  })();
447
485
  }
448
486
  }
487
+ class KeyedAsyncExecutionQueue {
488
+ entries = new Map();
489
+ async run(key, work) {
490
+ const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
491
+ entry.users += 1;
492
+ this.entries.set(key, entry);
493
+ try {
494
+ return await entry.queue.run(work);
495
+ }
496
+ finally {
497
+ entry.users -= 1;
498
+ if (entry.users === 0 && this.entries.get(key) === entry)
499
+ this.entries.delete(key);
500
+ }
501
+ }
502
+ }
503
+ function mergeBrowserRunMap(current, initial, updated) {
504
+ if (updated === initial)
505
+ return current;
506
+ const merged = new Map(current);
507
+ for (const [key, value] of updated) {
508
+ if (!initial.has(key) || initial.get(key) !== value)
509
+ merged.set(key, value);
510
+ }
511
+ for (const key of initial.keys()) {
512
+ if (!updated.has(key))
513
+ merged.delete(key);
514
+ }
515
+ return merged;
516
+ }
517
+ function mergeBrowserRunArtifactManifest(current, initial, updated) {
518
+ if (!updated || updated === initial)
519
+ return current;
520
+ const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
521
+ const changedEntries = updated.entries.filter((entry) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry);
522
+ return changedEntries.length === 0
523
+ ? current
524
+ : mergeSessionArtifactManifest({
525
+ base: current,
526
+ entries: changedEntries,
527
+ nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
528
+ });
529
+ }
449
530
  function findPackageRoot(startDir) {
450
531
  let currentDir = startDir;
451
532
  while (true) {
@@ -495,6 +576,7 @@ export default function agentBrowserExtension(pi) {
495
576
  let webSearchToolRegistered = false;
496
577
  let managedSessionActive = false;
497
578
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
579
+ let managedSessionCompatibilityWorkaround;
498
580
  let managedSessionName = managedSessionBaseName;
499
581
  let managedSessionCwd = process.cwd();
500
582
  let managedSessionNamespace;
@@ -508,16 +590,22 @@ export default function agentBrowserExtension(pi) {
508
590
  let ownedElectronLaunchRecords = new Map();
509
591
  let branchOwnedElectronLaunchIds = new Set();
510
592
  let electronChildProcesses = new Map();
593
+ const managedSessionRestoreState = new ManagedSessionRestoreState();
511
594
  const ownedManagedSessions = new Map();
512
595
  const managedSessionExecutionQueue = new AsyncExecutionQueue();
596
+ const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
597
+ let branchRestoreGeneration = 0;
513
598
  let branchStateGeneration = 0;
514
599
  const clearSessionScopedBrowserState = (sessionName, namespace) => {
515
600
  const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
601
+ allowedDomainsBySession = new Map(allowedDomainsBySession);
516
602
  allowedDomainsBySession.delete(key);
603
+ networkRoutesBySession = new Map(networkRoutesBySession);
517
604
  networkRoutesBySession.delete(key);
518
605
  sessionPageState.clearSession(key);
519
606
  };
520
607
  const restoreBranchBackedState = (ctx, options) => {
608
+ branchRestoreGeneration += 1;
521
609
  branchStateGeneration += 1;
522
610
  const previousManagedSessionActive = managedSessionActive;
523
611
  const previousManagedSessionName = managedSessionName;
@@ -526,6 +614,9 @@ export default function agentBrowserExtension(pi) {
526
614
  const branch = ctx.sessionManager.getBranch();
527
615
  const branchResourceEvents = collectBranchManagedResourceEvents(branch);
528
616
  const restoredState = restoreManagedSessionStateFromBranch(branch, managedSessionBaseName);
617
+ managedSessionRestoreState.replace(restoredState.managedSessionRestoreDisabledIdentities, {
618
+ preserveDaemonRestoreKeys: !options.resetRuntimeOwnership,
619
+ });
529
620
  managedSessionActive = restoredState.active;
530
621
  const restoredFreshSessionOrdinal = options.resetRuntimeOwnership
531
622
  ? restoredState.freshSessionOrdinal
@@ -546,6 +637,9 @@ export default function agentBrowserExtension(pi) {
546
637
  : createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
547
638
  : restoredState.sessionName;
548
639
  managedSessionNamespace = shouldReservePostCloseSession ? undefined : restoredState.namespace;
640
+ managedSessionCompatibilityWorkaround = managedSessionActive
641
+ ? restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, managedSessionName, managedSessionNamespace)
642
+ : undefined;
549
643
  managedSessionCwd = ctx.cwd;
550
644
  freshSessionOrdinal = nextFreshSessionOrdinal;
551
645
  sessionPageState = SessionPageState.fromBranch(branch);
@@ -600,6 +694,8 @@ export default function agentBrowserExtension(pi) {
600
694
  });
601
695
  });
602
696
  pi.on("session_shutdown", async (event, ctx) => {
697
+ branchRestoreGeneration += 1;
698
+ branchStateGeneration += 1;
603
699
  let preservedElectronProfileDirs = [];
604
700
  await managedSessionExecutionQueue.run(async () => {
605
701
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
@@ -614,6 +710,7 @@ export default function agentBrowserExtension(pi) {
614
710
  cwd: shutdownCwd,
615
711
  electronChildProcesses,
616
712
  electronLaunchRecords: electronRecordsToCleanup,
713
+ managedSessionRestoreState,
617
714
  timeoutMs: implicitSessionCloseTimeoutMs,
618
715
  });
619
716
  preservedElectronProfileDirs = [...new Set([
@@ -622,13 +719,14 @@ export default function agentBrowserExtension(pi) {
622
719
  ])];
623
720
  syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
624
721
  if (quitting) {
625
- await closeOwnedManagedSessions(ownedManagedSessions, implicitSessionCloseTimeoutMs);
722
+ await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs);
626
723
  }
627
724
  else {
628
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, managedSessionActive ? managedSessionNamespace : undefined);
725
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, managedSessionActive ? managedSessionNamespace : undefined);
629
726
  }
630
727
  });
631
728
  managedSessionActive = false;
729
+ managedSessionCompatibilityWorkaround = undefined;
632
730
  managedSessionNamespace = undefined;
633
731
  sessionPageState.reset();
634
732
  traceOwners = new Map();
@@ -640,6 +738,7 @@ export default function agentBrowserExtension(pi) {
640
738
  branchOwnedElectronLaunchIds = new Set();
641
739
  electronChildProcesses = new Map();
642
740
  ownedManagedSessions.clear();
741
+ cleanupManagedSessionRestoreConfig();
643
742
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
644
743
  });
645
744
  pi.on("before_agent_start", async (event, ctx) => {
@@ -709,6 +808,10 @@ export default function agentBrowserExtension(pi) {
709
808
  if (resolvedInput.status === "invalid") {
710
809
  return buildValidationFailureResult(resolvedInput);
711
810
  }
811
+ const outputPathValidationError = getAgentBrowserOutputPathValidationError(outputPath, ctx.cwd);
812
+ if (outputPathValidationError) {
813
+ return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: outputPathValidationError });
814
+ }
712
815
  const { toolArgs } = resolvedInput;
713
816
  const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
714
817
  const redactedCompiledElectron = resolvedInput.kind === "electron" ? resolvedInput.redactedCompiledElectron : undefined;
@@ -726,6 +829,8 @@ export default function agentBrowserExtension(pi) {
726
829
  implicitSessionCloseTimeoutMs,
727
830
  managedSessionActive,
728
831
  managedSessionName,
832
+ managedSessionNamespace,
833
+ managedSessionRestoreState,
729
834
  redactedCompiledElectron,
730
835
  sessionPageState,
731
836
  signal,
@@ -752,6 +857,7 @@ export default function agentBrowserExtension(pi) {
752
857
  clearSessionScopedBrowserState(closedSessionName);
753
858
  if (closedSessionName === managedSessionName) {
754
859
  managedSessionActive = false;
860
+ managedSessionCompatibilityWorkaround = undefined;
755
861
  managedSessionNamespace = undefined;
756
862
  freshSessionOrdinal += 1;
757
863
  managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
@@ -775,7 +881,11 @@ export default function agentBrowserExtension(pi) {
775
881
  ownedElectronLaunchRecords,
776
882
  ownedManagedSessions,
777
883
  });
884
+ const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
885
+ ? getSessionContextKey(explicitSessionName, resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE)) ?? explicitSessionName
886
+ : undefined;
778
887
  const runBrowserCommand = async () => {
888
+ const branchRestoreGenerationAtStart = branchRestoreGeneration;
779
889
  const generationAtStart = branchStateGeneration;
780
890
  const sessionPageStateUpdate = sessionPageState.beginUpdate();
781
891
  const browserRunState = {
@@ -788,14 +898,20 @@ export default function agentBrowserExtension(pi) {
788
898
  freshSessionOrdinal,
789
899
  managedSessionActive,
790
900
  managedSessionBaseName,
901
+ managedSessionCompatibilityWorkaround,
791
902
  managedSessionCwd,
792
903
  managedSessionName,
793
904
  managedSessionNamespace,
905
+ managedSessionRestoreState,
794
906
  networkRoutesBySession,
907
+ ownedManagedSessions,
795
908
  sessionPageState,
796
909
  traceOwners,
797
910
  };
798
- const result = await runAgentBrowserTool({
911
+ const initialAllowedDomainsBySession = browserRunState.allowedDomainsBySession;
912
+ const initialArtifactManifest = browserRunState.artifactManifest;
913
+ const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
914
+ let result = await runAgentBrowserTool({
799
915
  ctx,
800
916
  cwd: ctx.cwd,
801
917
  electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
@@ -810,13 +926,27 @@ export default function agentBrowserExtension(pi) {
810
926
  signal,
811
927
  state: browserRunState,
812
928
  });
929
+ const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
930
+ if (branchRestoreStillCurrent) {
931
+ allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
932
+ networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
933
+ artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, initialArtifactManifest, browserRunState.artifactManifest);
934
+ if (artifactManifest) {
935
+ result = {
936
+ ...result,
937
+ details: {
938
+ ...(isRecord(result.details) ? result.details : {}),
939
+ artifactManifest,
940
+ artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
941
+ },
942
+ };
943
+ }
944
+ }
813
945
  const branchStateStillCurrent = generationAtStart === branchStateGeneration;
814
946
  if (serializeBrowserCommand || branchStateStillCurrent) {
815
- allowedDomainsBySession = browserRunState.allowedDomainsBySession;
816
- networkRoutesBySession = browserRunState.networkRoutesBySession;
817
- artifactManifest = browserRunState.artifactManifest;
818
947
  freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
819
948
  managedSessionActive = browserRunState.managedSessionActive;
949
+ managedSessionCompatibilityWorkaround = browserRunState.managedSessionCompatibilityWorkaround;
820
950
  managedSessionCwd = browserRunState.managedSessionCwd;
821
951
  managedSessionName = browserRunState.managedSessionName;
822
952
  managedSessionNamespace = browserRunState.managedSessionNamespace;
@@ -835,8 +965,10 @@ export default function agentBrowserExtension(pi) {
835
965
  }
836
966
  return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
837
967
  };
838
- return serializeBrowserCommand
839
- ? managedSessionExecutionQueue.run(runBrowserCommand)
968
+ if (serializeBrowserCommand)
969
+ return managedSessionExecutionQueue.run(runBrowserCommand);
970
+ return callerOwnedSessionQueueKey
971
+ ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, runBrowserCommand)
840
972
  : runBrowserCommand();
841
973
  },
842
974
  });