pi-agent-browser-native 0.9.2 → 0.9.3

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 +14 -0
  2. package/README.md +18 -6
  3. package/dist/extensions/agent-browser/index.js +39 -16
  4. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
  5. package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
  6. package/dist/extensions/agent-browser/lib/native-output.js +2 -1
  7. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
  8. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +58 -7
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
  14. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
  15. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +26 -8
  16. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
  17. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
  18. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
  19. package/dist/extensions/agent-browser/lib/playbook.js +2 -2
  20. package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
  21. package/dist/extensions/agent-browser/lib/process.js +6 -6
  22. package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
  23. package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
  24. package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
  25. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +3 -0
  26. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
  27. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
  28. package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
  29. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
  30. package/dist/extensions/agent-browser/lib/runtime.js +5 -1
  31. package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
  32. package/docs/ARCHITECTURE.md +5 -5
  33. package/docs/COMMAND_REFERENCE.md +13 -9
  34. package/docs/RELEASE.md +6 -5
  35. package/docs/REQUIREMENTS.md +1 -0
  36. package/docs/SESSION_STORAGE.md +1 -1
  37. package/docs/SUPPORT_MATRIX.md +9 -3
  38. package/docs/TOOL_CONTRACT.md +20 -14
  39. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.3 - 2026-10-05
6
+
7
+ ### Fixed
8
+
9
+ - Avoid repeated JSON parsing exceptions for the exact redaction marker and ordinary text that cannot begin JSON, and skip redacting a discarded ref copy in browser-code results. Preserve native grammar validation, secret masking, complete output/spill content, retained snapshot/replay state and direct failure receipts.
10
+ - Run offline native discovery pipeline contracts automatically when the host exports its public instruction-groups factory; retain the explicit force-on diagnostic.
11
+
12
+ - Verify the current complete URL before one-use approved-capture actions in wrapper-owned managed sessions, preserving path/hash drift refusal and unchanged-page success.
13
+ - Preserve exact tab-creation confirmation decisions and visible conditional navigation guidance from narrowly validated transport metadata, without automatically executing continuation arguments.
14
+
15
+ - Complete semantic role click/check/fill and unique accessible select after snapshot approval by reusing only the eligible next-call capture. Render pending snapshots as native confirmation controls rather than empty pages.
16
+
17
+ - Keep the established native confirmation policy across same-session helpers, exact confirm/deny follow-ups, code/advanced calls and resume, without changing caller argv or native first-command precedence. Pending navigation, including native batch rows, never becomes an observed target; completed compound confirmations reconcile their actual target before follow-ups. Replacement cleanup retains the old identity's selected string and continuity state until successful close; native unset still uses native defaults. Exact native guarded-control markers avoid helper-slot replacement without command replay or native ID-validation claims. Completed click/tab/close effects and inner failures reconcile normally; cold pending helpers expose the exact decision before getters dispatch. Electron host helpers retain the setting, and definite unestablished requests no longer overwrite it.
18
+
5
19
  ## 0.9.2 - 2026-10-04
6
20
 
7
21
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@ It is for Pi users who want agents to browse sites, inspect pages, click through
6
6
 
7
7
  ## Pi release qualification
8
8
 
9
- Development host dependencies are pinned to official Pi **1.0.0**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. It does not substitute a fork SDK while leaving official types or child CLI installed.
9
+ CI qualifies the latest stable official Pi and latest maintained fork `main`, resolving their version/commit once per run and retaining SDK/CLI identity evidence. Locked development dependencies are reproducible build snapshots, not qualification targets; runtime peers remain wildcard. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. It does not substitute a fork SDK while leaving official types or child CLI installed.
10
10
 
11
11
  Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Native lifecycle controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
12
12
 
@@ -90,7 +90,7 @@ The result is optimized for agent work:
90
90
  | 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, including raw batch rows and ordered code journals; 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` |
91
91
  | Stale `@eN` refs fail mysteriously | Defines full per-session refs once in the native journal, exposes them on dedicated captures, 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` |
