pi-agent-browser-native 0.3.0 → 0.5.0

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 (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -0,0 +1,169 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createContext, runInContext, Script } from "node:vm";
3
+ function parseLimit(value, label) {
4
+ const parsed = Number(value);
5
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
6
+ throw new Error(`Invalid ${label}.`);
7
+ return parsed;
8
+ }
9
+ const maxMessageBytes = parseLimit(process.argv[2], "script IPC message limit");
10
+ const maxCumulativeBytes = parseLimit(process.argv[3], "script IPC cumulative limit");
11
+ let cumulativeBytes = 0;
12
+ let inputBuffer = Buffer.alloc(0);
13
+ let started = false;
14
+ const sandbox = Object.create(null);
15
+ const context = createContext(sandbox, {
16
+ codeGeneration: { strings: false, wasm: false },
17
+ name: "agent-browser-script",
18
+ });
19
+ const bridgeKey = `__piab_send_${randomBytes(16).toString("hex")}`;
20
+ const stateName = `__piab_state_${randomBytes(16).toString("hex")}`;
21
+ const hostSend = (json) => {
22
+ if (typeof json !== "string")
23
+ return false;
24
+ const bytes = Buffer.byteLength(json, "utf8") + 1;
25
+ if (bytes > maxMessageBytes || cumulativeBytes + bytes > maxCumulativeBytes)
26
+ return false;
27
+ cumulativeBytes += bytes;
28
+ try {
29
+ process.stdout.write(`${json}\n`);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
36
+ Object.setPrototypeOf(hostSend, null);
37
+ Object.freeze(hostSend);
38
+ sandbox[bridgeKey] = hostSend;
39
+ runInContext("const " + stateName + " = (() => {\n" +
40
+ " 'use strict';\n" +
41
+ " const send = globalThis[" + JSON.stringify(bridgeKey) + "];\n" +
42
+ " delete globalThis[" + JSON.stringify(bridgeKey) + "];\n" +
43
+ " for (const name of ['console','process','require','Buffer','fetch','WebSocket','setTimeout','setInterval','setImmediate','queueMicrotask','clearTimeout','clearInterval','clearImmediate']) Object.defineProperty(globalThis, name, { value: undefined, writable: false, configurable: false });\n" +
44
+ " const NativePromise = Promise;\n" +
45
+ " const promiseThen = Promise.prototype.then;\n" +
46
+ " const reflectApply = Reflect.apply;\n" +
47
+ " const pending = new Map();\n" +
48
+ " let nextId = 0;\n" +
49
+ " const encode = (value) => { const json = JSON.stringify(value); if (typeof json !== 'string') throw new TypeError('Value must be JSON-serializable.'); return json; };\n" +
50
+ " const sendValue = (value) => { const json = encode(value); if (json.length + 1 > " + maxMessageBytes + " || send(json) !== true) throw new RangeError('Script IPC limit exceeded.'); };\n" +
51
+ " const browser = function browser(params) {\n" +
52
+ " return new NativePromise((resolve, reject) => {\n" +
53
+ " const id = ++nextId;\n" +
54
+ " pending.set(id, { resolve, reject });\n" +
55
+ " try { sendValue({ type: 'call', id, params }); } catch (error) { pending.delete(id); reject(error); }\n" +
56
+ " });\n" +
57
+ " };\n" +
58
+ " const emit = function emit(value) { sendValue({ type: 'emit', value }); };\n" +
59
+ " Object.setPrototypeOf(browser, null);\n" +
60
+ " Object.setPrototypeOf(emit, null);\n" +
61
+ " Object.freeze(browser);\n" +
62
+ " Object.freeze(emit);\n" +
63
+ " Object.defineProperties(globalThis, { browser: { value: browser, writable: false, configurable: false }, emit: { value: emit, writable: false, configurable: false } });\n" +
64
+ " const complete = (ok, value) => {\n" +
65
+ " if (ok) {\n" +
66
+ " try { sendValue(value === undefined ? { type: 'complete', hasValue: false } : { type: 'complete', hasValue: true, value }); }\n" +
67
+ " catch { sendValue({ type: 'complete', error: { name: 'RangeError', message: 'Final script value is not serializable or exceeds the IPC limit.' } }); }\n" +
68
+ " return;\n" +
69
+ " }\n" +
70
+ " let name = 'Error'; let message = 'Script execution failed.';\n" +
71
+ " try { if (value && typeof value.name === 'string') name = value.name.slice(0, 80); } catch {}\n" +
72
+ " try { if (value && typeof value.message === 'string') message = value.message.replace(/[\\r\\n]+/g, ' ').slice(0, 400); } catch {}\n" +
73
+ " sendValue({ type: 'complete', error: { name, message } });\n" +
74
+ " };\n" +
75
+ " return Object.freeze({\n" +
76
+ " deliver(json) {\n" +
77
+ " const message = JSON.parse(json);\n" +
78
+ " const target = pending.get(message.id);\n" +
79
+ " if (!target) return;\n" +
80
+ " pending.delete(message.id);\n" +
81
+ " target.resolve(message.envelope);\n" +
82
+ " },\n" +
83
+ " run(thunk) {\n" +
84
+ " let promise;\n" +
85
+ " try { promise = reflectApply(thunk, undefined, []); } catch (error) { complete(false, error); return; }\n" +
86
+ " reflectApply(promiseThen, promise, [value => complete(true, value), error => complete(false, error)]);\n" +
87
+ " }\n" +
88
+ " });\n" +
89
+ "})();", context, { timeout: 1_000 });
90
+ const deliver = runInContext(`${stateName}.deliver`, context, { timeout: 1_000 });
91
+ function fail(name, message) {
92
+ hostSend(JSON.stringify({ type: "complete", error: { name, message } }));
93
+ }
94
+ function describeError(error, fallback) {
95
+ if (!error || typeof error !== "object")
96
+ return { message: fallback, name: "Error" };
97
+ const candidate = error;
98
+ return {
99
+ message: typeof candidate.message === "string" ? candidate.message.replace(/[\r\n]+/g, " ").slice(0, 400) : fallback,
100
+ name: typeof candidate.name === "string" ? candidate.name.slice(0, 80) : "Error",
101
+ };
102
+ }
103
+ function handleLine(line) {
104
+ const bytes = Buffer.byteLength(line, "utf8") + 1;
105
+ if (bytes > maxMessageBytes || cumulativeBytes + bytes > maxCumulativeBytes) {
106
+ fail("RangeError", "Script IPC limit exceeded.");
107
+ return;
108
+ }
109
+ cumulativeBytes += bytes;
110
+ let message;
111
+ try {
112
+ message = JSON.parse(line);
113
+ }
114
+ catch {
115
+ fail("Error", "Invalid parent IPC message.");
116
+ return;
117
+ }
118
+ if (!started) {
119
+ if (!message || typeof message !== "object" || message.type !== "start" || typeof message.code !== "string") {
120
+ fail("Error", "Invalid script start message.");
121
+ return;
122
+ }
123
+ started = true;
124
+ try {
125
+ const source = `'use strict';\n${stateName}.run(async function () {\n'use strict';\n${message.code}\n});`;
126
+ const script = new Script(source, {
127
+ filename: "agent-browser-script.js",
128
+ importModuleDynamically() {
129
+ process.exit(70);
130
+ },
131
+ });
132
+ script.runInContext(context, { timeout: undefined });
133
+ }
134
+ catch (error) {
135
+ const described = describeError(error, "Script compilation failed.");
136
+ fail(described.name, described.message);
137
+ }
138
+ return;
139
+ }
140
+ if (!message || typeof message !== "object" || message.type !== "response") {
141
+ fail("Error", "Invalid parent IPC response.");
142
+ return;
143
+ }
144
+ try {
145
+ deliver(line);
146
+ }
147
+ catch {
148
+ fail("Error", "Invalid browser response envelope.");
149
+ }
150
+ }
151
+ process.stdin.on("data", (rawChunk) => {
152
+ const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
153
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
154
+ if (inputBuffer.length > maxMessageBytes) {
155
+ fail("RangeError", "Script IPC message limit exceeded.");
156
+ process.stdin.pause();
157
+ return;
158
+ }
159
+ for (;;) {
160
+ const newline = inputBuffer.indexOf(10);
161
+ if (newline < 0)
162
+ break;
163
+ const line = inputBuffer.subarray(0, newline).toString("utf8");
164
+ inputBuffer = inputBuffer.subarray(newline + 1);
165
+ handleLine(line);
166
+ }
167
+ });
168
+ process.stdin.on("error", () => undefined);
169
+ hostSend(JSON.stringify({ type: "ready" }));
@@ -0,0 +1,3 @@
1
+ export const TARGET_AGENT_BROWSER_SOURCE = "scripts/agent-browser-target.mjs";
2
+ export const TARGET_AGENT_BROWSER_VERSION = "0.34.0";
3
+ export const TARGET_AGENT_BROWSER_VERSION_LABEL = `agent-browser ${TARGET_AGENT_BROWSER_VERSION}`;
@@ -42,15 +42,31 @@ The extension should:
42
42
  - support optional stdin only for `eval --stdin`, `batch`, `auth save --password-stdin`, and wrapper-generated `batch` stdin from top-level `job`, `qa`, `sourceLookup`, or `networkSourceLookup`, rejecting other command/stdin combinations before launch; top-level `electron` never accepts caller `stdin` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
43
43
  - support optional top-level `outputPath` for successful browser results by writing `details.data` (or model-facing text when no structured data exists) to a caller-requested local file and reporting `details.outputFile`, without changing upstream argv semantics or overwriting a browser artifact when both destinations resolve to the same file
44
44
  - support optional top-level `timeoutMs` as a per-call subprocess watchdog override for browser CLI input modes while keeping Electron-specific timeouts inside the `electron` object
45
- - accept an optional native `semanticAction` object as a mutually exclusive alternative to `args` on a single tool call (and to `job`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call), compile locator actions into upstream `find` argv, direct selector/ref click/check/fill into upstream command argv, and native dropdown selection into upstream `select <selector> <value...>` argv (with optional `semanticAction.session` expanding to a leading `--session <name>` before the compiled command when targeting a named upstream browser instead of the managed default), and echo the compiled shape in `details.compiledSemanticAction` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#semanticaction))
46
- - accept an optional native `job` object (mutually exclusive with `args`, `semanticAction`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) with a small fixed step vocabulary that compiles only to existing upstream `batch` argv rows, generates the JSON batch stdin string internally, defaults to fail-fast `batch --bail` unless `failFast:false` is explicit, supports semantic locator fields on constrained `click`/`fill` steps by reusing the top-level semantic-action compiler, supports human-paced `type` by expanding to a bounded set of existing focus/keyboard/wait/press rows while compacting model-visible batch text, and echoes `details.compiledJob` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#job))
47
- - accept an optional native `qa` object (mutually exclusive with `args`, `semanticAction`, `job`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same fail-fast `batch --bail` path as `job`, runs a fixed diagnostic smoke sequence with bounded visible-text predicates for `expectedText`, preserves existing diagnostics for `qa.attached` while clearing buffers only for URL-opening QA, and echoes `details.compiledQaPreset` plus structured `details.qaPreset` pass/fail evidence (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#qa))
48
- - accept an optional native `sourceLookup` object (mutually exclusive with `args`, `semanticAction`, `job`, `qa`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, gathers evidence-backed local source *candidates* for a selector/fiber/component name, and echoes `details.compiledSourceLookup` plus structured `details.sourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sourcelookup)); unlike `qa`, it never applies a second pass/fail layer that marks the tool failed when upstream already reported batch success—failed upstream steps still fail the invocation normally, and `details.sourceLookup` may still be present for partial evidence
49
- - accept an optional native `networkSourceLookup` object (mutually exclusive with `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, correlates failed network requests with initiator metadata and bounded workspace URL literals, and echoes `details.compiledNetworkSourceLookup` plus structured `details.networkSourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#networksourcelookup)); like `sourceLookup`, it never flips a successful upstream batch to failed solely because no source candidates were found
50
- - accept an optional native `electron` object (mutually exclusive with `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `networkSourceLookup` on the same call) for bounded desktop Electron lifecycle: `list` scans the host for install candidates, `launch` creates a wrapper-owned isolated profile plus OS-chosen remote-debugging port, then attaches through upstream `connect` with `sessionMode: "fresh"`, cancellation prevents host spawn or interrupts readiness polling with process/profile cleanup, and `status` / `cleanup` / `probe` operate only on wrapper-tracked launches; host-side spawn and CDP discovery live in `extensions/agent-browser/lib/electron/discovery.ts`, `launch.ts`, and `cleanup.ts`, while compilation, transcript restore for `launchId` records, handoff probes, and merged `details.electron*` fields live under `extensions/agent-browser/lib/orchestration/electron-host/` and `extensions/agent-browser/lib/orchestration/browser-run/` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
45
+ - accept an optional top-level `script` string as a mutually exclusive one-shot orchestration mode for loops, conditional page branches, and multi-page aggregation. Source runs in a separate permissioned Node child with a constrained VM context; only null-prototype `browser({ args, stdin?, timeoutMs? })` and `emit(value)` task functions cross a bounded JSON-lines IPC bridge. The parent serializes at most one inner call at a time through the same full ordinary tool executor, clears ambient upstream launch/proxy controls across helpers and cleanup, caps calls/source/post-redaction output/time, injects one unique restore-disabled wrapper-owned session, writes a strict Pi custom-entry cleanup lease before first launch, closes in `finally`, aborts and awaits active-script cleanup on branch change/shutdown, and recovers exact non-closed active-branch leases afterward. Script requires Pi session persistence and exposes no profile/attachment/session-control, host API, named recipe, import, or persistent workflow-state surface (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#script)).
46
+ - accept an optional native `semanticAction` object as a mutually exclusive alternative to `args` on a single tool call (and to `script`, `job`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call), compile locator actions into upstream `find` argv, direct selector/ref click/check/fill into upstream command argv, and native dropdown selection into upstream `select <selector> <value...>` argv (with optional `semanticAction.session` expanding to a leading `--session <name>` before the compiled command when targeting a named upstream browser instead of the managed default), and echo the compiled shape in `details.compiledSemanticAction` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#semanticaction))
47
+ - accept an optional native `job` object (mutually exclusive with `script`, `args`, `semanticAction`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) with a small fixed step vocabulary that compiles only to existing upstream `batch` argv rows, generates the JSON batch stdin string internally, defaults to fail-fast `batch --bail` unless `failFast:false` is explicit, supports semantic locator fields on constrained `click`/`fill` steps by reusing the top-level semantic-action compiler, supports human-paced `type` by expanding to a bounded set of existing focus/keyboard/wait/press rows while compacting model-visible batch text, and echoes `details.compiledJob` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#job))
48
+ - accept an optional native `qa` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same fail-fast `batch --bail` path as `job`, runs a fixed diagnostic smoke sequence with bounded visible-text predicates for `expectedText`, preserves existing diagnostics for `qa.attached` while clearing buffers only for URL-opening QA, and echoes `details.compiledQaPreset` plus structured `details.qaPreset` pass/fail evidence (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#qa))
49
+ - accept an optional native `sourceLookup` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, gathers evidence-backed local source *candidates* for a selector/fiber/component name, and echoes `details.compiledSourceLookup` plus structured `details.sourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sourcelookup)); unlike `qa`, it never applies a second pass/fail layer that marks the tool failed when upstream already reported batch success—failed upstream steps still fail the invocation normally, and `details.sourceLookup` may still be present for partial evidence
50
+ - accept an optional native `networkSourceLookup` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, correlates failed network requests with initiator metadata and bounded workspace URL literals, and echoes `details.compiledNetworkSourceLookup` plus structured `details.networkSourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#networksourcelookup)); like `sourceLookup`, it never flips a successful upstream batch to failed solely because no source candidates were found
51
+ - accept an optional native `electron` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `networkSourceLookup` on the same call) for bounded desktop Electron lifecycle: `list` scans the host for install candidates, `launch` creates a wrapper-owned isolated profile plus OS-chosen remote-debugging port, then attaches through upstream `connect` with `sessionMode: "fresh"`, cancellation prevents host spawn or interrupts readiness polling with process/profile cleanup, and `status` / `cleanup` / `probe` operate only on wrapper-tracked launches; host-side spawn and CDP discovery live in `extensions/agent-browser/lib/electron/discovery.ts`, `launch.ts`, and `cleanup.ts`, while compilation, transcript restore for `launchId` records, handoff probes, and merged `details.electron*` fields live under `extensions/agent-browser/lib/orchestration/electron-host/` and `extensions/agent-browser/lib/orchestration/browser-run/` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
51
52
  - when a compiled `find` semantic action fails as `stale-ref`, optionally append a `retry-semantic-action-after-stale-ref` entry to `details.nextActions` after the usual `refresh-interactive-refs` snapshot step so agents can re-issue the same compiled `find` argv only when the failure implies the interaction did not run; `select` shorthands with stale `@refs` get refresh guidance only (contract in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#semanticaction))
52
53
  - when the same compiled path fails as `selector-not-found` for the bounded locator/action pairs documented there, optionally append `try-*-candidate` entries to `details.nextActions` and mirror them in visible text as `Agent-browser candidate fallbacks` so agents can retry role/name `find` variants without hand-rebuilding argv (`select` misses are intentionally excluded)
53
54
 
55
+ ### One-shot script isolation
56
+
57
+ `script` is orchestration around the native tool, not a second browser runtime. Its custom Pi call renderer keeps the approval boundary inspectable with a bounded terminal-safe preview whose line breaks render as `↵` and full terminal-safe source when expanded; JavaScript line terminators stay visible as newlines and removed controls become visible markers. The child never imports this extension or invokes `agent-browser`; it only emits bounded JSON call requests. The parent validates each request against script-specific policy, injects `--namespace "" --session piab-script-<uuid>`, and recursively uses the registered tool's ordinary executor. This preserves the same argv parsing, local/protected-path gates, process policy, presentation/redaction, spill/artifact verification, result categories, and timeout behavior instead of creating a weaker bare-process shortcut. The one deliberate process difference is stricter: an async-local isolation scope filters ambient `AGENT_BROWSER_*` and standard proxy variables during planning and spawn, then wrapper-owned empty config, namespace, timeout, and compatibility values are applied.
58
+
59
+ Isolation is layered:
60
+
61
+ 1. schema/runtime bounds reject oversized/non-string source and invalid top-level mode combinations
62
+ 2. a separate Node child receives an empty environment, a 64 MiB heap ceiling, permission mode, no host capability allowlists, and a VM context with string/WebAssembly code generation disabled. Node documents `node:vm` as not a security mechanism by itself; the permissioned child process and bounded IPC are the containment boundary, while the VM removes ordinary capability references. Re-run the constructor/stack-escape sandbox tests when raising the Node major floor.
63
+ 3. the context receives JSON-created data only; the exposed `browser` / `emit` functions have null prototypes, and no parent promise/function/object is exposed to user source
64
+ 4. JSON-lines IPC has per-message and cumulative byte limits, one in-flight browser call, 25 attempted calls, bounded inner summary/text, a serialized-envelope check that turns oversized inner results into handleable failures, compact final-output serialization with a second post-redaction byte/depth guard, and one outer deadline that clamps every inner timeout
65
+ 5. inner `nextActions` are filtered back through script policy after the wrapper-owned isolated namespace/session prefix is removed, so source never receives a suggested browser payload that its next `browser()` call must reject
66
+ 6. abort/timeout/branch change/shutdown sends SIGTERM then bounded SIGKILL, aborts any active inner call, waits for normal isolated-session cleanup, and reaps the child before branch restoration or shutdown cleanup proceeds
67
+
68
+ Browser isolation is separate from language isolation. A pre-spawn Pi custom entry records only the generated session name, exact generated close argv, launch marker, and cleanup state. That entry is durable but model-invisible. The generated identity has no restore key, cannot be selected by script source, never updates the implicit managed-session pointer, and is always closed. Exact pending records on the active transcript branch are retried after restart; malformed names, altered close argv, or missing launch markers are ignored. If Pi persistence is disabled, script fails before child/browser launch because crash recovery cannot be guaranteed.
69
+
54
70
  ### Agent-first UX
55
71
 
56
72
  The primary UX is the agent calling the tool directly.
@@ -76,7 +92,7 @@ Browser default config is intentionally advisory. It can add prompt guidance for
76
92
 
77
93
  ### Prompt guidance budget
78
94
 
79
- Runtime `promptGuidelines` are a Tier A budget, not a full manual. They stay short enough to load on every `agent_browser`-aware turn and carry only high-impact rules: input-mode choice, the open → snapshot → ref loop, launch-scoped session handling, artifact verification, structured `nextActions`, extraction basics, and hard agent-responsibility boundaries such as stop before order/post/purchase/submit.”
95
+ Runtime `promptGuidelines` are a Tier A budget, not a full manual. They stay short enough to load on every `agent_browser`-aware turn and carry only high-impact rules: input-mode choice, the open → snapshot → ref loop, launch-scoped session handling, artifact verification, structured `nextActions`, extraction basics, and hard agent-responsibility boundaries: honor explicit stop boundaries, allow ordinary requested non-destructive submissions in authenticated unattended/auto-approved employee flows, and require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.
80
96
 
81
97
  Tier B guidance lives in `SHARED_BROWSER_PLAYBOOK_GUIDELINES`, generated README/command-reference fragments, and targeted docs. When a workflow needs examples, caveats, or long command-family coverage, add it there instead of expanding always-on prompt text. If a Tier B rule prevents a repeated real failure, promote only the smallest durable sentence into Tier A and keep the generated-doc mirrors aligned.
82
98
 
@@ -84,13 +100,15 @@ Tier B guidance lives in `SHARED_BROWSER_PLAYBOOK_GUIDELINES`, generated README/
84
100
 
85
101
  Do **not** add reusable browser recipes as a first-class runtime surface yet.
86
102
 
103
+ One-shot `script` does not change this decision. It accepts source only for the current tool call and has no names, registry, storage, imports, shared state, versioning, or invocation API. Do not layer those recipe capabilities on top of it without a new design pass.
104
+
87
105
  Current evidence does not justify another source of truth for workflows:
88
- - unit/fake-upstream coverage and real-app dogfood exercise `job`, `qa`, experimental lookups, and `electron` lifecycle without a separate accounting benchmark layer
106
+ - unit/fake-upstream coverage and real-app dogfood exercise ad hoc `script`, `job`, `qa`, experimental lookups, and `electron` lifecycle without a separate accounting benchmark layer
89
107
  - repo-local dogfood evidence does not show repeated project-specific job recipes that need versioning or ownership
90
108
  - `qa` already covers the only repeated smoke-test shape with a stable top-level preset
91
109
  - docs and prompt guidance can carry examples without adding recipe state, migration rules, or another schema
92
110
 
93
- Revisit this only when dogfood data shows at least two repeated, failure-prone job sequences that cannot be represented clearly by `job`, `qa`, top-level `electron`, or raw `batch`. If that happens, define ownership, versioning, schema boundaries, generated docs, and tests before adding executable recipes.
111
+ Revisit this only when dogfood data shows at least two repeated, failure-prone sequences that cannot be represented clearly by one-shot `script`, `job`, `qa`, top-level `electron`, or raw `batch`. If that happens, define ownership, versioning, schema boundaries, generated docs, and tests before adding executable recipes.
94
112
 
95
113
  ### Package layout versus local checkout development
96
114
 
@@ -140,12 +158,13 @@ Practical policy:
140
158
  - preserve the current branch-visible extension-managed session across `/reload`, exact-session relaunch, `/resume`, and Pi 0.84.0+ `session_tree` branch transitions so persisted sessions can keep following the live browser after lifecycle changes
141
159
  - close the active extension-managed session when the originating `pi` process quits, while leaving explicit caller-provided sessions alone
142
160
  - set an idle timeout on extension-managed sessions as a backstop for abnormal exits or cleanup failures, and apply that same `AGENT_BROWSER_IDLE_TIMEOUT_MS` value to every upstream subprocess (including wrapper helper snapshots, tab lists, and navigation-summary reads) because changing the launch environment between calls can make upstream restart the background browser, discard the active tab, and invalidate fresh refs
143
- - for wrapper-owned managed sessions only, also set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key on every compatible non-close upstream subprocess so cookies, localStorage, and sessionStorage autosave/restore across idle shutdowns and later Pi chats in the same checkout generation. The wrapper stores a UUID in the resolved Git admin directory and combines it with the checkout root and Git-admin directory filesystem identities: renames preserve the key, copied or path-replacement checkouts get a new key, non-Git directories fail closed, and cwd-only keys are not adopted. Policy lives in `extensions/agent-browser/lib/managed-session-restore.ts`; ownership is resolved by `resolveOwnedManagedSessionContext` (injected managed session, or explicit `--session` equal to the current managed name and namespace) and applied through `AsyncLocalStorage` `withOwnedManagedSessionContext` for prepare helpers plus main process/output, with typed `ownedManagedSession` process options for owned main/close spawns rather than an internal marker leaked into the child environment. `buildOwnedManagedSessionRestoreContext` sets call-scoped `restoreSuppressed` from main-plan argv so helper probes skip restore on incompatible plans without sticky-disabling when prepare returns early; sticky disable commits only after an owned-context subprocess actually starts with suppressed restore policy: POSIX commits on child `spawn`, while PowerShell-backed Windows commits after completion unless command-not-found stderr proves `agent-browser.cmd` never started. No-spawn preflight and missing-binary failures never commit an identity. Duplicate `--session` or `--namespace` flags are rejected, as are leading equals forms that upstream 0.33.2 does not recognize; global identity/config scanning follows upstream across the full argv rather than treating `--` as a sentinel. Native Windows command-first launcher adaptation relocates only valid leading global syntax, canonicalizes a valued optional `--restore <name>` to `--restore=<name>`, consumes only exact lowercase boolean literals, and leaves command-scoped, unknown, or unsupported equals-form input untouched so invalid calls cannot become valid browser activity. Namespace values are canonicalized with upstream's lowercase `sanitize_session_component` algorithm before ownership, sticky/page state, details, socket, or restore-directory identity comparisons; every wrapper-owned subprocess also pins that canonical namespace, including an empty default namespace, so parent environment cannot redirect helpers or close. Electron status target reads and current-managed probes acquire the same daemon-policy lock and owned restore context as ordinary commands for their underlying reads. Probe results then persist the same namespace plus top-level tab/ref state, keeping branch replay keyed to the probed identity. Ownership is typed rather than inferred from a name prefix, and `piab-*` live-session names are reserved: an explicit target is accepted only when it is the current/generated managed session or appears in this extension instance's ownership records. `session list` hides those rows, and the same reservation is rechecked at the final process boundary so another Pi process cannot attach to a managed authenticated browser through the shared per-user daemon socket. Skip when the caller already set restore/profile/state/CDP/provider/auto-connect/containment/session-name or a browser mutation surface (custom executable, extension, init script, raw launch args, proxy, plugin, WebGPU, or related engine/device controls) via argv or matching parent env, when the command is `connect`, when raw batch argv is used, when batch stdin contains nested `connect`/`batch`, or when `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. The wrapper's own site-specific headless user-agent compatibility injection is excluded from caller-mutation policy and remains pinned across follow-up subprocesses for the active managed session. For those owned sessions only, the process boundary also replaces the otherwise-empty safe `--args` value with a fixed, comma-safe Chrome `--user-agent=...` launch argument so new targets inherit the compatibility value; upstream's normal `--user-agent` flag applies only to the active CDP target. A user-private immutable ticket-claim lock keyed by canonical namespace/session serializes this inspect-through-spawn decision across cooperating Pi processes; every contender publishes a unique claim, deterministic tickets elect one owner, and the winner also holds the legacy v2 path as a bridge. The bridge is transitional for pre-release branch processes and is scheduled for removal after v0.2.74 in [#93](https://github.com/fitchmultz/pi-agent-browser-native/issues/93). Live pre-update processes and their in-flight candidate gaps therefore block new acquisition; an abandoned v2 owner fails closed for manual repair, while current-protocol recovery removes only unique claims and artifacts with proven-dead PID/start identity. Waits are asynchronous and bounded. Ordinary acquisition waits one second and fails with retry guidance rather than queueing behind another process's in-flight command, which may retain the lock through a 35-second daemon inspection and the requested browser operation. Every policy-lock winner re-runs `session info` even when this process previously recorded the applied/observed restore key, because another process can restart the same daemon identity between calls. That inspection uses a fixed bounded timeout independent of a caller's shorter `PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` override. Before an incompatible call, the wrapper reads `session info` for the actual same-identity daemon and fails before the requested spawn when that daemon retains any restore key or cannot be inspected. This covers restore-enabled daemons missing from transcript state after a crash and managed sessions launched with an explicit caller restore key; a confirmed inactive daemon remains reusable; a restore-disabled daemon is reusable only when this process recorded its expected null/custom restore policy after an owned spawn or successful policy match. After reload clears process-only provenance, an inactive old daemon may be restarted without restore and that started subprocess records a null daemon policy for its next follow-up. Same-process `session_tree` branch changes retain that process-owned provenance; a new extension instance after reload, restart, or `/resume` intentionally starts without it and fails closed on a still-live restore-disabled daemon even when the transcript restores sticky-disable state. Close the retained-key daemon first, use a fresh wrapper session, or choose a distinct explicit session. Once a managed session hits any allowed incompatible launch path, restore stays disabled for later bare follow-ups on that same session identity. Sticky identities live in the extension-owned `ManagedSessionRestoreState` instance, persist as `details.managedSessionRestoreDisabled`, and are replaced from current-branch rows during branch restore rather than stored in module-global process state. The opt-out returns before config/storage probes and sticky-records a successfully spawned identity as restore-disabled, allowing later calls to reuse that non-restore daemon without tripping the active restore-enabled conflict gate. This is env-based persistence, not a hidden argv relaunch. Upstream still owns restore file paths/modes under `~/.agent-browser/`; set `AGENT_BROWSER_ENCRYPTION_KEY` on multi-user hosts if plaintext session files are unacceptable. Any project `./agent-browser.json`, explicit `--config` / `AGENT_BROWSER_CONFIG`, or `~/.agent-browser/config.json` discovered while planning disables managed restore without reading caller-selected content in the Pi host; owned spawns sticky-disable that session identity. Each subprocess that receives the wrapper restore key, plus every wrapper-owned close, overrides config discovery with a process-private empty `AGENT_BROWSER_CONFIG` (`0400` on POSIX) inside the canonical marked `0700` secure-temp root, closing the check-to-spawn race without trusting project or user config while retaining normal shutdown cleanup and PID/start-identity stale-root recovery after abnormal exit on POSIX and native Windows; versioned Windows identities treat legacy cross-format markers as unknown instead of incorrectly proving PID reuse, and temp ownership marker schema v2 makes older readers ignore new-format markers. Spawn-time revalidation rejects changed checkout identity, restore storage, unpinned launch-mutator environment, foreign managed-session targets, or forbidden managed-state access before agent-browser starts; the same check runs again after protected-config and socket-directory awaits immediately adjacent to the synchronous spawn. A failed fresh command that started agent-browser triggers an exact-identity daemon probe; an active or uninspectable daemon remains current and wrapper-owned so shutdown cleanup can close it, while pre-aborted and missing-binary calls remain unowned. Wrapper-owned close commands canonicalize upstream argv to JSON plus the known namespace/session and `close`, discarding caller config/restore globals, and do not inject a newly derived restore key into an existing daemon, so checkout replacement cannot make old auth save under the replacement generation; the close path retains the observed wrapper key long enough to record the returned old-generation snapshot safely. Because upstream writes a snapshot per daemon session, a successful wrapper-owned close requests JSON output and persists only the returned state path as an atomic record in a lockless convergent per-key ownership directory beside the snapshots (`0700`, with `0600` records, on POSIX). Cleanup carries that ownership proof across Pi restarts, self-heals malformed or stale regular records without claiming their snapshots, uses immutable atomic record names plus rescan-after-delete convergence so concurrent closers cannot skip ownership recording or exceed the aggregate cap, removes proven snapshots older than 30 days for the exact restore key while retaining the two newest, expires stale ownership-proven snapshots and empty manifests from other restore-key generations after 30 days only when a private lineage record proves the same canonical checkout path, caps young close churn at 256 records per key, and never deletes matching unrecorded files or the current checkout key. Upstream restore files under `~/.agent-browser/` remain plaintext unless `AGENT_BROWSER_ENCRYPTION_KEY` is set; before automatic managed restore the wrapper requires a durable Git generation and absolute platform home root; it pins the planned encryption-key value after caller env merging; on POSIX it also resolves `HOME` once, validates owner-trusted non-writable ancestry plus stable device/inode/birth-time metadata for both checkout and Git-admin directories, and pins that canonical value, enforces mode `0700` without silently repairing unsafe existing paths, and rejects symlinks/non-directories along the exact `~/.agent-browser[/namespaces/<canonical>/state]/sessions` path and its `.tmp` transactional-write area, while Windows requires an absolute `USERPROFILE` and the documented 64-character hex encryption key because POSIX mode checks cannot verify profile ACLs; malformed keys fail closed on every platform. POSIX process-start probes use absolute `/bin/ps` then `/usr/bin/ps`; if neither is available, managed policy locking fails closed with an actionable validation message. Managed `piab-r2-*` keys and key-bearing paths are redacted from visible/structured/JSON transcript surfaces. Malformed oversized upstream output is discarded after parsing rather than copied into a persistent parse-failure spill, and raw parse-failure stdout is omitted from result details. `session list` and `state list` filter wrapper-managed rows, and the pre-spawn policy blocks foreign managed restore/state references, broad clear/clean operations, and managed save/rename targets while preserving targeted caller-owned state workflows.
161
+ - for wrapper-owned managed sessions only, also set a Pi-transcript- and Git-checkout-generation-scoped `AGENT_BROWSER_RESTORE` key on every compatible non-close upstream subprocess so cookies, localStorage, and sessionStorage autosave/restore across idle shutdowns, `/reload`, exact-session relaunch, and `/resume` of the same Pi transcript. Upstream 0.33.2 loads the newest file matching one restore key regardless of browser session suffix, so unrelated concurrent Pi transcripts must use distinct keys to prevent state loss or cross-chat cookie/storage bleed. Fresh browser rotations within one Pi transcript share its scope. The wrapper stores a UUID in the resolved Git admin directory and combines it with the checkout root, Git-admin directory filesystem identities, and the transcript's cwd-derived managed-session base name. Android app storage rejects hard-link marker publication, so the Android path uses exclusive-create publication; its device/inode identity remains stable while Node's Android birth-time field follows mutable ctime. On other POSIX hosts the candidate hard-link publication and device/inode/birth-time identity remain unchanged: the generation marker survives renames, but the composite key includes that cwd-derived base name, so a renamed checkout or a different working directory yields a new key (fail-closed; re-authenticate once); copied/path-replacement checkouts and different Pi transcripts get new keys, non-Git directories fail closed, and cwd-only keys are not adopted. Policy lives in `extensions/agent-browser/lib/managed-session-restore.ts`; ownership is resolved by `resolveOwnedManagedSessionContext` (injected managed session, or explicit `--session` equal to the current managed name and namespace) and applied through `AsyncLocalStorage` `withOwnedManagedSessionContext` for prepare helpers plus main process/output, with typed `ownedManagedSession` process options for owned main/close spawns rather than an internal marker leaked into the child environment. `buildOwnedManagedSessionRestoreContext` sets call-scoped `restoreSuppressed` from main-plan argv so helper probes skip restore on incompatible plans without sticky-disabling when prepare returns early; sticky disable commits only after an owned-context subprocess actually starts with suppressed restore policy: POSIX commits on child `spawn`, while PowerShell-backed Windows commits after completion unless command-not-found stderr proves `agent-browser.cmd` never started. No-spawn preflight and missing-binary failures never commit an identity. Duplicate `--session` or `--namespace` flags are rejected, as are leading equals forms that upstream 0.33.2 does not recognize; global identity/config scanning follows upstream across the full argv rather than treating `--` as a sentinel. Native Windows command-first launcher adaptation relocates only valid leading global syntax, canonicalizes a valued optional `--restore <name>` to `--restore=<name>`, consumes only exact lowercase boolean literals, and leaves command-scoped, unknown, or unsupported equals-form input untouched so invalid calls cannot become valid browser activity. Namespace values are canonicalized with upstream's lowercase `sanitize_session_component` algorithm before ownership, sticky/page state, details, socket, or restore-directory identity comparisons; every wrapper-owned subprocess also pins that canonical namespace, including an empty default namespace, so parent environment cannot redirect helpers or close. Electron status target reads and current-managed probes acquire the same daemon-policy lock and owned restore context as ordinary commands for their underlying reads. Probe results then persist the same namespace plus top-level tab/ref state, keeping branch replay keyed to the probed identity. Ownership is typed rather than inferred from a name prefix, and `piab-*` live-session names are reserved: an explicit target is accepted only when it is the current/generated managed session or appears in this extension instance's ownership records. `session list` hides those rows, and the same reservation is rechecked at the final process boundary so another Pi process cannot attach to a managed authenticated browser through the shared per-user daemon socket. Skip when the caller already set restore/profile/state/CDP/provider/auto-connect/containment/session-name or a browser mutation surface (custom executable, extension, init script, raw launch args, proxy, plugin, WebGPU, or related engine/device controls) via argv or matching parent env, when the command is `connect`, when raw batch argv is used, when batch stdin contains nested `connect`/`batch`, or when `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. The wrapper's own site-specific headless user-agent compatibility injection is excluded from caller-mutation policy and retained as managed-session state, but its launch flags are omitted while daemon inspection confirms that session is active. Initial launches and sessions proven inactive receive the retained compatibility launch values, including a fixed, comma-safe Chrome `--user-agent=...` argument so new targets inherit the value; re-emitting either to an active launch-configured session can replace its browser with `about:blank` in upstream 0.34.0. A user-private immutable ticket-claim lock keyed by canonical namespace/session serializes this inspect-through-spawn decision across cooperating Pi processes; every contender publishes a unique claim, deterministic tickets elect one owner, and the winner also holds the legacy v2 path as a bridge. The bridge is transitional for pre-release branch processes and is scheduled for removal after v0.2.74 in [#93](https://github.com/fitchmultz/pi-agent-browser-native/issues/93). Live pre-update processes and their in-flight candidate gaps therefore block new acquisition; an abandoned v2 owner fails closed for manual repair, while current-protocol recovery removes only unique claims and artifacts with proven-dead PID/start identity. Waits are asynchronous and bounded. Ordinary acquisition waits one second and fails with retry guidance rather than queueing behind another process's in-flight command, which may retain the lock through a 35-second daemon inspection and the requested browser operation. Every policy-lock winner re-runs `session info` even when this process previously recorded the applied/observed restore key, because another process can restart the same daemon identity between calls. That inspection uses a fixed bounded timeout independent of a caller's shorter `PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` override. Before an incompatible call, the wrapper reads `session info` for the actual same-identity daemon and fails before the requested spawn when that daemon retains any restore key or cannot be inspected. This covers restore-enabled daemons missing from transcript state after a crash and managed sessions launched with an explicit caller restore key; a confirmed inactive daemon remains reusable; a restore-disabled daemon is reusable only when this process recorded its expected null/custom restore policy after an owned spawn or successful policy match. After reload clears process-only provenance, an inactive old daemon may be restarted without restore and that started subprocess records a null daemon policy for its next follow-up. Same-process `session_tree` branch changes retain that process-owned provenance; a new extension instance after reload, restart, or `/resume` intentionally starts without it and fails closed on a still-live restore-disabled daemon even when the transcript restores sticky-disable state. Close the retained-key daemon first, use a fresh wrapper session, or choose a distinct explicit session. Once a managed session hits any allowed incompatible launch path, restore stays disabled for later bare follow-ups on that same session identity. Sticky identities live in the extension-owned `ManagedSessionRestoreState` instance, persist as `details.managedSessionRestoreDisabled`, and are replaced from current-branch rows during branch restore rather than stored in module-global process state. The opt-out returns before config/storage probes and sticky-records a successfully spawned identity as restore-disabled, allowing later calls to reuse that non-restore daemon without tripping the active restore-enabled conflict gate. This is env-based persistence, not a hidden argv relaunch. Upstream still owns restore file paths/modes under `~/.agent-browser/`; set `AGENT_BROWSER_ENCRYPTION_KEY` on multi-user hosts if plaintext session files are unacceptable. Passive upstream `./agent-browser.json` and `~/.agent-browser/config.json` files do not disable managed restore because accepted browser-backed subprocesses pin a protected empty config. Only an explicit `--config` argument or `AGENT_BROWSER_CONFIG` environment override disables restore without reading caller-selected content in the Pi host; an owned spawn with either explicit override sticky-disables that session identity. Each subprocess that receives the wrapper restore key, plus every wrapper-owned close, overrides config discovery with a process-private empty `AGENT_BROWSER_CONFIG` (`0400` on POSIX) inside the canonical marked `0700` secure-temp root, closing the check-to-spawn race without trusting project or user config while retaining normal shutdown cleanup and PID/start-identity stale-root recovery after abnormal exit on POSIX and native Windows; versioned Windows identities treat legacy cross-format markers as unknown instead of incorrectly proving PID reuse, and temp ownership marker schema v2 makes older readers ignore new-format markers. Spawn-time revalidation rejects changed checkout identity, restore storage, unpinned launch-mutator environment, foreign managed-session targets, or forbidden managed-state access before agent-browser starts; the same check runs again after protected-config and socket-directory awaits immediately adjacent to the synchronous spawn. A failed fresh command that started agent-browser triggers an exact-identity daemon probe; an active or uninspectable daemon remains current and wrapper-owned so shutdown cleanup can close it, while pre-aborted and missing-binary calls remain unowned. Wrapper-owned close commands canonicalize upstream argv to JSON plus the known namespace/session and `close`, discarding caller config/restore globals, and do not inject a newly derived restore key into an existing daemon, so checkout replacement cannot make old auth save under the replacement generation; the close path retains the observed wrapper key long enough to record the returned old-generation snapshot safely. Because upstream writes a snapshot per daemon session, a successful wrapper-owned close requests JSON output and persists only the returned state path as an atomic record in a lockless convergent per-key ownership directory beside the snapshots (`0700`, with `0600` records, on POSIX). Cleanup carries that ownership proof across Pi restarts, self-heals malformed or stale regular records without claiming their snapshots, uses immutable atomic record names plus rescan-after-delete convergence so concurrent closers cannot skip ownership recording or exceed the aggregate cap, removes proven snapshots older than 30 days for the exact restore key while retaining the two newest, expires stale ownership-proven snapshots and empty manifests from other restore-key generations after 30 days only when a private lineage record proves the same canonical checkout path, caps young close churn at 256 records per key, and never deletes matching unrecorded files or the current checkout key. Upstream restore files under `~/.agent-browser/` remain plaintext unless `AGENT_BROWSER_ENCRYPTION_KEY` is set; before automatic managed restore the wrapper requires a durable Git generation and absolute platform home root; it pins the planned encryption-key value after caller env merging; on POSIX it also resolves `HOME` once, validates owner-trusted non-writable ancestry plus stable device/inode/birth-time metadata for both checkout and Git-admin directories (Android recognizes the private app-data sandbox and uses stable device/inode identity plus the generation UUID because Node reports mutable ctime as birth time), and pins that canonical value, enforces mode `0700` without silently repairing unsafe existing paths, and rejects symlinks/non-directories along the exact `~/.agent-browser[/namespaces/<canonical>/state]/sessions` path and its `.tmp` transactional-write area, while Windows requires an absolute `USERPROFILE` and the documented 64-character hex encryption key because POSIX mode checks cannot verify profile ACLs; malformed keys fail closed on every platform. POSIX process-start probes use absolute `/bin/ps` then `/usr/bin/ps`; Android/Termux first uses `ps` beside `process.execPath`, where Termux installs it. If no platform candidate is available, managed policy locking fails closed with an actionable validation message. Managed `piab-r2-*` keys and key-bearing paths are redacted from visible/structured/JSON transcript surfaces. Malformed oversized upstream output is discarded after parsing rather than copied into a persistent parse-failure spill, and raw parse-failure stdout is omitted from result details. `session list` and `state list` filter wrapper-managed rows, and the pre-spawn policy blocks foreign managed restore/state references, broad clear/clean operations, and managed save/rename targets while preserving targeted caller-owned state workflows.
144
162
  - clean up process-private temp spill artifacts on shutdown, but keep persisted-session snapshot spill files in a private session-scoped artifact directory with a bounded per-session budget so `details.fullOutputPath` stays usable after reload/resume without unbounded growth
145
163
  - keep explicit screenshots, downloads, PDFs, traces, HAR captures, and recordings written to caller-chosen paths on disk after a successful upstream close command (`close`, `quit`, or `exit`); before artifact-producing commands run, create missing parent directories for requested host paths, and for simple loopback HTML anchor downloads with resolvable HTTP(S) hrefs the wrapper may save directly to the requested path before upstream fallback. When the bounded `details.artifactManifest` has entries, successful close commands also surface `details.artifactCleanup` and a compact `Artifact lifecycle` note pointing to structured explicit paths so operators remove files with normal host tools—the native tool does not delete arbitrary user paths (`extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts`, `getArtifactCleanupGuidance`); contract in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), checklist `RQ-0079` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
146
- - reconstruct the current branch-visible extension-managed session, every transcript-proven still-active wrapper-owned managed identity, page-scoped refs, newest-revision aggregate artifact manifest, and Electron launch records from the active transcript branch on `session_start` and `session_tree` so later default and explicit off-current calls keep following owned browsers after resume/reload or branch switching; restore also honors successful explicit `--session <wrapper-owned> close` rows and `electron.cleanup` managed-session steps so closed wrapper-owned sessions are not resurrected
147
- - keep process-owned cleanup registries for extension-managed sessions and wrapper-launched Electron records separate from the current branch-visible view; `session_tree` restore and wrapper-owned browser commands are serialized with managed-session work, while caller-owned explicit-session commands are serialized by process-local queues keyed to effective canonical namespace/session across prepare helpers (explicit namespace argv overrides inherited `AGENT_BROWSER_NAMESPACE`, including an explicit empty default) and main execution. macOS and Windows additionally normalize and case-fold namespace and session components to match case-insensitive daemon identity. Different caller-owned identities remain concurrent, nested helpers never re-enter the outer queue, policy/route/artifact deltas merge across unrelated managed-state commits, and a separate branch-restore generation guard prevents stale completions from overwriting newer branch-visible state; aggregate artifact results use monotonic revisions so transcript replay cannot lose a concurrently completed entry. Branch switches still must not drop resources the current Pi process owns and must keep fresh-session allocation monotonic
148
- - record successful `connect`, `--cdp`, enabled `--auto-connect`, environment-configured CDP/auto-connect, and wrapper Electron attachment identities in branch-visible state, then run their main commands, helper probes, and cleanup inside an attached-browser process context that omits wrapper launch-only `--args` / `--allow-file-access` defaults. Re-sending those flags makes upstream choose local launch instead of its existing CDP connection and can trigger remote-debugging permission on every call. Block first-use attachment/content combinations until URL verification, and apply the existing live `get url` gate before every later content-bearing read or interaction because attached targets can drift outside Pi. Every child clears `AGENT_BROWSER_ALLOW_FILE_ACCESS`, including attached calls that omit the canonical launch flags. Successful close removes the marker; existing config, environment, URL, and protected-path checks still apply
164
+ - reconstruct the current branch-visible extension-managed session, every transcript-proven still-active wrapper-owned managed identity, page-scoped refs, newest-revision aggregate artifact manifest, unbounded active-recording reservation events, and Electron launch records from the active transcript branch on `session_start` and `session_tree` so later default and explicit off-current calls keep following owned browsers after resume/reload or branch switching; restore also honors successful explicit `--session <wrapper-owned> close` rows, terminal nested-batch close outcomes even when aggregate artifact verification failed, and `electron.cleanup` managed-session steps so closed wrapper-owned sessions are not resurrected; a nested close invalidates the pre-close page target so a lifecycle-proven relaunch at `about:blank` is not treated as stale focus drift; explicit lifecycle evidence that a later diagnostic did not launch a browser preserves the terminal close, while any later row—including a failed row—whose lifecycle reports a browser launch keeps active/attached provenance; failed-step presentation persists only that bounded launch boolean so transcript replay reaches the same decision, missing lifecycle evidence remains conservatively active even on the first managed call, successful closes clear wrapper trace/profiler ownership before ordered later successful rows can rebuild it, namespace-scoped `close --all` clears all matching managed/attached/page/ref/route/trace/recording ownership, and recording starts after close are rejected before spawn
165
+ - keep active recording destination reservations separate from the bounded metadata-only artifact manifest. The process-wide map is keyed by canonical namespace/session identity, rebuilt from append-only branch events, and retained for still-live process-owned recordings across branch switches. Shutdown/reload appends both terminal tombstones and still-live reservations onto the current branch so restart cannot resurrect a cross-branch close or lose a live-daemon reservation. One artifact lifecycle/output queue makes global destination preflight and reservation updates atomic across otherwise-concurrent caller-owned session queues. Every successful direct, ordered nested-batch, managed replacement, script, Electron, or shutdown close retires its exact identity at that lifecycle point; only the newest pending recording path remains authoritative across live merge and transcript replay (including same-timestamp restart rows), legacy terminal batch closes override stale pending manifests, and recording starts after a nested close are rejected because upstream can falsely report success. Existing and dangling symlink ancestry, hardlink inode identity, full Unicode/platform case folding, and same-call `outputPath` comparison prevent alias reuse. One shared command-token projection mirrors upstream's full-argv global cleanup before artifact, recording, presentation, and protected-path parsing; wait-download detection removes only the first timeout pair, follows upstream long/short mode precedence, and accepts both `--download` and `-d` wherever download mode wins; screenshot destinations use upstream's exact-flag, selector-prefix, case-sensitive extension, slash-path, and second-positional rules, while retaining the wrapper's intentional slash-bearing hidden-workspace path normalization. Legacy aggregate replay retires a pending manifest only when the ordered lifecycle leaves recording closed, and malformed manifest rows are ignored rather than reaching session/path identity code
166
+ - keep process-owned cleanup registries for extension-managed sessions and wrapper-launched Electron records separate from the current branch-visible view; `session_tree` restore and wrapper-owned browser commands are serialized with managed-session work, while caller-owned explicit-session commands are serialized by process-local queues keyed to effective canonical namespace/session across prepare helpers (explicit namespace argv overrides inherited `AGENT_BROWSER_NAMESPACE`, including an explicit empty default) and main execution. macOS and Windows additionally normalize and case-fold namespace and session components to match case-insensitive daemon identity. Different caller-owned identities remain concurrent, except namespace-scoped `close --all` drains and exclusively barriers managed plus matching caller-owned work before clearing global namespace state; nested helpers never re-enter the outer queue, policy/route/artifact deltas merge across unrelated managed-state commits, and a separate branch-restore generation guard prevents stale completions from overwriting newer branch-visible state; aggregate artifact results use monotonic revisions so transcript replay cannot lose a concurrently completed entry. Branch switches still must not drop resources the current Pi process owns and must keep fresh-session allocation monotonic
167
+ - record successful `connect`, `--cdp`, enabled `--auto-connect`, environment-configured CDP/auto-connect, and wrapper Electron attachment identities in branch-visible state, then run their main commands, helper probes, and cleanup inside an attached-browser process context that omits wrapper launch-only `--args` / `--allow-file-access` defaults. Re-sending those flags makes upstream choose local launch instead of its existing CDP connection and can trigger remote-debugging permission on every call. Block first-use attachment/content combinations until URL verification, and apply the existing live `get url` gate before every later content-bearing read or interaction because attached targets can drift outside Pi. Every child clears `AGENT_BROWSER_ALLOW_FILE_ACCESS`, including attached calls that omit the canonical launch flags. A terminal successful close removes the marker; a close followed by a later step whose lifecycle reports a browser launch preserves it, while a non-launching diagnostic such as `stream status` leaves the close terminal. Existing config, environment, URL, and protected-path checks still apply
149
168
  - when a successful close targets the current extension-managed session, including an explicit `--session <current> close` or an `electron.cleanup` managed-session step, clear page/ref state, mark that session inactive, untrack cleanup ownership, and rotate the next default auto call to a fresh wrapper-generated session name rather than reusing the closed name
150
169
  - on non-quit shutdown such as `/reload`, close off-branch owned managed sessions and off-branch owned Electron launches before clearing process-local ownership, but preserve the current branch-visible active managed session and Electron launch plus that launch's isolated `userDataDir` so reload continuity still works from the active transcript branch
151
170
  - expose still-owned off-branch Electron launch records to `electron.status { launchId }`, `electron.status { all: true }`, `electron.probe { launchId }`, and `electron.cleanup`, while leaving default `electron.probe` scoped to the current managed session
@@ -155,11 +174,11 @@ Practical policy:
155
174
  - once the wrapper observes tab-drift risk for a session (profile restore correction, overlapping stale opens, or restored session state), later active-tab commands may synthesize a tiny upstream `batch` that re-selects that tab and then runs the requested command in the same upstream invocation; routine same-session commands avoid `tab list` preflights to reduce probes that can perturb upstream click behavior
156
175
  - for sessions with observed tab-drift risk, after a successful command on a known tab target, the wrapper may best-effort restore that same target again if restored/background tabs steal focus after the command returns; routine same-session commands skip this post-command `tab list` probe
157
176
  - after successful `tab close`, read the now-active URL/title before updating per-session page state because upstream's close payload identifies the closed tab but does not report the remaining active tab; this keeps subsequent ref guards and interactions aligned without requiring a recovery snapshot solely to repair wrapper state
158
- - keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), and refuse mutation-prone `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, or when `batch` stdin would reuse `@e…` on a guarded step after an earlier invalidating step without a later `snapshot` step in the same stdin array. Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
177
+ - keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore it from persisted tool `details` when reloading, resuming, or moving to a different Pi session-tree branch, store bounded ref role/name metadata from the same snapshot for wrapper-side current-ref diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; upstream swaps the session to a fresh active page before its already-active check, so failed starts count) or a `record restart` with a URL operand (which navigates the current page; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned steps containing such a recording swap still record the invalidation), or after a failed non-batch transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection) whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start` and URL-bearing `record restart`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); the tab-pinned batch rewrite (which re-emits the caller's exact `--bail` token so fail-fast semantics survive the rewrite), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
159
178
  - for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it uses that role/name metadata, including snapshot-order `duplicateIndex` for duplicate-name refs, instead of taking a fresh pre-click snapshot that could recycle upstream refs. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`job`/`qa` click steps
160
179
  - derive narrow prompt guards only for concrete evidence invariants: explicitly requested screenshot/recording output paths block browser close until the artifact manifest verifies those paths, while bare inbound attachment paths remain inputs. The wrapper intentionally does not infer broad business/user intent from prompt text such as order/payment/post boundaries; agents must follow those instructions themselves. The artifact guard is bounded preflight policy (`details.promptGuard`, `failureCategory: "policy-blocked"`), not a reusable browser recipe layer
161
180
  - after successful `get text` on a qualifying non-ref CSS selector, optionally issue one read-only `eval --stdin` probe per selector when multiple DOM matches or a hidden first match with visible peers could misread tabbed or off-screen content; simple id selectors and sensitive-looking literals skip this probe. Merge `details.selectorTextVisibility` / `selectorTextVisibilityAll`, visible warning lines, and `inspect-visible-text-candidates*` next actions as documented in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) and `RQ-0074` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
162
- - for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under trusted non-replaceable ancestry, and free of symlink, foreign-owner, or special planted entries; reject pre-existing unsafe modes instead of repairing them, then recheck before spawn
181
+ - for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit; require the selected path to be absolute, owned by the current uid, mode `0700`, under trusted non-replaceable ancestry, and free of symlink, foreign-owner, or special planted entries; reject pre-existing unsafe modes instead of repairing them, then recheck before spawn. Android/Termux uses `/data/data/<package>/piab`, treats the owner-only app-data directory as the trust anchor, permits the app's matching private uid/gid ancestry, compacts generated managed identities to one 80-bit digest so ordinary namespace plus fresh-session paths remain within the limit, and places policy-lock coordination under `os.tmpdir()` because Android `/tmp` is shell-owned and inaccessible
163
182
  - keep wrapper-spawned upstream CLI calls bounded by clamping `AGENT_BROWSER_DEFAULT_TIMEOUT` to the upstream documented 25-second default while deriving a longer subprocess watchdog for explicit long `wait <ms>` / `wait --timeout <ms>` calls; dialog commands, likely dialog-trigger clicks/taps/finds, and `eval --stdin` snippets that look like alert/confirm/prompt/dialog triggers use shorter wrapper subprocess budgets so blocking JavaScript prompts surface recovery actions before the full default watchdog
164
183
 
165
184
  This is primarily about ownership clarity and avoiding surprise, not adding a heavy safety wrapper. If the extension invented the session, the extension should own its lifecycle without breaking reload, resume, or branch-tree semantics. If the caller explicitly chose the upstream session model, the extension should stay out of the way.
@@ -169,13 +188,13 @@ This is primarily about ownership clarity and avoiding surprise, not adding a he
169
188
  `agent-browser` startup flags are sticky once a session is already running.
170
189
  The extension should surface that clearly and avoid hidden restart behavior in v1.
171
190
 
172
- That means explicit startup-scoping flags like `--allowed-domains`, `--auto-connect`, `--cdp`, `--enable`, `--executable-path`, `--webgpu`, `--headed`, `--init-script`, `--device`, `--namespace`, `--profile`, `--provider`, `-p`, `--restore`, `--restore-save`, restore check flags, `--session-name`, and `--state` should remain explicit upstream argv choices instead of being wrapped in extra hidden restart or cloning logic. The one deliberate exception is the env-only managed-session `AGENT_BROWSER_RESTORE` key above, which does not inject `--restore` into argv and therefore does not trip launch-scoped `sessionMode: "fresh"` recovery.
191
+ That means explicit startup-scoping flags like `--allowed-domains`, `--auto-connect`, `--args`, `--user-agent`, `--cdp`, `--enable`, `--executable-path`, `--webgpu`, `--headed`, `--init-script`, `--device`, `--namespace`, `--profile`, `--provider`, `-p`, `--restore`, `--restore-save`, restore check flags, `--session-name`, and `--state` should remain explicit upstream argv choices instead of being wrapped in extra hidden restart or cloning logic. The one deliberate exception is the env-only managed-session `AGENT_BROWSER_RESTORE` key above, which does not inject `--restore` into argv and therefore does not trip launch-scoped `sessionMode: "fresh"` recovery.
173
192
 
174
- The wrapper may still apply narrow compatibility normalizations when observed behavior justifies them and the result remains thin, local, and opt-out. For example, OpenAI web properties and `dash.cloudflare.com` reject the default local `HeadlessChrome` user agent while the same flow works with a normal Chrome UA, so the extension injects a domain-specific fallback only when the caller did not already choose raw Chrome arguments, a custom user agent, headed mode, CDP, auto-connect, a provider-backed launch, or a non-Chrome engine through argv or matching upstream environment. Managed sessions retain the injected value across helper and follow-up subprocesses, including branch reload/resume, because omitting it can make upstream relaunch the browser with a different launch identity. The process boundary also pins the same fixed compatibility value as a comma-safe Chrome launch argument for owned sessions, covering tabs and SSO popups that do not inherit upstream's per-page CDP override. Wrapper-owned headed launches also default upstream periodic restore autosave off because agent-browser 0.33.2 collects non-current origins through visible temporary targets while holding the daemon state lock; save-on-close remains enabled, and an explicit `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` value opts in when the daemon launches. The effective interval is retained in owned-session state and transcript results; changing it in either direction on a running wrapper-owned headed daemon is rejected until close plus a fresh launch.
193
+ The wrapper may still apply narrow compatibility normalizations when observed behavior justifies them and the result remains thin, local, and opt-out. For example, OpenAI web properties and `dash.cloudflare.com` reject the default local `HeadlessChrome` user agent while the same flow works with a normal Chrome UA, so the extension injects a domain-specific fallback only when the caller did not already choose raw Chrome arguments, a custom user agent, headed mode, CDP, auto-connect, a provider-backed launch, or a non-Chrome engine through argv or matching upstream environment. Managed sessions retain the injected value as per-session wrapper state across helper calls and branch reload/resume. Active daemons omit both launch forms so upstream does not replace a launch-configured browser; a session proven inactive receives the retained compatibility launch values, including the same fixed compatibility value as a comma-safe Chrome launch argument covering tabs and SSO popups that do not inherit upstream's per-page CDP override. Wrapper-owned headed launches also default upstream periodic restore autosave off because agent-browser 0.33.2 collects non-current origins through visible temporary targets while holding the daemon state lock; save-on-close remains enabled, and an explicit `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` value opts in when the daemon launches. The effective interval is retained in owned-session state and transcript results; changing it in either direction on a running wrapper-owned headed daemon is rejected until close plus a fresh launch.
175
194
 
176
- If the implicit session is already active and one of those startup-scoped flags appears again while `sessionMode` is still `"auto"`, the extension should fail clearly instead of silently sending a command shape that upstream would ignore.
195
+ If the current managed session is already active and one of those startup-scoped flags appears again while `sessionMode` is still `"auto"`, the extension should fail clearly instead of silently sending a command shape that upstream would ignore. An explicitly targeted older wrapper-owned session gets the same protection after daemon inspection proves it active.
177
196
 
178
- That failure should include a structured recovery hint pointing to `sessionMode: "fresh"` as the first-line fix, while still allowing an explicit `--session` when the caller wants to name the new upstream session.
197
+ The current-session failure should include a structured recovery hint pointing to `sessionMode: "fresh"` as the first-line fix. If the call explicitly targets the current managed session, the recovery payload removes that `--session` so fresh rotation is actionable. An active older owned session instead requires close first or removing the explicit session before a fresh rotation; caller-owned explicit sessions remain available when the caller intentionally names a separate upstream session.
179
198
 
180
199
  Implementation detail lives in `extensions/agent-browser/lib/launch-scoped-flags.ts` (canonical flag metadata shared with playbook/docs assertions), `extensions/agent-browser/lib/argv-descriptor.ts` and `extensions/agent-browser/lib/argv-grammar.ts` (command discovery, `VALUE_FLAGS`, `parseArgvDescriptor`) plus `extensions/agent-browser/lib/runtime.ts` (`getStartupScopedFlags`, `buildExecutionPlan`):
181
200
 
@@ -186,7 +205,7 @@ Implementation detail lives in `extensions/agent-browser/lib/launch-scoped-flags
186
205
  - **`--headed`:** Treated as launch-scoped for both enabled and explicit `false` values so a visible-window choice cannot be silently ignored by an already-running managed session.
187
206
  - **`--allowed-domains`:** Treated as launch-scoped so containment cannot silently relaunch or reuse the active implicit browser. Upstream 0.32.0 owns request, worker, popup, and WebRTC containment and rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and unsafe startup/profile Chrome args; the wrapper keeps only a final-URL policy check as defense in depth.
188
207
 
189
- **Sessionless inspection and local commands:** Plain-text global help and version probes (`--help`, `-h`, `--version`, `-V`) must never allocate or bind the extension-managed session. The same session-ownership rule applies to read-only upstream `skills list`, `skills get …`, and `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), plus local/setup surfaces such as `profiles`, `dashboard start/stop`, `device list`, `doctor`, `install`, `upgrade`, `session id`, `session info`, `session list`, and caller-owned local saved-state maintenance (`state list/show`, targeted `state clear <caller-owned-name>`, and `state rename`). Broad clear/clean and managed-state targets remain syntactically sessionless but are rejected by the pre-spawn managed-state boundary. The same boundary rejects any discovered or explicit upstream config for browser-backed native calls without reading it, pins an empty private config for accepted browser-backed spawns to close config-creation races, and rejects browser access to `.agent-browser` local storage through command-specific input/output operands (including dash-prefixed values), their path-bearing environment mirrors (state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked paths (including nonexistent descendants of symlinked directories), protected top-level `outputPath`, content-returning local-URL calls, any follow-up on a local file page, and persisted unverified top-level or batch tab/attachment/script/state-load transitions, and recursively inspected raw batch command strings. Electron launch handoff, status/probe reads, and capture from a tracked protected target use the same guard; snapshot/tabs handoff and status/probe reads verify the live URL before tab/title/content helpers, while handoff failure or cancellation closes the managed session and host process/profile. Raw artifact destinations use the same command parser as preparation and are checked before it creates parent directories. Enabled `--allow-file-access` argv/env and raw Chrome file-access or protected-path values are rejected; every upstream spawn clears `AGENT_BROWSER_ARGS`, removes caller file-access occurrences, and adds canonical `--args "" --allow-file-access false` defaults so project/user config cannot override the boundary while a validated safe CLI `--args` value remains usable. Post-transition summaries, including forced live probes after arbitrary `eval`, verify URL before reading title and fail an implicit transition that lands on a local file page. Failed/unexecuted navigation stays unverified, stale completions serialize authoritative state only, and replay treats unknown state as dominant over inconsistent target/ref fields. `get url`, `tab list`, non-content `tab <id>` selection, explicit safe HTTP(S) navigation away, and close remain available; tab selection stays unverified until `get url` succeeds. `session list` and `state list` remove wrapper-managed rows, and explicit `piab-*` live-session targets require an ownership record from the current extension instance. Non-plain-text sessionless commands still run with `--json` for machine-readable output, but the planner does not prepend the implicit managed `--session`, so an agent can inspect local capabilities or start/stop the standalone dashboard without consuming the implicit session slot before a real `open`. Browser-backed, context-dependent, or incomplete commands such as root `session`, untargeted `state clear`, bare `state clean`, `auth login`, `state save`, and `state load` keep normal managed-session injection. Command-shape allowlisting lives in `extensions/agent-browser/lib/command-policy.ts` (`needsManagedSession`), while `extensions/agent-browser/lib/runtime.ts` (`isPlainTextInspectionArgs`, `buildExecutionPlan`) applies that decision to execution planning.
208
+ **Sessionless inspection and local commands:** Plain-text global help and version probes (`--help`, `-h`, `--version`, `-V`) must never allocate or bind the extension-managed session. The same session-ownership rule applies to read-only upstream `skills list`, `skills get …`, and `skills path …`, local auth profile management (`auth save/list/show/delete/remove`), plus local/setup surfaces such as `profiles`, `dashboard start/stop`, `device list`, `doctor`, `install`, `upgrade`, `session id`, `session info`, `session list`, and caller-owned local saved-state maintenance (`state list/show`, targeted `state clear <caller-owned-name>`, and `state rename`). Broad clear/clean and managed-state targets remain syntactically sessionless but are rejected by the pre-spawn managed-state boundary. The same boundary rejects any discovered or explicit upstream config for browser-backed native calls without reading it, pins an empty private config for accepted browser-backed spawns to close config-creation races, and rejects browser access to `.agent-browser` local storage through command-specific input/output operands (including dash-prefixed values), their path-bearing environment mirrors (state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked paths (including nonexistent descendants of symlinked directories), protected top-level `outputPath`, content-returning local-URL calls, any follow-up on a local file page, and persisted unverified top-level or batch tab/attachment/script/state-load transitions, and recursively inspected raw batch command strings. Electron launch handoff, status/probe reads, and capture from a tracked protected target use the same guard; snapshot/tabs handoff and status/probe reads verify the live URL before tab/title/content helpers, while handoff failure or cancellation closes the managed session and host process/profile. Raw artifact destinations use the same command parser as preparation and are checked before it creates parent directories. Enabled `--allow-file-access` argv/env and raw Chrome file-access or protected-path values are rejected; every upstream spawn clears `AGENT_BROWSER_ARGS`, removes caller file-access occurrences, and adds canonical `--allow-file-access false`; routine HTTP(S) work relies on the protected empty config and cleared raw-args environment instead of sending the empty `--args` launch override that resets profiled sessions in `agent-browser 0.34.0`. Local-file navigation is limited to wrapper-managed local browsers because caller-owned and attached launch provenance is unknown, while the fixed non-empty compatibility argument is emitted only for launches or proven-inactive daemons and validated safe caller `--args` values remain usable as launch-scoped input. Post-transition summaries, including forced live probes after arbitrary `eval`, verify URL before reading title and fail an implicit transition that lands on a local file page. Failed/unexecuted navigation triggers a live `get url` re-verification probe (non-batch commands only, browser started, not aborted or watchdog-timed-out): an observed http(s) page stays verified, an observed file page stays gated, and a failed or empty probe keeps the target unverified. Stale completions serialize authoritative state only, and replay treats unknown state as dominant over inconsistent target/ref fields. `get url`, `tab list`, non-content `tab <id>` selection, explicit safe HTTP(S) navigation away, and close remain available; tab selection stays unverified until `get url` succeeds. `session list` and `state list` remove wrapper-managed rows, and explicit `piab-*` live-session targets require an ownership record from the current extension instance. Non-plain-text sessionless commands still run with `--json` for machine-readable output, but the planner does not prepend the implicit managed `--session`, so an agent can inspect local capabilities or start/stop the standalone dashboard without consuming the implicit session slot before a real `open`. Browser-backed, context-dependent, or incomplete commands such as root `session`, untargeted `state clear`, bare `state clean`, `auth login`, `state save`, and `state load` keep normal managed-session injection. Command-shape allowlisting lives in `extensions/agent-browser/lib/command-policy.ts` (`needsManagedSession`), while `extensions/agent-browser/lib/runtime.ts` (`isPlainTextInspectionArgs`, `buildExecutionPlan`) applies that decision to execution planning.
190
209
 
191
210
  A successful unnamed `sessionMode: "fresh"` launch should become the new extension-managed session so later default calls follow that browser instead of silently snapping back to the older managed session.
192
211
 
@@ -227,7 +246,7 @@ Upstream restore-state persistence remains upstream-owned. The wrapper passes an
227
246
 
228
247
  The extension does not ship `agent-browser`, but it does ship maintainer-owned documentation that must stay aligned when upstream help text grows. That work splits into two checks with different responsibilities:
229
248
 
230
- 1. **Canonical baseline metadata** lives in `scripts/agent-browser-capability-baseline.mjs` (target version, which `agent-browser` help invocations to sample in live checks, and which literal tokens must appear in upstream help and in human-written `docs/COMMAND_REFERENCE.md` inventory sections). That file does not execute `agent-browser`; rebasing it is an explicit edit after comparing real `--help` output from the installed binary.
249
+ 1. **Canonical target and baseline metadata** are split deliberately: `scripts/agent-browser-target.mjs` owns the runtime target version, while `scripts/agent-browser-capability-baseline.mjs` imports it and declares which `agent-browser` help invocations to sample plus which literal tokens must appear in upstream help and human-written `docs/COMMAND_REFERENCE.md` inventory sections. Neither file executes `agent-browser`; rebasing them is an explicit edit after comparing real `--help` output from the installed binary.
231
250
 
232
251
  2. **Generated Markdown blocks** in `docs/COMMAND_REFERENCE.md` are bounded by stable HTML comments. `scripts/check-command-reference-baseline.mjs` renders those blocks from the baseline metadata only. Use `npm run docs -- command-reference check` or `npm run docs -- command-reference write` after baseline edits so checked-in blocks cannot drift silently.
233
252