pi-agent-browser-native 0.6.8 → 0.6.10
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.
- package/CHANGELOG.md +28 -0
- package/README.md +16 -14
- package/dist/extensions/agent-browser/index.js +8 -21
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
- package/dist/extensions/agent-browser/lib/argv-grammar.js +6 -0
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +5 -5
- package/dist/extensions/agent-browser/lib/electron/launch.js +77 -23
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +2 -2
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +3 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +6 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +11 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +4 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +26 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +16 -18
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +5 -11
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +4 -3
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +4 -1
- package/dist/extensions/agent-browser/lib/playbook.js +4 -4
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +17 -29
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -5
- package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
- package/dist/extensions/agent-browser/lib/temp.js +14 -0
- package/dist/scripts/agent-browser-target.mjs +1 -1
- package/docs/ARCHITECTURE.md +5 -4
- package/docs/COMMAND_REFERENCE.md +38 -20
- package/docs/ELECTRON.md +8 -4
- package/docs/RELEASE.md +8 -4
- package/docs/SUPPORT_MATRIX.md +19 -15
- package/docs/TOOL_CONTRACT.md +15 -13
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +10 -3
- package/scripts/agent-browser-target.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.10 - 2026-09-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Follow native artifact operands and raw-batch precedence consistently in preflight, recording reservations, result metadata and timeout recovery. Preserve literal batch operands and replay retries as a single native row (#168).
|
|
8
|
+
- Handle recording `--fps` options without losing the requested path or intended pinned page. Keep conservative start/ref protection for older supported natives without claiming a page replacement; FPS-only restarts keep refs (#169).
|
|
9
|
+
- Show positive native WebMCP availability in navigation summaries and distinguish current recording dependency checks from older deferred failures (#169).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Recommend `agent-browser` 0.37.0 while keeping the stable 0.35.0 minimum, no upper cap and native-owned recording/tab setup.
|
|
14
|
+
|
|
15
|
+
### Known limitations
|
|
16
|
+
|
|
17
|
+
- Native 0.37 short/cold recordings on Ubuntu can still fail or produce a shorter clip. This release does not change the upstream recording engine.
|
|
18
|
+
|
|
19
|
+
## 0.6.9 - 2026-09-08
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Include redacted stdout/stderr tails in failed Electron startup diagnostics and visible errors (#128). Capture uses private regular files inside the isolated profile, with the last 4096 bytes read per stream; this is not a lifetime disk limit. Preserve the profile and logs when failed-startup process cleanup cannot finish, without changing normal quit cleanup.
|
|
24
|
+
|
|
25
|
+
- Restore ordinary browser access to a tracked Electron app after Pi reload or resume by checking its live debug endpoint and the named upstream connection. Keep the app, profile, and session intact; unrelated or replaced connections still fail verification.
|
|
26
|
+
|
|
27
|
+
### Validation
|
|
28
|
+
|
|
29
|
+
- Verified startup output, reload continuity and quit cleanup with genuine Electron through the official Pi SDK on native macOS and Ubuntu. Native Windows was waived and not run; permanent release gates are unchanged.
|
|
30
|
+
|
|
3
31
|
## 0.6.8 - 2026-09-07
|
|
4
32
|
|
|
5
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -82,13 +82,13 @@ The result is optimized for agent work:
|
|
|
82
82
|
| 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. 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` |
|
|
83
83
|
| 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; 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` |
|
|
84
84
|
| Dropdown/combobox clicks can focus or hit native option box-model errors | Adds first-class `select <selector> <value...>` paths through raw `args`, `job`, and `semanticAction`; 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` |
|
|
85
|
-
| 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
|
|
86
|
-
| Upstream CLI drift can silently invalidate wrapper behavior | Publishes a repo-readable command reference, verifies it against the recommended 0.
|
|
87
|
-
| Pages can expose structured workflows through experimental WebMCP |
|
|
85
|
+
| 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` |
|
|
86
|
+
| Upstream CLI drift can silently invalidate wrapper behavior | Publishes a repo-readable command reference, verifies it against the recommended 0.37.0 target, and probes browser-backed calls once per cwd/PATH so stable versions below the 0.35.0 floor fail before browser launch with installed/expected version evidence | `npm run verify` |
|
|
87
|
+
| Pages can expose structured workflows through experimental WebMCP | Shows native positive WebMCP availability in navigation summaries and passes through `webmcp list`, `invoke`, detached `result` / `cancel`, params/frame/timeout options, and the bundled `webmcp-gen` skill; treats `--no-webmcp` as launch-scoped, keeps pending or unsuccessfully settled targets unverified with an actionable `get url` follow-up, invalidates stale refs after page tools run, and budgets effective raw or stdin batch timeouts | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#webmcp-page-tools), `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.wait-timeouts.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
88
88
|
| 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) |
|
|
89
89
|
| Agents need bundled `skills` text and local setup/status commands without touching the live session | Treats `skills list/get/path`, local auth/profile/setup commands, `session list`, and local state lifecycle commands as sessionless reads/actions when upstream does not need a live page. Session/state rows and targets remain visible, and supported upstream state/config/path operations pass through unchanged. Browser-backed workflows still receive an implicit session only when the caller did not choose one. | [`docs/COMMAND_REFERENCE.md`](docs/COMMAND_REFERENCE.md#built-in-skills), `extensions/agent-browser/lib/command-policy.ts`, `extensions/agent-browser/lib/runtime.ts` |
|
|
90
90
|
|
|
91
|
-
Artifact results show known requested paths separately from reported/resolved locations and infer image MIME types from file headers, not suffixes. Parent-directory failures return path-specific `validation-error` guidance before browser dispatch. Use absolute artifact paths in raw batch strings because the daemon's working directory may differ from Pi's. Dispatched `record start` and URL-bearing `record restart` attempts also return one fresh-snapshot warning on success or failure, including in JSON output; unreached rows do not
|
|
91
|
+
Artifact results show known requested paths separately from reported/resolved locations and infer image MIME types from file headers, not suffixes. Parent-directory failures return path-specific `validation-error` guidance before browser dispatch. Use absolute artifact paths in raw batch strings because the daemon's working directory may differ from Pi's. Dispatched `record start` and URL-bearing `record restart` attempts also return one fresh-snapshot warning on success or failure, including in JSON output. It describes conservative ref invalidation, not an observed page change; unreached rows do not emit it.
|
|
92
92
|
|
|
93
93
|
## Fastest way to try it
|
|
94
94
|
|
|
@@ -104,13 +104,13 @@ Optional external tools unlock the full command surface:
|
|
|
104
104
|
| Dependency | Required for | macOS install example |
|
|
105
105
|
| --- | --- | --- |
|
|
106
106
|
| `agent-browser` | All browser automation through this extension | See upstream install docs |
|
|
107
|
-
| `ffmpeg` |
|
|
107
|
+
| `ffmpeg` | Recording WebM (libvpx) or MP4 (libx264); install before `record start` / `record restart` | `brew install ffmpeg` or `brew install ffmpeg-full` |
|
|
108
108
|
|
|
109
|
-
Keep both binaries on `PATH`. This package recommends `agent-browser 0.
|
|
109
|
+
Keep both binaries on `PATH`. This package recommends `agent-browser 0.37.0` and accepts stable versions at or above the 0.35.0 floor; browser-backed calls fail fast below that floor while local inspection/setup commands remain available for diagnosis. Native 0.37 validates `ffmpeg`, the output extension and FPS before recording; older supported natives may defer failures until stop. Use `record start <path.webm|path.mp4> [url] [--fps <n>]` (1–60 fps, default 30), then verify the completed file after `record stop`. Native `doctor` checks the recording dependency and encoders.
|
|
110
110
|
|
|
111
111
|
### Android / Termux
|
|
112
112
|
|
|
113
|
-
Android support currently uses Termux's system Chromium rather than Chrome for Testing. Upstream issue [vercel-labs/agent-browser#1587](https://github.com/vercel-labs/agent-browser/issues/1587) tracks native Android packaging; until upstream ships an Android launcher, install the packaged Linux-musl arm64 binary without lifecycle scripts and point the global command at it:
|
|
113
|
+
Android support currently uses Termux's system Chromium rather than Chrome for Testing. Upstream issue [vercel-labs/agent-browser#1587](https://github.com/vercel-labs/agent-browser/issues/1587) tracks native Android packaging; until upstream ships an Android launcher, install the packaged Linux-musl arm64 binary without lifecycle scripts and point the global command at it. The last locally validated Android setup used 0.36.0:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
pkg install tur-repo x11-repo
|
|
@@ -135,6 +135,8 @@ Then install this Pi package:
|
|
|
135
135
|
pi install npm:pi-agent-browser-native
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
After updating `pi-agent-browser-native`, fully quit and restart Pi before using the updated tools. `/reload` can retain previously loaded compiled JavaScript even after `dist/` is rebuilt, so it is not a reliable way to pick up package updates.
|
|
139
|
+
|
|
138
140
|
Start Pi and ask for a browser action:
|
|
139
141
|
|
|
140
142
|
```text
|
|
@@ -184,7 +186,7 @@ npm run doctor
|
|
|
184
186
|
The doctor checks:
|
|
185
187
|
|
|
186
188
|
- upstream `agent-browser` exists on `PATH`
|
|
187
|
-
- the installed upstream is a stable version at or above the supported 0.35.0 floor; 0.
|
|
189
|
+
- the installed upstream is a stable version at or above the supported 0.35.0 floor; 0.37.0 remains the recommended baseline
|
|
188
190
|
- `pi --version` meets the minimum Pi runtime floor for this release; older Pi versions are setup failures
|
|
189
191
|
- Pi settings do not point at multiple active `pi-agent-browser-native` sources
|
|
190
192
|
|
|
@@ -385,7 +387,7 @@ Evaluate page JavaScript through stdin. Put the script in the top-level `stdin`
|
|
|
385
387
|
|
|
386
388
|
Use `outputPath` when `eval`, `get`, `snapshot`, or another extraction should be saved as a durable workspace file. 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; in that case the save notice lives only in `details.outputFile`.
|
|
387
389
|
|
|
388
|
-
Extract several known refs or selectors in one `batch` call instead of many serial getter calls:
|
|
390
|
+
Extract several known refs or selectors in one `batch` call instead of many serial getter calls. When a prior snapshot and session are available and the same-page freshness checks apply, ref-consuming calls add one extra `snapshot -i` preflight per top-level call or batch. Batching shares that probe across rows; it does not remove it:
|
|
389
391
|
|
|
390
392
|
```json
|
|
391
393
|
{ "args": ["batch"], "stdin": "[[\"get\",\"text\",\"@e64\"],[\"get\",\"text\",\"@e65\"]]" }
|
|
@@ -455,7 +457,7 @@ Typical pitfalls:
|
|
|
455
457
|
|
|
456
458
|
### Constrained browser jobs
|
|
457
459
|
|
|
458
|
-
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 after the 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, `inspect-current-page-after-timeout` when the target is already verified, or a fail-fast `verify-page-target-after-timeout` batch that runs `get url` before `snapshot -i` when the target is unknown (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). There is no separate catalog of reusable named browser recipes above one-shot ad hoc `script`, `job`, `qa`, and raw `batch`; `script` has no names, registry, or persistent workflow state; 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.
|
|
460
|
+
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 after the 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, `inspect-current-page-after-timeout` when the target is already verified, or a fail-fast `verify-page-target-after-timeout` batch that runs `get url` before `snapshot -i` when the target is unknown (see [`docs/TOOL_CONTRACT.md#details`](docs/TOOL_CONTRACT.md#details)). Timeout recovery follows the effective batch source and native artifact operand positions, including literal dash-leading paths. Executable retries preserve the original row in a one-row batch. There is no separate catalog of reusable named browser recipes above one-shot ad hoc `script`, `job`, `qa`, and raw `batch`; `script` has no names, registry, or persistent workflow state; 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.
|
|
459
461
|
|
|
460
462
|
`assertText` accepts only `text`, not selector or locator fields. Clicks can stale later `@refs`; split the job and take a fresh snapshot before using them.
|
|
461
463
|
|
|
@@ -505,7 +507,7 @@ For desktop Electron apps, use top-level `electron` to avoid hand-building the d
|
|
|
505
507
|
{ "electron": { "action": "cleanup", "launchId": "electron-…" } }
|
|
506
508
|
```
|
|
507
509
|
|
|
508
|
-
`electron.list` has no configurable timeout. Other Electron actions accept nested `electron.timeoutMs`; `electron.probe.timeoutMs` bounds each underlying read subprocess when dense desktop apps need a shorter or longer probe budget (omit for the normal tool subprocess default). `electron.cleanup.timeoutMs` applies separately to upstream `close` and the initial process-exit wait, not the whole teardown, and defaults to the implicit session close budget unless overridden; if the managed-session close step succeeds but host cleanup is partial, later default browser calls still rotate away from that closed wrapper-managed session. `electron.status.timeoutMs`
|
|
510
|
+
`electron.list` has no configurable timeout. Other Electron actions accept nested `electron.timeoutMs`; `electron.probe.timeoutMs` bounds each underlying read subprocess when dense desktop apps need a shorter or longer probe budget (omit for the normal tool subprocess default). `electron.cleanup.timeoutMs` applies separately to upstream `close` and the initial process-exit wait, not the whole teardown, and defaults to the implicit session close budget unless overridden; if the managed-session close step succeeds but host cleanup is partial, later default browser calls still rotate away from that closed wrapper-managed session. `electron.status.timeoutMs` applies to managed-session title/URL reads and any `get cdp-url` read needed to verify a restored connection. Pass `electron.probe.launchId` when you want the probe tied to a wrapper-tracked launch instead of only the current managed session. Launch/status/probe results show both `launchId` (for status/cleanup/probe) and `sessionName` (for browser `snapshot`/`tab` commands); if the managed session drifts to `about:blank` while wrapper status still sees a live renderer, Electron-specific mismatch warnings and `status`/`probe`/`reattach`/`snapshot` next actions replace generic tab guidance. `/reload` preserves the current branch-visible active Electron launch and its isolated temp `userDataDir` for continuity, and cleans off-branch owned Electron launches. First reuse after reload/resume checks the live app's saved debug endpoint and the exact named upstream connection without reconnecting or resetting page refs; if cleanup is partial and skips or fails profile removal, the generic temp sweep preserves that `userDataDir` across reload, quit, later temp cleanup, process exit, and stale temp-root pruning after restart. If the app process/debug port dies after a successful-looking mutation, the wrapper reports `details.electronPostCommandHealth` and fails with `tab-drift` instead of quietly continuing on `about:blank`. Failed startups expose redacted stdout/stderr tails in visible errors and `details.electron.failure.diagnostics`, alongside PID, profile, DevToolsActivePort, and timing evidence. Each tail reads at most 4096 source bytes; private capture files follow the profile lifecycle, not a lifetime disk cap. See [`docs/ELECTRON.md`](docs/ELECTRON.md#failure-categories-and-recovery).
|
|
509
511
|
|
|
510
512
|
Explicit-ID `electron.status` labels historical cleaned launch records; default and `all: true` selection exclude them. Current PID/port liveness stays independent of cleanup history. `details.electron.statuses[].userDataDirState` freshly measures only the tracked profile path: `present` (including dangling symlinks), `absent` (ENOENT), or `unknown` (other `lstat` errors), not whether all app residue is gone.
|
|
511
513
|
|
|
@@ -711,7 +713,7 @@ The opt-in real-upstream suite is separate because it drives a real browser inst
|
|
|
711
713
|
npm run verify -- real-upstream
|
|
712
714
|
```
|
|
713
715
|
|
|
714
|
-
That mode sets `PI_AGENT_BROWSER_REAL_UPSTREAM=1` and runs `test/agent-browser.real-upstream-contract.test.ts` against the real `agent-browser` on `PATH` (the stable version must meet the 0.35.0 floor; current command-reference validation targets the recommended 0.
|
|
716
|
+
That mode sets `PI_AGENT_BROWSER_REAL_UPSTREAM=1` and runs `test/agent-browser.real-upstream-contract.test.ts` against the real `agent-browser` on `PATH` (the stable version must meet the 0.35.0 floor; current command-reference validation targets the recommended 0.37.0 capability baseline). It covers inspection, skills, experimental WebMCP list/invoke/result/cancel plus `--no-webmcp`, and a broad core interaction and navigation matrix on localhost fixtures (including off-viewport click, frame-scoped selector/wait/click behavior, form command fixes, `batch` stdin, and `pushstate`), plus `vitals`, network route/requests/HAR, diff snapshot/screenshot/url, trace/profiler, console/errors/highlight, stream enable/status/disable, `cookies set --curl`, a `react tree` missing-renderer path, and `wait --download` with the on-disk caveat documented in release notes. The harness uses a throwaway temp `HOME` and dedicated socket/screenshot directories so the run does not touch your normal browser profile paths. Browser-opening or credential-dependent families such as `inspect`, `dashboard`, `chat`, provider clouds, and OS clipboard flows stay in fake-upstream or manual validation unless a safe deterministic fixture is added. For prerequisites, isolation details, and troubleshooting, see [`docs/RELEASE.md`](docs/RELEASE.md#real-upstream-contract-validation).
|
|
715
717
|
|
|
716
718
|
A deterministic host-only live-browser wrapper smoke is available without an LLM choosing tool calls:
|
|
717
719
|
|
|
@@ -744,12 +746,12 @@ npm run verify -- release
|
|
|
744
746
|
5. Screenshots, downloads, recordings, traces, profiles, and spill files are normalized as Pi-visible artifacts where possible.
|
|
745
747
|
6. Generated playbook text in docs and tool metadata stays aligned with `extensions/agent-browser/lib/playbook.ts`.
|
|
746
748
|
|
|
747
|
-
The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/). This package does not bundle it. The recommended baseline is 0.
|
|
749
|
+
The upstream browser engine remains [`agent-browser`](https://agent-browser.dev/). This package does not bundle it. The recommended baseline is 0.37.0 and the stable runtime floor is 0.35.0; newer stable versions are accepted without version-specific compatibility shims.
|
|
748
750
|
|
|
749
751
|
## Current limits
|
|
750
752
|
|
|
751
753
|
- Published pre-1.0 package.
|
|
752
|
-
- Recommends upstream `agent-browser` 0.
|
|
754
|
+
- Recommends upstream `agent-browser` 0.37.0 and accepts stable runtimes at or above 0.35.0.
|
|
753
755
|
- Does not bundle `agent-browser`; users install it separately.
|
|
754
756
|
- 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.
|
|
755
757
|
- 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. A `file://` fixture is supported when upstream browser launch settings allow it; use HTTP(S) only when the browser environment cannot reach the local file.
|
|
@@ -6,7 +6,7 @@ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./
|
|
|
6
6
|
import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
|
|
7
7
|
import { SessionPageState } from "./lib/session-page-state.js";
|
|
8
8
|
import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
|
|
9
|
-
import { extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
9
|
+
import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
10
10
|
import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
|
|
11
11
|
import { needsManagedSession } from "./lib/command-policy.js";
|
|
12
12
|
import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
|
|
@@ -67,7 +67,7 @@ function getArtifactPreflightValidationError(options) {
|
|
|
67
67
|
return error;
|
|
68
68
|
const activeRecordingDestinations = new Set();
|
|
69
69
|
const cleanupOnly = steps.length > 0 && steps.every((step) => {
|
|
70
|
-
const [command, subcommand] =
|
|
70
|
+
const [command, subcommand] = step;
|
|
71
71
|
return isCloseCommand(command) || (command === "record" && subcommand === "stop");
|
|
72
72
|
});
|
|
73
73
|
for (const reservation of options.activeRecordingReservations ?? []) {
|
|
@@ -93,10 +93,9 @@ function getArtifactPreflightValidationError(options) {
|
|
|
93
93
|
}
|
|
94
94
|
const artifactDestinations = new Map();
|
|
95
95
|
let sawBatchClose = false;
|
|
96
|
-
for (const [index,
|
|
97
|
-
const commandStep = extractUpstreamCommandTokens(step);
|
|
96
|
+
for (const [index, commandStep] of steps.entries()) {
|
|
98
97
|
if (batch) {
|
|
99
|
-
const stepValidationError = validateToolArgs(
|
|
98
|
+
const stepValidationError = validateToolArgs(commandStep, { batchStep: true });
|
|
100
99
|
if (stepValidationError)
|
|
101
100
|
return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
|
|
102
101
|
if (sawBatchClose && commandStep[0] === "record" && (commandStep[1] === "start" || commandStep[1] === "restart")) {
|
|
@@ -127,7 +126,7 @@ function getArtifactPreflightValidationError(options) {
|
|
|
127
126
|
}
|
|
128
127
|
artifactDestinations.set(canonicalDestination, index);
|
|
129
128
|
}
|
|
130
|
-
if (batch && commandStep[0] === "screenshot" &&
|
|
129
|
+
if (batch && commandStep[0] === "screenshot" && commandStep.includes("--annotate")) {
|
|
131
130
|
return [
|
|
132
131
|
`Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
|
|
133
132
|
`Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
|
|
@@ -138,7 +137,7 @@ function getArtifactPreflightValidationError(options) {
|
|
|
138
137
|
}
|
|
139
138
|
function commandClosesAllSessions(args, stdin) {
|
|
140
139
|
const parsed = getArtifactCommandSteps(args, stdin);
|
|
141
|
-
return !parsed.error && parsed.steps.some(
|
|
140
|
+
return !parsed.error && parsed.steps.some(isCloseAllCommand);
|
|
142
141
|
}
|
|
143
142
|
function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
|
|
144
143
|
if (outputPath)
|
|
@@ -146,10 +145,7 @@ function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
|
|
|
146
145
|
const parsed = getArtifactCommandSteps(args, stdin);
|
|
147
146
|
if (parsed.error)
|
|
148
147
|
return true;
|
|
149
|
-
return parsed.steps.some((step) =>
|
|
150
|
-
const commandStep = extractUpstreamCommandTokens(step);
|
|
151
|
-
return getExplicitArtifactDestination(commandStep) !== undefined || commandStep[0] === "record" || commandStep[0] === "screenshot" || isCloseCommand(commandStep[0]);
|
|
152
|
-
});
|
|
148
|
+
return parsed.steps.some((step) => getExplicitArtifactDestination(step) !== undefined || step[0] === "record" || step[0] === "screenshot" || isCloseCommand(step[0]));
|
|
153
149
|
}
|
|
154
150
|
function isResultFileArtifact(artifact) {
|
|
155
151
|
return isRecord(artifact)
|
|
@@ -299,12 +295,6 @@ function detailsReportCloseAllApplied(details, succeeded) {
|
|
|
299
295
|
|| (succeeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
|
|
300
296
|
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
301
297
|
}
|
|
302
|
-
function deleteIdentityKeysInNamespace(entries, namespace) {
|
|
303
|
-
for (const key of entries.keys()) {
|
|
304
|
-
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
305
|
-
entries.delete(key);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
298
|
function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironment()) {
|
|
309
299
|
const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
|
|
310
300
|
return extractUpstreamCommandTokens(args)[0] === "connect"
|
|
@@ -663,9 +653,6 @@ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessi
|
|
|
663
653
|
}
|
|
664
654
|
}
|
|
665
655
|
}
|
|
666
|
-
async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys, onClosed) {
|
|
667
|
-
await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys, undefined, onClosed);
|
|
668
|
-
}
|
|
669
656
|
function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
|
|
670
657
|
const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
|
|
671
658
|
const offBranchRecords = new Map();
|
|
@@ -1308,7 +1295,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1308
1295
|
for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
|
|
1309
1296
|
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
1310
1297
|
if (quitting) {
|
|
1311
|
-
await
|
|
1298
|
+
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
1312
1299
|
}
|
|
1313
1300
|
else {
|
|
1314
1301
|
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
@@ -48,14 +48,13 @@ export function parseWaitCommandTokens(commandTokens) {
|
|
|
48
48
|
if (match)
|
|
49
49
|
return { subcommand: match.token };
|
|
50
50
|
}
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
const candidate = commandTokens[downloadPathIndex];
|
|
51
|
+
const downloadIndex = considered.findIndex((entry) => entry.token === "--download" || entry.token === "-d");
|
|
52
|
+
if (downloadIndex >= 0) {
|
|
53
|
+
const candidate = considered[downloadIndex + 1];
|
|
55
54
|
return {
|
|
56
|
-
downloadPath: candidate && !candidate.startsWith("--") ? candidate : undefined,
|
|
57
|
-
downloadPathIndex: candidate && !candidate.startsWith("--") ?
|
|
58
|
-
subcommand:
|
|
55
|
+
downloadPath: candidate && !candidate.token.startsWith("--") ? candidate.token : undefined,
|
|
56
|
+
downloadPathIndex: candidate && !candidate.token.startsWith("--") ? candidate.index : undefined,
|
|
57
|
+
subcommand: considered[downloadIndex].token,
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
return { subcommand: considered[0]?.token };
|
|
@@ -175,6 +175,12 @@ export function isAgentBrowserSessionIdentityKeyInNamespace(identityKey, namespa
|
|
|
175
175
|
const prefix = getAgentBrowserSessionIdentityKey("", namespace);
|
|
176
176
|
return prefix ? identityKey.startsWith(prefix) : !identityKey.includes("\0");
|
|
177
177
|
}
|
|
178
|
+
export function deleteIdentityKeysInNamespace(entries, namespace) {
|
|
179
|
+
for (const key of entries.keys()) {
|
|
180
|
+
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
181
|
+
entries.delete(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
178
184
|
/** Mirror upstream global parsing: full argv, no `--` sentinel, and only global value payloads are skipped. */
|
|
179
185
|
export function scanUpstreamGlobalFlagOccurrences(args, targetFlag) {
|
|
180
186
|
const occurrences = [];
|
|
@@ -1,12 +1,6 @@
|
|
|
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
|
-
function getRowBrowserLaunched(row) {
|
|
5
|
-
const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
|
|
6
|
-
const lifecycle = isRecord(row.lifecycle) ? row.lifecycle : isRecord(result?.lifecycle) ? result.lifecycle : undefined;
|
|
7
|
-
const effectiveLaunch = isRecord(lifecycle?.effectiveLaunch) ? lifecycle.effectiveLaunch : undefined;
|
|
8
|
-
return typeof effectiveLaunch?.browserLaunched === "boolean" ? effectiveLaunch.browserLaunched : undefined;
|
|
9
|
-
}
|
|
10
4
|
export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
|
|
11
5
|
if (!Array.isArray(data))
|
|
12
6
|
return false;
|
|
@@ -34,7 +28,10 @@ export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
|
|
|
34
28
|
const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
|
|
35
29
|
? row.command
|
|
36
30
|
: fallbackCommands[index];
|
|
37
|
-
const
|
|
31
|
+
const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
|
|
32
|
+
const lifecycle = isRecord(row.lifecycle) ? row.lifecycle : isRecord(result?.lifecycle) ? result.lifecycle : undefined;
|
|
33
|
+
const effectiveLaunch = isRecord(lifecycle?.effectiveLaunch) ? lifecycle.effectiveLaunch : undefined;
|
|
34
|
+
const browserLaunched = typeof effectiveLaunch?.browserLaunched === "boolean" ? effectiveLaunch.browserLaunched : undefined;
|
|
38
35
|
if (!rowCommand) {
|
|
39
36
|
if (sawClose && browserLaunched !== false) {
|
|
40
37
|
endsClosed = false;
|
|
@@ -49,7 +46,6 @@ export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
|
|
|
49
46
|
endsClosed = true;
|
|
50
47
|
browserActiveAfterClose = false;
|
|
51
48
|
recordingClosedAfterBatch = true;
|
|
52
|
-
const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
|
|
53
49
|
statePath = typeof result?.statePath === "string" ? result.statePath : undefined;
|
|
54
50
|
}
|
|
55
51
|
else if (sawClose && command === "record") {
|
|
@@ -316,13 +316,26 @@ export function isSessionTabPinningExcludedCommand(command) {
|
|
|
316
316
|
export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
|
|
317
317
|
return hasCommandCapability(command, "excludedFromPostCommandCorrection");
|
|
318
318
|
}
|
|
319
|
-
|
|
319
|
+
export function getRecordCommandOperands(tokens) {
|
|
320
|
+
if (tokens[0] !== "record" || !["start", "restart"].includes(tokens[1] ?? ""))
|
|
321
|
+
return {};
|
|
322
|
+
const operands = [];
|
|
323
|
+
for (let index = 2; index < tokens.length && operands.length < 2; index += 1) {
|
|
324
|
+
// Native validates the range; bare/non-numeric --fps keeps its old literal meaning.
|
|
325
|
+
if (tokens[index] === "--fps" && /^\+?\d+$/.test(tokens[index + 1] ?? ""))
|
|
326
|
+
index += 1;
|
|
327
|
+
else
|
|
328
|
+
operands.push(tokens[index]);
|
|
329
|
+
}
|
|
330
|
+
return operands.length > 0 ? { path: operands[0], url: operands[1] } : { path: tokens[2], url: tokens[3] };
|
|
331
|
+
}
|
|
332
|
+
/** Starts conservatively invalidate refs because older supported natives replace the page, even on failure. Restarts invalidate only when they have a URL. */
|
|
320
333
|
export function isRecordPageTransitionCommand(tokens) {
|
|
321
334
|
if (tokens[0] !== "record")
|
|
322
335
|
return false;
|
|
323
336
|
if (tokens[1] === "start")
|
|
324
337
|
return true;
|
|
325
|
-
return tokens[1] === "restart" && tokens.
|
|
338
|
+
return tokens[1] === "restart" && getRecordCommandOperands(tokens).url !== undefined;
|
|
326
339
|
}
|
|
327
340
|
export function isWebMcpPageMutationCommand(tokens) {
|
|
328
341
|
return isWebMcpPageMutation(tokens[0], tokens[1]);
|
|
@@ -24,15 +24,15 @@ function isPidAlive(pid) {
|
|
|
24
24
|
return code === "EPERM" ? true : false;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
async function isPortAlive(port) {
|
|
28
|
-
const version = parseCdpVersion(await fetchCdpJson(`http://127.0.0.1:${port}/json/version
|
|
27
|
+
async function isPortAlive(port, signal) {
|
|
28
|
+
const version = parseCdpVersion(await fetchCdpJson(`http://127.0.0.1:${port}/json/version`, signal));
|
|
29
29
|
if (!version)
|
|
30
30
|
return { targets: [] };
|
|
31
|
-
const targets = parseCdpTargets(await fetchCdpJson(`http://127.0.0.1:${port}/json/list
|
|
31
|
+
const targets = parseCdpTargets(await fetchCdpJson(`http://127.0.0.1:${port}/json/list`, signal));
|
|
32
32
|
return { targets, version };
|
|
33
33
|
}
|
|
34
|
-
export async function inspectElectronLaunchStatus(record) {
|
|
35
|
-
const cdp = await isPortAlive(record.port);
|
|
34
|
+
export async function inspectElectronLaunchStatus(record, signal) {
|
|
35
|
+
const cdp = await isPortAlive(record.port, signal);
|
|
36
36
|
let userDataDirState;
|
|
37
37
|
try {
|
|
38
38
|
await lstat(record.userDataDir);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { dirname } from "node:path";
|
|
3
|
+
import { open, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
|
|
6
6
|
import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
|
|
7
|
-
import { createSecureTempDirectory } from "../temp.js";
|
|
7
|
+
import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
|
|
8
8
|
export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
|
|
9
9
|
export const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
10
|
export const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
|
|
@@ -12,6 +12,8 @@ const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
|
12
12
|
export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
|
|
13
13
|
const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"];
|
|
14
14
|
const ELECTRON_DEVTOOLS_POLL_INTERVAL_MS = 100;
|
|
15
|
+
// ponytail: bound failure reads, not lifetime log growth; noisy long-lived apps need rotation if that becomes a problem.
|
|
16
|
+
const ELECTRON_OUTPUT_TAIL_BYTES = 4096;
|
|
15
17
|
function normalizeTimeoutMs(timeoutMs) {
|
|
16
18
|
if (!Number.isSafeInteger(timeoutMs) || (timeoutMs ?? 0) <= 0)
|
|
17
19
|
return ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS;
|
|
@@ -284,40 +286,90 @@ export async function launchElectronApp(options) {
|
|
|
284
286
|
let exitCode = null;
|
|
285
287
|
let exitSignal = null;
|
|
286
288
|
const args = buildLaunchArgs(userDataDir, appArgs);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
289
|
+
let child;
|
|
290
|
+
let outputCaptured = false;
|
|
291
|
+
const outputFiles = [];
|
|
292
|
+
try {
|
|
293
|
+
for (const stream of ["stdout", "stderr"])
|
|
294
|
+
outputFiles.push(await open(join(userDataDir, `${stream}.log`), "wx", 0o600));
|
|
295
|
+
options.signal?.throwIfAborted();
|
|
296
|
+
child = spawn(target.executablePath, args, {
|
|
297
|
+
cwd: dirname(target.executablePath),
|
|
298
|
+
detached: process.platform !== "win32",
|
|
299
|
+
stdio: ["ignore", outputFiles[0].fd, outputFiles[1].fd],
|
|
300
|
+
});
|
|
301
|
+
outputCaptured = true;
|
|
302
|
+
child.once("error", (error) => {
|
|
303
|
+
spawnError = error;
|
|
304
|
+
});
|
|
305
|
+
child.once("exit", (code, signal) => {
|
|
306
|
+
exitCode = code;
|
|
307
|
+
exitSignal = signal;
|
|
308
|
+
});
|
|
309
|
+
child.unref();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
spawnError = error instanceof Error ? error : new Error(String(error));
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
for (const file of outputFiles) {
|
|
316
|
+
await file.close().catch((error) => {
|
|
317
|
+
cleanupError = [cleanupError, `Output handle close failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("; ");
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
300
321
|
const buildFailureDiagnostics = (options = {}) => ({
|
|
301
322
|
cdpVersionReached: options.cdpVersionReached,
|
|
302
323
|
devToolsActivePort: options.devToolsActivePort,
|
|
303
324
|
elapsedMs: Math.max(0, Date.now() - startedAtMs),
|
|
304
325
|
exitCode,
|
|
305
326
|
exitSignal,
|
|
306
|
-
outputCaptured
|
|
307
|
-
pid: child
|
|
308
|
-
pidAlive: isLaunchChildPidAlive(child),
|
|
327
|
+
outputCaptured,
|
|
328
|
+
pid: child?.pid,
|
|
329
|
+
pidAlive: child ? isLaunchChildPidAlive(child) : undefined,
|
|
309
330
|
port: options.port ?? options.devToolsActivePort?.port,
|
|
310
331
|
timeoutMs,
|
|
311
332
|
userDataDir,
|
|
312
333
|
});
|
|
313
334
|
const fail = async (reason, detail, diagnosticOptions) => {
|
|
314
335
|
const diagnostics = buildFailureDiagnostics(diagnosticOptions);
|
|
315
|
-
const processCleanupError = await terminateLaunchChild(child);
|
|
336
|
+
const processCleanupError = child ? await terminateLaunchChild(child) : undefined;
|
|
337
|
+
const outputLines = [];
|
|
338
|
+
if (outputCaptured) {
|
|
339
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
340
|
+
let file;
|
|
341
|
+
try {
|
|
342
|
+
file = await open(join(userDataDir, `${stream}.log`), "r");
|
|
343
|
+
const { size } = await file.stat();
|
|
344
|
+
const buffer = Buffer.alloc(Math.min(size, ELECTRON_OUTPUT_TAIL_BYTES));
|
|
345
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
|
|
346
|
+
diagnostics[`${stream}Tail`] = buffer.subarray(0, bytesRead).toString("utf8");
|
|
347
|
+
diagnostics[`${stream}Truncated`] = size > buffer.length;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
diagnostics[`${stream}Error`] = error instanceof Error ? error.message : String(error);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await file?.close().catch((error) => {
|
|
354
|
+
diagnostics[`${stream}Error`] = [diagnostics[`${stream}Error`], `Output reader close failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("; ");
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const tail = diagnostics[`${stream}Tail`];
|
|
358
|
+
if (tail !== undefined)
|
|
359
|
+
outputLines.push(`App ${stream}${diagnostics[`${stream}Truncated`] ? ` (last ${ELECTRON_OUTPUT_TAIL_BYTES} bytes)` : ""}: ${tail || "(empty)"}`);
|
|
360
|
+
if (diagnostics[`${stream}Error`])
|
|
361
|
+
outputLines.push(`App ${stream} capture error: ${diagnostics[`${stream}Error`]}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
316
364
|
try {
|
|
317
|
-
|
|
365
|
+
if (processCleanupError)
|
|
366
|
+
await preserveSecureTempDirectory(userDataDir);
|
|
367
|
+
else
|
|
368
|
+
await rm(userDataDir, { force: true, recursive: true });
|
|
318
369
|
}
|
|
319
370
|
catch (error) {
|
|
320
|
-
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
cleanupError = [cleanupError, processCleanupError ? `Profile preservation failed: ${message}` : message].filter(Boolean).join("; ");
|
|
321
373
|
}
|
|
322
374
|
cleanupError = [processCleanupError, cleanupError].filter((value) => value !== undefined).join("; ") || undefined;
|
|
323
375
|
return {
|
|
@@ -326,13 +378,15 @@ export async function launchElectronApp(options) {
|
|
|
326
378
|
appArgs,
|
|
327
379
|
cleanupError,
|
|
328
380
|
diagnostics,
|
|
329
|
-
error: launchFailureMessage(reason, target, detail),
|
|
381
|
+
error: [launchFailureMessage(reason, target, detail), ...outputLines].join("\n"),
|
|
330
382
|
reason,
|
|
331
383
|
target,
|
|
332
384
|
userDataDir,
|
|
333
385
|
},
|
|
334
386
|
};
|
|
335
387
|
};
|
|
388
|
+
if (!child)
|
|
389
|
+
return fail(options.signal?.aborted ? "aborted" : "spawn-error", spawnError?.message);
|
|
336
390
|
const portResult = await pollDevToolsActivePort({
|
|
337
391
|
deadlineMs,
|
|
338
392
|
getChildExit: () => ({ code: exitCode, signal: exitSignal }),
|
|
@@ -104,7 +104,7 @@ export function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserP
|
|
|
104
104
|
return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
|
|
105
105
|
}
|
|
106
106
|
/** Caller-selected upstream config disables the wrapper's automatic restore injection without blocking that config. */
|
|
107
|
-
export function agentBrowserConfigBlocksManagedRestore(
|
|
107
|
+
export function agentBrowserConfigBlocksManagedRestore(parentEnv = getAgentBrowserProcessEnvironment(), args = [], platform = process.platform) {
|
|
108
108
|
return !resolveManagedSessionRestoreHome(parentEnv, platform) || agentBrowserExplicitConfigIsPresent(parentEnv, args);
|
|
109
109
|
}
|
|
110
110
|
function omitWrapperInjectedUserAgent(args, enabled) {
|
|
@@ -151,7 +151,7 @@ function isManagedSessionRestoreIncompatible(options, namespace = extractExplici
|
|
|
151
151
|
const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
|
|
152
152
|
if (options.cwd && !hasManagedSessionRestoreProjectIdentity(options.cwd))
|
|
153
153
|
return true;
|
|
154
|
-
if (options.cwd && agentBrowserConfigBlocksManagedRestore(
|
|
154
|
+
if (options.cwd && agentBrowserConfigBlocksManagedRestore(effectiveEnv, args))
|
|
155
155
|
return true;
|
|
156
156
|
return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, namespace);
|
|
157
157
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { directoryContainsSymlink, ensureManagedSessionRestoreStorageIsSecure, ensureOwnerOnlyDirectory, getManagedRestoreSessionsDirectory, isManagedSessionRestoreKey, resolveManagedSessionRestoreCheckoutRoot, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
|
|
5
5
|
const OWNED_RESTORE_SNAPSHOT_FAMILIES_TO_KEEP = 2;
|
|
6
6
|
const OWNED_RESTORE_SNAPSHOT_MAX_RECORDS = 256;
|
|
7
7
|
const OWNED_RESTORE_SNAPSHOT_RECORD_MAX_BYTES = 16 * 1_024;
|
|
@@ -286,10 +286,8 @@ function scanOwnedSnapshots(options) {
|
|
|
286
286
|
export function pruneOwnedManagedSessionRestoreSnapshots(options) {
|
|
287
287
|
const parentEnv = options.parentEnv ?? process.env;
|
|
288
288
|
const platform = options.platform ?? process.platform;
|
|
289
|
-
const restoreKey = options.restoreKey
|
|
290
|
-
|
|
291
|
-
: isManagedSessionRestoreKey(options.restoreKey) ? options.restoreKey : undefined;
|
|
292
|
-
if (!restoreKey)
|
|
289
|
+
const restoreKey = options.restoreKey;
|
|
290
|
+
if (!isManagedSessionRestoreKey(restoreKey))
|
|
293
291
|
return 0;
|
|
294
292
|
const home = resolveManagedSessionRestoreHome(parentEnv, platform);
|
|
295
293
|
if (!home)
|