92
92
  | Agents need stable success/failure buckets | Exposes bounded `resultCategory`, `successCategory`, and `failureCategory` on tool `details` for branching without parsing prose; final results return native `isError` and a visible prose failure notice while preserving parseable caller-requested `--json` output. Native structured results expose the same bounded, redacted observations, not internal replay metadata | [`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` |
93
- | Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
93
+ | Clicks can report success without the page receiving the event | Top-level non-Electron direct `click` calls on `xpath=` targets or role-gated current `@e…` refs (`button`, `checkbox`, `menuitem`, `radio`, `switch`, `tab`) install a bounded target-specific DOM-event probe; eligible `@e…` refs require a unique role/name in both the latest snapshot and the live candidates. A per-probe temporary DOM marker must be read back by native `get attr <original-selector> <marker>` before the candidate can verify dispatch, including XPath scope. Missing or mismatched identity cleans up the probe and leaves the native click unverified, not failed. Normal collection cancels the probe timer; if a pending native decision blocks cleanup, listeners and the window marker self-expire after this call's process-timeout budget plus the existing two-second cleanup grace, without another browser command. Duplicate-name refs pass through without a probe: their old ordinal cannot prove which element received the click. If upstream reports success but no trusted event reaches the resolved target, the wrapper fails the tool, exposes `details.clickDispatch`, and suggests explicit retry/inspect next actions (no in-page replay), including a nested-scroll `scrollintoview` action when the probe sees the target outside a scroll container or viewport. Unresolved locator clicks such as raw `find … click` are left upstream-owned to avoid false failures for frame-scoped targets. Other click results still expose `details.pageChangeSummary`; `observed: false` explicitly marks dispatch-only mutation summaries and adds a visible `Action dispatched; application change unverified` warning. Unchanged-URL clicks can surface evidence-backed `details.overlayBlockers` candidates. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), `extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts`, `extensions/agent-browser/lib/results/presentation/navigation.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.extension-click-dispatch.test.ts` |
94
94
  | Dashboard scroll commands can look successful while nothing moves | Handles standard `scroll <dir> [px]` against the document first (including pages whose smooth-scroll CSS defeats upstream wheel timing), falls back upstream when the document cannot move, and samples viewport/containers around the fallback; explicit CSS-container scrolling uses instant movement so smooth-scroll CSS cannot cause a premature no-movement failure; unchanged positions fail as `upstream-error` with `details.scrollNoop`, visible recovery guidance, and exact snapshot/screenshot checks. Unsupported `scrollintoview text=...` fails before dispatch, including inside effective batch rows, and shows exact native `find text ... hover` and snapshot/ref recovery payloads; help remains native pass-through. | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `test/agent-browser.extension-validation.test.ts` |
95
95
  | Dropdown/combobox clicks can focus or hit native option box-model errors | Adds first-class `select <selector> <value...>` paths through native `args` and `agent_browser_action`; semantic role/name or label select resolves exactly one current visible combobox/listbox ref before action. Custom combobox clicks still detect focused controls with explicit `aria-expanded` state but no visible options and return `details.comboboxFocus` plus exact recovery `nextActions` | [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#core-page-and-element-commands), `extensions/agent-browser/lib/input-modes/semantic-action.ts`, `test/agent-browser.extension-input-modes.test.ts`, `test/agent-browser.extension-validation.test.ts` |
96
96
  | Recording workflows fail late when `ffmpeg` is missing or report stale lifecycle state | After successful `record start` / `record restart`, reports `successCategory: "artifact-pending"`, returns an exact `stop-pending-recording` action, warns when older natives report pending output without `ffmpeg`, and conservatively invalidates prior page-scoped `@e…` refs on every executed start attempt and URL-bearing restart to protect older supported natives. This is not evidence of a page change: 0.37 records the active page unless a URL is supplied; FPS-only calls keep the intended tab; an unbounded transcript-backed namespace/session index reserves active destinations across aliases, serializes artifact lifecycle and explicit wait/output writes, persists cross-branch close tombstones, retires every successful close path (including every matching namespace owner for `close --all`), rejects missing/stale restart output, coalesces terminal batch state, keeps only the newest pending path per identity, rejects recording starts after a nested close, folds Unicode path aliases, and retains exact cleanup actions with visible guidance on any later same-session failure | [`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`, `test/agent-browser.presentation-artifacts-batch.test.ts` |
@@ -103,7 +103,7 @@ Artifact results show known requested paths separately from reported/resolved lo
103
103
 
104
104
  ## Fastest way to try it
105
105
 
106
- Use Node 24.21.0 or newer and Pi 1.0.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, pins its direct Pi validation dependencies to 1.0.0, and makes Pi hosts below 1.0.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
106
+ Use Node 24.21.0 or newer and Pi 1.0.0 or newer. This package keeps optional Pi core imports as wildcard `peerDependencies` because Pi package docs require the host Pi install to provide those packages, retains a locked Pi development snapshot while qualifying the latest official/fork graphs, and makes Pi hosts below 1.0.0 a setup failure through `pi-agent-browser-doctor`. There are no compatibility shims for older Pi releases.
107
107
 
108
108
  Install upstream `agent-browser` first and make sure it is on `PATH`:
109
109
 
@@ -327,7 +327,7 @@ Profile hints with `authenticated-only` remain advisory. Global/override profile
327
327
 
328
328
  You usually prompt the agent in natural language. These JSON snippets show the exact native tool shape the agent should use.
329
329
 
330
- Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Omit `--json` unless you need JSON text; structured details are always available.
330
+ Open a page and inspect it (first-call recipe: open → snapshot -i → interact with current `@refs` → snapshot -i after changes). Omit `--json` unless you need JSON text; structured details are available by default. Explicit `--json false` selects native text without trusted row, confirmation, lifecycle, page-target or artifact receipts; see [the output contract](docs/TOOL_CONTRACT.md#wrapper-json).
331
331
 
332
332
  ```json
333
333
  { "args": ["open", "https://example.com"] }
@@ -427,6 +427,8 @@ Evaluate page JavaScript through stdin. Put the script in the top-level `stdin`
427
427
  { "args": ["eval", "--stdin"], "stdin": "({ title: document.title, url: location.href })", "outputPath": "logs/page-state.json" }
428
428
  ```
429
429
 
430
+ Native text cookie reads redact `name=value` values, including empty cookie names; storage reads redact `key: value`, including empty and colon-containing keys, using the same benign-value policy as JSON. Standalone `storage local|session get <key>` and native shorthand `storage local|session <key>` protect the entire sensitive value, including embedded newlines, even when invocation display redaction changes the supplied key. Names and benign preferences remain visible in content, details, spills and exports. Native all-entry storage and raw batches do not escape multiline values, so text redaction is heuristic: use structured JSON when sensitive storage must be unambiguous.
431
+
430
432
  Use `outputPath` when `eval`, `get`, `snapshot`, or another extraction should be saved as a durable workspace file. Recording results also export on failure or timeout: their JSON envelope retains the failed attempt, native receipt, verification and any recovery evidence rather than writing misleading bare success data. Keep it distinct from screenshot, download, recording, and other browser artifact destinations; preflight rejects known same-call aliases before browser activity, and the result writer preserves the browser artifact if an alias becomes apparent only afterward. The wrapper writes `details.data` when present, otherwise the model-facing text content. When presentation compacted a large direct result, a result row, or the whole `batch`, it instead reads the full command-redacted pre-compaction payload only from the corresponding live wrapper-managed spill recorded in `details.artifactManifest`; if any required spill is unavailable or untrusted, the call fails without writing compact metadata to the requested path. `details.outputFile` reports the saved path and byte count. Explicit upstream `--json` content stays parseable, including early snapshot/network filters, scroll results, and preparation failures; metadata stays in `details` and the save notice lives only in `details.outputFile`. Help/version output remains native text.
431
433
 
432
434
  Recording receipts separate captured-frame rate and wall-clock capture duration from nominal/output FPS. `details.artifacts[].recording` includes native capture timestamps, received frame counts (not pixel-unique frames), encoded/written/held/dropped/skipped counts, and output duration. Missing native metrics stay unknown. Repeated, static, sparse or final-state-only frames cannot establish UI smoothness.
@@ -561,7 +563,9 @@ Each automatic root has its own stable native restore key. Native save/restore p
561
563
 
562
564
  Explicit native `session` defaults (config or `AGENT_BROWSER_SESSION`) still override automatic root selection, including `sessionMode: "fresh"`. They intentionally share whichever browser the caller selected. Per-call session/namespace flags retain precedence. Ordinary launch options such as `--profile` or `--executable-path` still use the root-group name. Repeat explicit options when deliberately relaunching a persistent per-root profile after close. Explicit `fresh` and attachment choices retain their existing managed-session lifecycle; later auto calls follow that selected managed session. Use `session info` with the reported `sessionName` for read-only native daemon/browser evidence. Unknown native fields remain unknown.
563
565
 
564
- Policy-required URL reads keep the two-call read → confirm/deny flow. Returned actions name the actual native namespace/session, including `default`; routing survives transcript resume and branch changes. Only explicit-read provenance plus native `capabilities.readRequiresConfirmation: true` permits matching confirm/deny without page helpers. This capability includes native confirmation-ID checking. Legacy prompts retain correct routing but normal page checks; DOM or content-shaped prompts never receive the exemption.
566
+ Set `--confirm-actions` on the session's first command. Its established setting follows same-namespace/session helpers and ordinary, code, advanced and resumed calls, so exact returned confirm/deny actions work without repeating the policy. Bare follow-ups retain a selected string, including explicit `""`, over ambient environment or discovered-config changes. Native unset is not retained and keeps ordinary native default resolution. A new explicit `--confirm-actions` or `--config` selection uses native argv → environment → config precedence and can restart the daemon, losing pending confirmations. Successful close retires the setting. Replacement cleanup uses the old identity's setting and retains its continuity state if close fails or needs confirmation. Electron launch and host helpers use the same selected setting. Definite pre-dispatch, native CLI parse/config and local-inspection rejections do not replace an established setting unless an earlier browser helper admitted it; interrupted work retains its selected intent.
567
+
568
+ Native confirmation controls retain their exact ID, namespace/session and compact command/action provenance in the existing transcript state. Use the exact returned confirm/deny action: matching guarded decisions skip page helpers that could overwrite the native pending slot, then reconcile the completed browser action normally. In native batch, that helper suppression applies only to a matching first effective decision; raw arguments win over stdin and later page/ref checks remain. This does not prove native ID validation or make the action browser-independent. Page content cannot create this provenance. A pending `tab_new` transport may retain paired `after_confirmation: ["open", <requested URL>]` and nonempty `guidance` only when the continuation matches the original tab URL. Its conditional guidance stays visible: approve to create the blank/labeled tab, then explicitly navigate that new tab; denial never authorizes the continuation. The extension does not execute these arguments automatically. Completed confirmed full snapshots install their refs, including in batch row order. A final confirmed capture can supply the next same-identity call's freshness sample once, including semantic role click/check/fill and unique role/label select resolution; intervening calls or later rows consume that opportunity. Pending snapshots render a confirmation control, not an empty page. Target/membership/invalidation and native-generation checks still apply. On this one use, live verification compares the complete capture URL, including fragments, and rejects refs when it changes—including wrapper-owned managed sessions and warm sessions without resume. Ordinary ref/tab URL comparisons remain fragment-insensitive. Only a DOM-only rename on the same page after that sample is not resampled on this one use. Further ref operations use the normal freshness check. Policy-required explicit URL reads keep their separate two-call flow; only `capabilities.readRequiresConfirmation: true` grants browser-independent confirmation with native ID checking. Legacy URL-read prompts retain correct routing but normal page checks. A pending cold-reopen helper exposes its decision without dispatching the requested getter or replaying the original open.
565
569
 
566
570
  Full live identity, browser-independent native reads/confirmations and detailed recording receipts require the companion upstream fixes; the current recommended release does not yet supply all of them. Older supported versions remain accepted with unavailable evidence marked unknown. The extension does not upgrade or restart your installed browser to obtain these fields.
567
571
 
@@ -737,6 +741,14 @@ The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/
737
741
  - Real authenticated profile use is powerful but sensitive. Treat profile and cookie access as user-approved, task-specific behavior.
738
742
  - Wrapper tab/session recovery is best effort around observed upstream behavior, not a replacement for explicit profile/session design.
739
743
 
744
+ ## Automatic npm releases (maintainers)
745
+
746
+ Follow the [shared release procedure](https://github.com/fitchmultz/.github#automatic-npm-releases): merge a reviewed PR into `main` with an intentional `package.json` version bump and a matching versioned `CHANGELOG.md` section. Automation never bumps versions, overwrites releases, or republishes an existing version. Once configured and enabled, it runs existing compatibility and native lifecycle checks, qualifies one candidate tarball, then waits for `fitchmultz` approval in the `npm` environment.
747
+
748
+ Approve only after checking the exact source commit, version, downloaded candidate tarball and summary against [the existing release gates](docs/RELEASE.md#pre-release-checks), including required lifecycle/live-site evidence, artifact checks and all three final reviewer approvals. Apply the documented Windows waiver without claiming a Windows or full composed-gate pass. Approval attests genuine satisfaction of those requirements; it does not create proof. Authenticated, model-backed and paid checks stay outside CI. Automated publication disables npm hooks to publish the already checked bytes; the manual publisher instructions and their gates remain valid.
749
+
750
+ Failed/unpublished candidates can retry daily at 12:17 UTC or via manual dispatch of `npm release` on `main`, without another bump. Set repository variable `NPM_RELEASE_ENABLED` to anything other than `true` to stop new release plans; cancel pending runs separately when needed. Workflow validation is not evidence of a completed real OIDC publication.
751
+
740
752
  ## Local development
741
753
 
742
754
  Install upstream `agent-browser`, then install dependencies:
@@ -745,7 +757,7 @@ Install upstream `agent-browser`, then install dependencies:
745
757
  npm install
746
758
  ```
747
759
 
748
- Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.2.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 1.0.0 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
760
+ 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; locked Pi devDependencies are reproducible build snapshots, not qualification targets. CI selects the once-resolved latest official graph for native lifecycle and package smoke, and independently qualifies the latest maintained fork. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
749
761
 
750
762
  Checkout-only extension smoke test:
751
763
 
@@ -9,7 +9,7 @@ import { SessionPageState } from "./lib/session-page-state.js";
9
9
  import { appendBrowserTransition, applyArtifactChanges, artifactChanges, browserStateEffects, getBrowserRecord, getBrowserResultMessage, pageChange, snapshotDefinition } from "./lib/browser-transcript.js";
10
10
  import { appendBrowserRecord, captureBrowserBranch, hasPublishedBrowserJournal, readBrowserEntries, requirePublishedBrowserJournal } from "./lib/browser-journal.js";
11
11
  import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, isPlainTextInspectionArgs, } from "./lib/runtime.js";
12
- import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
12
+ import { isBooleanFlagEnabled, deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
13
13
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
14
14
  import { needsManagedSession } from "./lib/command-policy.js";
15
15
  import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
@@ -43,7 +43,7 @@ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, apply
43
43
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
44
44
  import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
45
45
  import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
46
- import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
46
+ import { isSuccessfulNativeConfirmedClose, scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
47
47
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
48
48
  import { AgentBrowserResultComponent, formatAgentBrowserRenderCall, } from "./lib/pi-tool-rendering.js";
49
49
  function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
@@ -113,6 +113,8 @@ function getArtifactPreflightValidationError(options) {
113
113
  const artifactDestinations = new Map();
114
114
  let sawBatchClose = false;
115
115
  for (const [index, commandStep] of steps.entries()) {
116
+ if (commandStep.length === 0)
117
+ continue; // Native skips empty stdin rows.
116
118
  if (batch) {
117
119
  const stepValidationError = validateToolArgs(commandStep, { batchStep: true });
118
120
  if (stepValidationError)
@@ -288,7 +290,7 @@ function restoreAttachedSessionKeysFromBranch(branch) {
288
290
  if (!sessionName)
289
291
  continue;
290
292
  const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
291
- if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
293
+ if ((succeeded && (isCloseCommand(typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0]) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(args), details.data))) || terminalBatchClose)
292
294
  attachedSessionKeys.delete(sessionKey);
293
295
  else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
294
296
  attachedSessionKeys.add(sessionKey);
@@ -563,10 +565,11 @@ function collectBranchManagedResourceEvents(branch) {
563
565
  const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
564
566
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
565
567
  }
566
- if (succeeded && !isCloseCommand(command) && sessionName && ((!explicitSessionName && (usedImplicitSession || sessionMode === "fresh")) || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
568
+ const closesSession = isCloseCommand(command) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(getToolResultArgs(details)), details.data);
569
+ if (succeeded && !closesSession && sessionName && ((!explicitSessionName && (usedImplicitSession || sessionMode === "fresh")) || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
567
570
  setBranchManagedSessionActive(events, sessionName, namespace, eventRank);
568
571
  }
569
- if (succeeded && isCloseCommand(command)) {
572
+ if (succeeded && closesSession) {
570
573
  setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
571
574
  }
572
575
  if (closeAllApplied) {
@@ -618,12 +621,12 @@ function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNa
618
621
  untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
619
622
  }
620
623
  }
621
- async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
624
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, sessionPageState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
622
625
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
623
626
  for (const [key, owner] of [...sessions]) {
624
627
  if (key === keepKey)
625
628
  continue;
626
- const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, socketDir: owner.socketDir, timeoutMs });
629
+ const error = await closeManagedSession({ confirmActions: sessionPageState.get(key).confirmActions ?? process.env.AGENT_BROWSER_CONFIRM_ACTIONS, cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, socketDir: owner.socketDir, timeoutMs });
627
630
  if (!error) {
628
631
  sessions.delete(key);
629
632
  onClosed?.(owner);
@@ -943,6 +946,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
943
946
  };
944
947
  const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
945
948
  const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
949
+ confirmActions: sessionPageState.get(getAgentBrowserSessionIdentityKey(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE)).confirmActions,
946
950
  cwd,
947
951
  namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
948
952
  restoreState: managedSessionRestoreState,
@@ -991,6 +995,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
991
995
  const previousManagedSessionName = managedSessionName;
992
996
  const previousFreshSessionOrdinal = freshSessionOrdinal;
993
997
  const previousAttachedSessionKeys = attachedSessionKeys;
998
+ const previousSessionPageState = sessionPageState;
994
999
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
995
1000
  browserOwnerSessionId = ctx.sessionManager.getSessionId();
996
1001
  let branch;
@@ -1160,6 +1165,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1160
1165
  }
1161
1166
  }
1162
1167
  if (!options.resetRuntimeOwnership) {
1168
+ // Off-branch live owners still need their established setting for cleanup.
1169
+ for (const key of ownedManagedSessions.keys()) {
1170
+ if (!branchResourceEvents.managedSessionActiveIdentities.has(key))
1171
+ sessionPageState.setConfirmActions(key, previousSessionPageState.get(key).confirmActions);
1172
+ }
1163
1173
  for (const sessionKey of previousAttachedSessionKeys) {
1164
1174
  if (ownedManagedSessions.has(sessionKey))
1165
1175
  attachedSessionKeys.add(sessionKey);
@@ -1239,6 +1249,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1239
1249
  electronLaunchRecords: electronRecordsToCleanup,
1240
1250
  managedSessionRestoreState,
1241
1251
  ownedManagedSessions,
1252
+ sessionPageState,
1242
1253
  timeoutMs: implicitSessionCloseTimeoutMs,
1243
1254
  });
1244
1255
  preservedElectronProfileDirs = [...new Set([
@@ -1249,10 +1260,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1249
1260
  for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
1250
1261
  retireRecordingSession(identity.sessionName, identity.namespace);
1251
1262
  if (quitting) {
1252
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
1263
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, sessionPageState, undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
1253
1264
  }
1254
1265
  else {
1255
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
1266
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, sessionPageState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
1256
1267
  }
1257
1268
  flushRecordingReservations();
1258
1269
  notifyRecordingPersistence(ctx);
@@ -1404,11 +1415,11 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1404
1415
  await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
1405
1416
  const runtimeBrowserConfig = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
1406
1417
  const rootProfile = runtimeBrowserConfig.trustedBrowserDefaultProfile;
1407
- const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
1418
+ const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE), resolvedInput.toolStdin);
1408
1419
  const rootSessionId = process.env.PI_SUBAGENT_CHILD === "1" && process.env.PI_SUBAGENT_ROOT_SESSION_ID
1409
1420
  ? process.env.PI_SUBAGENT_ROOT_SESSION_ID : ctx.sessionManager.getSessionId();
1410
1421
  return withNativeSessionDefaults(resolvedInput, browserCwd, signal, async (resolvedInput, withLaunchDefaults) => {
1411
- const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
1422
+ const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE), resolvedInput.toolStdin);
1412
1423
  if (readConfirmation)
1413
1424
  resolvedInput = { ...resolvedInput, toolArgs: scopeReadConfirmationArgs(resolvedInput.toolArgs, readConfirmation) };
1414
1425
  if (resolvedInput.kind === "qa" && resolvedInput.compiledQaPreset.checks.attached && !managedSessionActive && !extractExplicitSessionName(resolvedInput.toolArgs)) {
@@ -1657,13 +1668,16 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1657
1668
  && (needsManagedSession(parseArgvDescriptor(toolArgs), resolvedInput.toolStdin) || (selectedPlan.startupScopedFlags?.length ?? 0) > 0);
1658
1669
  const operationId = randomUUID();
1659
1670
  const selectedKey = getAgentBrowserSessionIdentityKey(selectedPlan.sessionName ?? "default", selectedPlan.namespace);
1671
+ browserRunState.confirmationPolicyIdentity = selectedKey;
1672
+ const confirmActions = getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS;
1673
+ workingPageState.setConfirmActions(selectedKey, confirmActions);
1660
1674
  const begin = { event: {
1661
1675
  version: 1, phase: "begin", operationId, toolCallId, commandIndex: commandIndex ?? 0, isError: true,
1662
1676
  state: { args: redactInvocationArgs(toolArgs), sessionName: selectedPlan.sessionName, namespace: selectedPlan.namespace,
1663
1677
  ownerSessionId: ctx.sessionManager.getSessionId(), usedImplicitSession: selectedPlan.usedImplicitSession,
1664
1678
  wrapperManaged: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey), managedSessionCwd: browserCwd,
1665
1679
  managedSessionSocketDir: resolveAgentBrowserSocketDir({ ownedManagedSession: selectedPlan.managedSessionName !== undefined || ownedManagedSessions.has(selectedKey) }) },
1666
- pages: [{ key: selectedKey, refs: { kind: "unknown" }, unknown: true }],
1680
+ pages: [{ key: selectedKey, confirmActions: confirmActions ?? null, refs: { kind: "unknown" }, unknown: true }],
1667
1681
  } };
1668
1682
  if (browserAffecting) {
1669
1683
  try {
@@ -1716,6 +1730,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1716
1730
  result = { ...failure, details: { ...browserRunState.observedBrowserEffects, ...(isRecord(failure.details) ? failure.details : {}) } };
1717
1731
  }
1718
1732
  const branchRestoreStillCurrent = branch.isCurrent();
1733
+ if (browserRunState.confirmationPolicyMayBeEstablished === false)
1734
+ workingPageState.setConfirmActions(selectedKey, priorPages.get(selectedKey)?.confirmActions);
1719
1735
  const resultDetails = isRecord(result.details) ? result.details : undefined;
1720
1736
  if (resultDetails && begin.event.state.wrapperManaged === true)
1721
1737
  resultDetails.managedSessionSocketDir = begin.event.state.managedSessionSocketDir;
@@ -1736,7 +1752,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1736
1752
  const closeAllApplied = resultDetails?.closeAllApplied === true;
1737
1753
  const attachedSessionRemainsActive = result.isError !== true
1738
1754
  || ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
1739
- const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
1755
+ const closesAttachedSession = (result.isError !== true && (isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(toolArgs), resultDetails?.data)))
1740
1756
  || resultBatchCloseLifecycle?.endsClosed === true;
1741
1757
  if (closeAllApplied) {
1742
1758
  deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
@@ -1835,7 +1851,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1835
1851
  delete result.details.refSnapshot;
1836
1852
  result.details.artifactManifest = invocationArtifactManifest(branch.isCurrent() ? artifactManifest : browserRunState.artifactManifest, initialArtifactManifest, result.details);
1837
1853
  }
1838
- return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1854
+ return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && isBooleanFlagEnabled(params.args, "--json"), result: warnRecordingPersistence(result) });
1839
1855
  };
1840
1856
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1841
1857
  const runWithLaunchDefaults = async () => {
@@ -1847,7 +1863,14 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1847
1863
  stdin: resolvedInput.toolStdin,
1848
1864
  });
1849
1865
  const owner = ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1850
- return withAgentBrowserProcessEnvironment(owner?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}, async () => {
1866
+ // Resolve inside the identity queue, before daemon inspection or any page helper.
1867
+ const explicitConfirmationSetting = ["--confirm-actions", "--config"].some(flag => scanUpstreamGlobalFlagOccurrences(toolArgs, flag).length > 0);
1868
+ const retainedConfirmActions = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).confirmActions;
1869
+ const confirmActions = !explicitConfirmationSetting && retainedConfirmActions !== undefined ? retainedConfirmActions : resolvedInput.nativeConfirmActions;
1870
+ return withAgentBrowserProcessEnvironment({
1871
+ ...(owner?.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: owner.socketDir } : {}),
1872
+ AGENT_BROWSER_CONFIRM_ACTIONS: confirmActions,
1873
+ }, async () => {
1851
1874
  const run = (executionSignal = signal) => {
1852
1875
  const execute = () => withLaunchDefaults
1853
1876
  ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
@@ -2033,7 +2056,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
2033
2056
  if (result.isError && details.error === undefined)
2034
2057
  details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
2035
2058
  const priorManifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
2036
- const renderOptions = { content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true,
2059
+ const renderOptions = { content: result.content, details, json: "code" in params || isBooleanFlagEnabled(params.args ?? [], "--json"), succeeded: result.isError !== true,
2037
2060
  persistentArtifactStore: branch.isCurrent() ? getPersistentSessionArtifactStore(ctx) : undefined,
2038
2061
  withArtifactWrite: (write) => artifactExecutionQueue.run(() => {
2039
2062
  if (!branch.isCurrent())
@@ -1,6 +1,8 @@
1
1
  import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
3
3
  import { isRecord } from "./parsing.js";
4
+ import { isSuccessfulNativeConfirmedClose } from "./read-confirmation.js";
5
+ import { detectConfirmationRequired } from "./results/confirmation.js";
4
6
  export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
5
7
  if (!Array.isArray(data))
6
8
  return false;
@@ -10,7 +12,7 @@ export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
10
12
  const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
11
13
  ? row.command
12
14
  : fallbackCommands[index];
13
- return rowCommand ? isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
15
+ return rowCommand ? !detectConfirmationRequired(row.result) && isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
14
16
  });
15
17
  }
16
18
  export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
@@ -41,12 +43,13 @@ export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
41
43
  continue;
42
44
  }
43
45
  const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
44
- if (stepSucceeded && isCloseCommand(command)) {
46
+ if (stepSucceeded && !detectConfirmationRequired(result) && (isCloseCommand(command) || isSuccessfulNativeConfirmedClose(extractUpstreamCommandTokens(rowCommand), result))) {
45
47
  sawClose = true;
46
48
  endsClosed = true;
47
49
  browserActiveAfterClose = false;
48
50
  recordingClosedAfterBatch = true;
49
- statePath = typeof result?.statePath === "string" ? result.statePath : undefined;
51
+ const closeData = command === "confirm" && isRecord(result?.result) ? result.result.data : result;
52
+ statePath = isRecord(closeData) && typeof closeData.statePath === "string" ? closeData.statePath : undefined;
50
53
  }
51
54
  else if (sawClose && command === "record") {
52
55
  if (browserLaunched !== false) {
@@ -44,6 +44,7 @@ export function getBrowserRecord(entry) {
44
44
  }
45
45
  if (event.pages !== undefined && (!Array.isArray(event.pages) || !event.pages.every(page => isRecord(page)
46
46
  && typeof page.key === "string" && isRecord(page.refs) && ["reuse", "replace", "invalidate", "unknown"].includes(String(page.refs.kind))
47
+ && (page.confirmActions === undefined || page.confirmActions === null || typeof page.confirmActions === "string")
47
48
  && (!["reuse", "replace"].includes(String(page.refs.kind)) || typeof page.refs.snapshotId === "string" && page.refs.snapshotId.length > 0)
48
49
  && (page.target === undefined || isRecord(page.target) && normalizeSessionTabTarget(page.target) !== undefined)
49
50
  && (page.unknown === undefined || page.unknown === true) && (page.clear === undefined || page.clear === true)
@@ -96,7 +97,7 @@ export function snapshotFromDefinition(definition) {
96
97
  }
97
98
  export function pageChange(key, page, previousSnapshotId) {
98
99
  return {
99
- key, target: page.tabTarget, unknown: page.tabTargetUnknown, reopenPending: page.tabReopenPending, pinningReason: page.pinningReason,
100
+ key, confirmActions: page.confirmActions ?? null, target: page.tabTarget, unknown: page.tabTargetUnknown, reopenPending: page.tabReopenPending, pinningReason: page.pinningReason,
100
101
  refs: page.tabTargetUnknown ? { kind: "unknown", invalidation: page.refSnapshotInvalidation }
101
102
  : page.refSnapshot && !targetsMatch(page.refSnapshot.target, page.tabTarget) ? { kind: "invalidate", invalidation: buildPageTransitionRefSnapshotInvalidation(`The saved refs came from a snapshot for ${page.refSnapshot.target?.url}; the current session target is ${page.tabTarget?.url}. Take a fresh snapshot before using page-scoped refs.`) }
102
103
  : page.refSnapshot?.snapshotId ? { kind: page.refSnapshot.snapshotId === previousSnapshotId ? "reuse" : "replace", snapshotId: page.refSnapshot.snapshotId }
@@ -1,5 +1,6 @@
1
1
  import { JsonSchema } from "./json-schema.js";
2
2
  import { isRecord } from "./parsing.js";
3
+ import { isBooleanFlagEnabled } from "./argv-grammar.js";
3
4
  import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
4
5
  import { isPlainTextInspectionArgs } from "./runtime.js";
5
6
  import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
@@ -32,7 +33,7 @@ export async function finalizeAgentBrowserNativeResult(result, input) {
32
33
  const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
33
34
  const rendered = await renderAgentBrowserObservation({
34
35
  content: finalized.content, details, succeeded,
35
- json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args.includes("--json")),
36
+ json: isRecord(input) && ("code" in input || Array.isArray(input.args) && isBooleanFlagEnabled(input.args, "--json")),
36
37
  // The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
37
38
  preserveContent: true,
38
39
  });
@@ -69,13 +69,7 @@ export function parseBatchCommandArgument(command) {
69
69
  function validateUserBatchStep(step, index) {
70
70
  if (!Array.isArray(step)) {
71
71
  return {
72
- error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
73
- ok: false,
74
- };
75
- }
76
- if (step.length === 0) {
77
- return {
78
- error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
72
+ error: `agent_browser batch stdin step ${index} must be an array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
79
73
  ok: false,
80
74
  };
81
75
  }
@@ -137,5 +131,5 @@ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
137
131
  });
138
132
  if (argumentSteps.length > 0)
139
133
  return argumentSteps;
140
- return parseUserBatchStdin(stdin).steps ?? [];
134
+ return parseUserBatchStdin(stdin).steps?.filter(step => step.length > 0) ?? [];
141
135
  }
@@ -2,6 +2,8 @@ import { isRecord, parseRefId } from "../../parsing.js";
2
2
  import { redactSensitiveText } from "../../runtime.js";
3
3
  import { withOptionalSessionArgs } from "../../results/next-actions.js";
4
4
  import { runSessionCommandData } from "./session-state.js";
5
+ import { getAgentBrowserProcessTimeoutMs } from "../../process.js";
6
+ import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
5
7
  const CLICK_DISPATCH_MARKER_PREFIX = "__piAgentBrowserClickDispatchProbe_";
6
8
  const CLICK_DISPATCH_CLEANUP_TIMEOUT_MS = 2_000;
7
9
  const ACCESSIBLE_REF_CLICK_DISPATCH_ROLES = new Set(["button", "checkbox", "menuitem", "radio", "switch", "tab"]);
@@ -34,7 +36,7 @@ function getEvalResultRecord(data) {
34
36
  function getClickDispatchIdentityAttribute(probe) {
35
37
  return `data-pi-click-dispatch-${probe.marker.toLowerCase()}`;
36
38
  }
37
- function buildClickDispatchProbeInstallScript(probe) {
39
+ function buildClickDispatchProbeInstallScript(probe, timeoutMs) {
38
40
  const target = probe.target;
39
41
  const resolveTarget = target.kind === "selector"
40
42
  ? `(() => { try { return document.querySelector(${JSON.stringify(target.selector)}); } catch { return null; } })()`
@@ -129,10 +131,15 @@ const listeners = eventTypes.map((type) => {
129
131
  });
130
132
  const identityAttribute = ${JSON.stringify(getClickDispatchIdentityAttribute(probe))};
131
133
  state.removeIdentityMarker = () => element.removeAttribute(identityAttribute);
134
+ let cleanupTimer;
132
135
  state.cleanup = () => {
136
+ clearTimeout(cleanupTimer);
133
137
  state.removeIdentityMarker();
134
138
  listeners.forEach(([type, listener]) => document.removeEventListener(type, listener, true));
139
+ try { delete window[marker]; } catch {}
135
140
  };
141
+ // A native pending decision can forbid cleanup eval; expire without another command.
142
+ cleanupTimer = setTimeout(state.cleanup, ${timeoutMs + CLICK_DISPATCH_CLEANUP_TIMEOUT_MS});
136
143
  window[marker] = state;
137
144
  element.setAttribute(identityAttribute, marker);
138
145
  return { status: "installed", marker, target: state.target };
@@ -205,13 +212,16 @@ export function buildClickDispatchNextActions(options) {
205
212
  export async function prepareClickDispatchProbe(options) {
206
213
  if (!options.sessionName || options.commandTokens[0] !== "click" || options.commandTokens.includes("--new-tab"))
207
214
  return undefined;
215
+ // This optional diagnostic must not require approving a fresh eval on every click retry.
216
+ if (getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS?.split(",").some(action => action.trim().toLowerCase() === "evaluate"))
217
+ return undefined;
208
218
  const target = getClickDispatchProbeTarget(options.commandTokens, options.refSnapshot);
209
219
  if (!target)
210
220
  return undefined;
211
221
  const probe = { marker: `${CLICK_DISPATCH_MARKER_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`, target };
212
222
  let prepared = false;
213
223
  try {
214
- const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe) });
224
+ const installData = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeInstallScript(probe, options.timeoutMs ?? getAgentBrowserProcessTimeoutMs()) });
215
225
  if (getEvalResultRecord(installData)?.status !== "installed")
216
226
  return undefined;
217
227
  // Name matching and in-page XPath only find candidates. Native resolution
@@ -1,11 +1,18 @@
1
1
  import { runAgentBrowserProcess, withAttachedBrowserSessionContext, withChromeStartupArgs } from "../../process.js";
2
2
  import { isRecord } from "../../parsing.js";
3
+ import { getAgentBrowserSessionIdentityKey, isBooleanFlagEnabled, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
4
+ import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
5
+ import { parseArgvDescriptor } from "../../argv-descriptor.js";
6
+ import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
7
+ import { nextReadConfirmation, parseReadConfirmation, suppressConfirmationPageHelpers } from "../../read-confirmation.js";
8
+ import { buildToolPresentation } from "../../results/presentation.js";
9
+ import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
3
10
  import { collectNativeWebMcp, getNativeWebMcpCatalog } from "../../webmcp-observation.js";
4
11
  import { isPlainTextInspectionArgs, redactSensitiveValue } from "../../runtime.js";
5
12
  import { formatWebMcpCatalogUpdate } from "../../results/presentation/common.js";
6
13
  import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
7
14
  import { cleanupClickDispatchProbe } from "./click-dispatch.js";
8
- import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
15
+ import { applyBrowserRunStatePatch, getSessionContextKey, nativePolicyDefinitelyUnestablished, withSessionCommandObservation } from "./session-state.js";
9
16
  import { buildScreenshotGeometry, collectScreenshotSample } from "./screenshot-observation.js";
10
17
  import { buildJsonVisibleContent, buildMissingBinaryFailureResult } from "./final-result.js";
11
18
  import { prepareBrowserRun } from "./prepare.js";
@@ -13,14 +20,42 @@ import { processBrowserOutput } from "./process-output.js";
13
20
  export { closeManagedSession } from "./managed-session-daemon-policy.js";
14
21
  export { getSessionContextKey } from "./session-state.js";
15
22
  export async function runAgentBrowserTool(options) {
16
- const observed = await collectNativeWebMcp(() => withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options))));
23
+ const observed = await collectNativeWebMcp(() => withSessionCommandObservation({
24
+ allow: helper => {
25
+ const pending = parseReadConfirmation(options.state.observedBrowserEffects?.readConfirmation);
26
+ return !needsManagedSession(parseArgvDescriptor(helper.args), helper.stdin) && !isBrowserIndependentRead(helper.args, helper.stdin)
27
+ || pending?.state !== "pending" || getAgentBrowserSessionIdentityKey(helper.sessionName ?? "default", resolveAgentBrowserNamespace([], helper.namespace ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)) !== getAgentBrowserSessionIdentityKey(pending.sessionName, pending.namespace);
28
+ },
29
+ observe: (helper, processResult, envelope) => {
30
+ const namespace = resolveAgentBrowserNamespace([], helper.namespace ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
31
+ const sessionName = helper.sessionName ?? "default";
32
+ const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
33
+ if (key === options.state.confirmationPolicyIdentity && !nativePolicyDefinitelyUnestablished(processResult, envelope, helper.args))
34
+ options.state.confirmationPolicyMayBeEstablished = true;
35
+ const confirmation = nextReadConfirmation({ commandTokens: helper.args, current: options.state.sessionPageState.getReadConfirmation(key), data: envelope?.data, namespace, sessionName, succeeded: envelope?.success === true && processResult.exitCode === 0 && !processResult.aborted && !processResult.timedOut });
36
+ if (confirmation) {
37
+ options.state.sessionPageState.applyReadConfirmation(confirmation, options.sessionPageStateUpdate);
38
+ if (helper.args[0] === "open" && confirmation.state === "pending") {
39
+ const invalidation = options.state.sessionPageState.get(key).refSnapshotInvalidation;
40
+ options.state.sessionPageState.markTabTargetUnknown({ sessionName: key, update: options.sessionPageStateUpdate });
41
+ if (invalidation)
42
+ options.state.sessionPageState.applyRefSnapshotInvalidation({ sessionName: key, update: options.sessionPageStateUpdate, invalidation });
43
+ }
44
+ options.state.observedBrowserEffects = { ...options.state.observedBrowserEffects, readConfirmation: confirmation };
45
+ }
46
+ },
47
+ }, () => withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options)))));
17
48
  const result = observed.result;
18
49
  let details = isRecord(result.details) ? result.details : undefined;
50
+ if (details?.readConfirmation === undefined && options.state.observedBrowserEffects?.readConfirmation) {
51
+ details = { ...details, readConfirmation: options.state.observedBrowserEffects.readConfirmation };
52
+ result.details = details;
53
+ }
19
54
  if (observed.catalog) {
20
55
  const catalog = redactSensitiveValue(observed.catalog);
21
56
  details = { ...details, webMcpCatalog: catalog };
22
57
  result.details = details;
23
- if (!options.input.toolArgs.includes("--json") && JSON.stringify(getNativeWebMcpCatalog(details.data)) !== JSON.stringify(catalog)) {
58
+ if (!isBooleanFlagEnabled(options.input.toolArgs, "--json") && JSON.stringify(getNativeWebMcpCatalog(details.data)) !== JSON.stringify(catalog)) {
24
59
  const notice = formatWebMcpCatalogUpdate(catalog);
25
60
  if (result.content[0]?.type === "text")
26
61
  result.content[0] = { ...result.content[0], text: `${result.content[0].text}\n\n${notice}` };
@@ -42,11 +77,25 @@ export async function runAgentBrowserTool(options) {
42
77
  };
43
78
  }
44
79
  async function runAgentBrowserToolInContext(options) {
45
- const preparedResult = await prepareBrowserRun(options);
80
+ let preparedResult = await prepareBrowserRun(options);
46
81
  applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
82
+ const helperConfirmation = parseReadConfirmation(options.state.observedBrowserEffects?.readConfirmation);
83
+ if (helperConfirmation?.state === "pending") {
84
+ if (preparedResult.kind === "ready")
85
+ await preparedResult.prepared.managedSessionPolicyLock?.release();
86
+ const data = { confirmation_required: true, confirmation_id: helperConfirmation.id, action: helperConfirmation.action ?? "read" };
87
+ const presentation = await buildToolPresentation({ commandInfo: { command: helperConfirmation.command, commandTokens: [helperConfirmation.command ?? "read"] }, cwd: options.cwd, envelope: { success: true, data }, namespace: helperConfirmation.namespace, sessionName: helperConfirmation.sessionName });
88
+ preparedResult = { kind: "early-result", result: {
89
+ content: [{ type: "text", text: `Native helper ${helperConfirmation.command} requires confirmation (${helperConfirmation.action}). The requested command was not dispatched.` }, ...presentation.content],
90
+ details: { ...options.state.observedBrowserEffects, agentBrowserStarted: false, args: options.input.redactedArgs, data, sessionName: helperConfirmation.sessionName, namespace: helperConfirmation.namespace, sessionTabTargetUnknown: options.state.sessionPageState.get(getAgentBrowserSessionIdentityKey(helperConfirmation.sessionName, helperConfirmation.namespace)).tabTargetUnknown, readConfirmation: helperConfirmation, nextActions: presentation.nextActions,
91
+ ...buildAgentBrowserResultCategoryDetails({ succeeded: false, failureCategory: "confirmation-required" }) },
92
+ isError: true,
93
+ } };
94
+ }
47
95
  if (preparedResult.kind === "early-result") {
96
+ options.state.confirmationPolicyMayBeEstablished ??= false;
48
97
  const result = preparedResult.result;
49
- if (options.modelVisible !== false && options.input.toolArgs.includes("--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
98
+ if (options.modelVisible !== false && isBooleanFlagEnabled(options.input.toolArgs, "--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
50
99
  const details = isRecord(result.details) ? result.details : {};
51
100
  const summary = result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
52
101
  result.content = buildJsonVisibleContent({
@@ -70,7 +119,7 @@ async function runAgentBrowserToolInContext(options) {
70
119
  const artifactRunStartedAtMs = Date.now();
71
120
  const processResult = await withChromeStartupArgs(prepared.chromeStartupArgs, () => runAgentBrowserProcess({
72
121
  args: prepared.processArgs,
73
- browserIndependentReadConfirmation: prepared.readConfirmation !== undefined,
122
+ nativeConfirmationDecision: suppressConfirmationPageHelpers(prepared.readConfirmation),
74
123
  cwd: options.cwd,
75
124
  env: ownedManagedSession
76
125
  ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs }
@@ -99,8 +148,10 @@ async function runAgentBrowserToolInContext(options) {
99
148
  sessionMode: prepared.sessionMode,
100
149
  sessionTabCorrection: prepared.sessionTabCorrection,
101
150
  });
102
- if (missingBinaryResult)
151
+ if (missingBinaryResult) {
152
+ options.state.confirmationPolicyMayBeEstablished ??= false;
103
153
  return missingBinaryResult;
154
+ }
104
155
  const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
105
156
  applyBrowserRunStatePatch(options.state, output.statePatch);
106
157
  if (isRecord(output.result.details) && Array.isArray(output.result.details.imageObservations) && prepared.commandTokens[0] === "screenshot") {