pi-agent-browser-native 0.9.0 → 0.9.2

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 (47) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +19 -3
  3. package/dist/extensions/agent-browser/index.js +3 -107
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
  8. package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
  9. package/dist/extensions/agent-browser/lib/config.js +3 -48
  10. package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
  13. package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +4 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
  16. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +4 -3
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +6 -2
  23. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
  24. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +4 -3
  25. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +1 -1
  26. package/dist/extensions/agent-browser/lib/playbook.js +5 -4
  27. package/dist/extensions/agent-browser/lib/process.js +0 -3
  28. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
  29. package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
  30. package/dist/extensions/agent-browser/lib/results/network.js +1 -1
  31. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +7 -1
  32. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
  33. package/dist/extensions/agent-browser/lib/results/presentation/common.js +1 -0
  34. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
  35. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
  36. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
  37. package/dist/extensions/agent-browser/lib/runtime.js +4 -2
  38. package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
  39. package/dist/extensions/agent-browser/lib/web-search.js +15 -15
  40. package/docs/ARCHITECTURE.md +9 -8
  41. package/docs/COMMAND_REFERENCE.md +26 -4
  42. package/docs/RELEASE.md +1 -1
  43. package/docs/REQUIREMENTS.md +2 -1
  44. package/docs/SUPPORT_MATRIX.md +6 -3
  45. package/docs/TOOL_CONTRACT.md +12 -9
  46. package/package.json +3 -3
  47. package/scripts/doctor.mjs +10 -2
@@ -104,9 +104,10 @@ async function runAgentBrowserToolInContext(options) {
104
104
  const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
105
105
  applyBrowserRunStatePatch(options.state, output.statePatch);
106
106
  if (isRecord(output.result.details) && Array.isArray(output.result.details.imageObservations) && prepared.commandTokens[0] === "screenshot") {
107
- output.result.details.imageObservations = redactSensitiveValue(output.result.details.imageObservations.map(image => ({
108
- ...image, geometry: buildScreenshotGeometry({ capture: image.capture, pixels: image.pixels, before, after }),
109
- })));
107
+ output.result.details.imageObservations = redactSensitiveValue(output.result.details.imageObservations.map(image => {
108
+ const rendering = image.rendering ?? before?.rendering ?? after?.rendering;
109
+ return { ...image, ...(rendering ? { rendering } : {}), geometry: buildScreenshotGeometry({ rendering, capture: image.capture, pixels: image.pixels, before, after }) };
110
+ }));
110
111
  }
111
112
  return output.result;
112
113
  }
@@ -333,7 +333,7 @@ function resolveSemanticActionVisibleRefArgsFromSnapshot(compiled, snapshotData)
333
333
  return undefined;
334
334
  return { args: [...getCompiledSemanticActionSessionPrefix(compiled), ...resolution.args], snapshot: resolution.snapshot };
335
335
  }
336
- export async function resolveSemanticActionVisibleRefArgs(options) {
336
+ async function resolveSemanticActionVisibleRefArgs(options) {
337
337
  if (!options.compiled || !options.sessionName)
338
338
  return undefined;
339
339
  const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "../../parsing.js";
2
+ import { LIGHTPANDA_IMAGE_REASON } from "../../results/presentation/common.js";
2
3
  import { getScreenshotPathTokenIndex, getScreenshotPositionalIndices } from "./artifact-paths.js";
3
4
  import { runSessionCommandData } from "./session-state.js";
4
5
  export function getScreenshotCapture(command) {
@@ -28,7 +29,8 @@ export async function collectScreenshotSample(options) {
28
29
  element = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
29
30
  }
30
31
  } catch {}
31
- return { url: location.href, frame: window === window.top ? "main" : "child", childFrameCount: window.frames.length,
32
+ const textRendered = navigator.userAgentData?.brands?.some(b => b.brand === "Lightpanda") || /\\bLightpanda\\//i.test(navigator.userAgent);
33
+ return { ...(textRendered ? { rendering: "text" } : {}), url: location.href, frame: window === window.top ? "main" : "child", childFrameCount: window.frames.length,
32
34
  viewport: { width: innerWidth, height: innerHeight },
33
35
  document: { width: Math.max(root.scrollWidth, body?.scrollWidth || 0, innerWidth), height: Math.max(root.scrollHeight, body?.scrollHeight || 0, innerHeight) },
34
36
  scroll: { x: scrollX, y: scrollY }, dpr: devicePixelRatio,
@@ -37,7 +39,7 @@ export async function collectScreenshotSample(options) {
37
39
  try {
38
40
  const data = await runSessionCommandData({ ...options, args: ["eval", "--stdin"], stdin: script, timeoutMs: 2_000 });
39
41
  const sample = isRecord(data) ? data.result : undefined;
40
- if (!isRecord(sample) || typeof sample.url !== "string" || !["main", "child"].includes(String(sample.frame))
42
+ if (!isRecord(sample) || (sample.rendering !== undefined && sample.rendering !== "text") || typeof sample.url !== "string" || !["main", "child"].includes(String(sample.frame))
41
43
  || !finiteRecord(sample.viewport, ["width", "height"]) || !finiteRecord(sample.document, ["width", "height"])
42
44
  || !finiteRecord(sample.scroll, ["x", "y"]) || !finiteRecord(sample.visualViewport, ["x", "y", "scale"])
43
45
  || typeof sample.childFrameCount !== "number" || !Number.isInteger(sample.childFrameCount) || sample.childFrameCount < 0
@@ -53,6 +55,8 @@ export async function collectScreenshotSample(options) {
53
55
  export function buildScreenshotGeometry(options) {
54
56
  const { before, after, capture, pixels } = options;
55
57
  const unknown = (reason) => ({ status: "unknown", reason, before, after });
58
+ if (options.rendering === "text" || before?.rendering === "text" || after?.rendering === "text")
59
+ return unknown(LIGHTPANDA_IMAGE_REASON);
56
60
  if (!before || !after)
57
61
  return unknown("Capture was not bracketed by browser geometry samples; coordinates are unknown.");
58
62
  if (JSON.stringify(before) !== JSON.stringify(after))
@@ -0,0 +1,86 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../argv-grammar.js";
3
+ // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
4
+ export class AsyncExecutionQueue {
5
+ tail = Promise.resolve();
6
+ active = new AsyncLocalStorage();
7
+ isCurrent() { return this.active.getStore()?.active === true; }
8
+ run(work, signal, barrier = Promise.resolve()) {
9
+ if (this.isCurrent()) {
10
+ signal?.throwIfAborted();
11
+ return work();
12
+ }
13
+ const previous = this.tail;
14
+ let release;
15
+ this.tail = new Promise((resolve) => {
16
+ release = resolve;
17
+ });
18
+ let rejectWaiting;
19
+ const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
20
+ const abortWaiting = () => rejectWaiting(signal?.reason);
21
+ signal?.addEventListener("abort", abortWaiting, { once: true });
22
+ if (signal?.aborted)
23
+ abortWaiting();
24
+ const execution = (async () => {
25
+ await Promise.all([previous, barrier]);
26
+ signal?.removeEventListener("abort", abortWaiting);
27
+ try {
28
+ signal?.throwIfAborted();
29
+ const scope = { active: true };
30
+ try {
31
+ return await this.active.run(scope, work);
32
+ }
33
+ finally {
34
+ scope.active = false;
35
+ }
36
+ }
37
+ finally {
38
+ release();
39
+ }
40
+ })();
41
+ return signal ? Promise.race([execution, cancelled]) : execution;
42
+ }
43
+ }
44
+ export class KeyedAsyncExecutionQueue {
45
+ barriers = new Map();
46
+ entries = new Map();
47
+ async run(key, namespace, work, signal) {
48
+ const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
49
+ if (entry.queue.isCurrent())
50
+ return work();
51
+ const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
52
+ entry.users += 1;
53
+ this.entries.set(key, entry);
54
+ try {
55
+ return await entry.queue.run(work, signal, barrier);
56
+ }
57
+ finally {
58
+ entry.users -= 1;
59
+ if (entry.users === 0 && this.entries.get(key) === entry)
60
+ this.entries.delete(key);
61
+ }
62
+ }
63
+ async runExclusive(namespace, work) {
64
+ const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
65
+ const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
66
+ let release;
67
+ const blocked = new Promise((resolve) => {
68
+ release = resolve;
69
+ });
70
+ const barrier = previous.then(() => blocked);
71
+ this.barriers.set(namespaceKey, barrier);
72
+ const drains = [...this.entries]
73
+ .filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
74
+ .map(([, { queue }]) => queue.run(async () => undefined));
75
+ await previous;
76
+ await Promise.all(drains);
77
+ try {
78
+ return await work();
79
+ }
80
+ finally {
81
+ release();
82
+ if (this.barriers.get(namespaceKey) === barrier)
83
+ this.barriers.delete(namespaceKey);
84
+ }
85
+ }
86
+ }
@@ -100,11 +100,12 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
100
100
  const configuredSession = env.AGENT_BROWSER_SESSION ?? identity.session;
101
101
  const launchArgs = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--args").at(-1)?.value ?? env.AGENT_BROWSER_ARGS ?? identity.args;
102
102
  const engine = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--engine").at(-1)?.value ?? env.AGENT_BROWSER_ENGINE ?? identity.engine;
103
+ const chromeEngine = engine === undefined || engine === "chrome";
103
104
  const provider = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--provider").at(-1)?.value ?? scanUpstreamGlobalFlagOccurrences(input.toolArgs, "-p").at(-1)?.value ?? env.AGENT_BROWSER_PROVIDER ?? identity.provider;
104
105
  const tokens = parseArgvDescriptor(input.toolArgs).upstreamCommandTokens;
105
106
  const batchAttaches = requestsConnection(tokens, input.toolStdin);
106
107
  const localChrome = browserCommand && !attachment && !batchAttaches && provider === undefined
107
- && (engine === undefined || engine === "chrome");
108
+ && chromeEngine;
108
109
  const chromeStartupArgs = localChrome ? ["--no-startup-window", ...(typeof launchArgs === "string" ? [launchArgs] : [])].join(",") : undefined;
109
110
  const persistentChromeArgs = hasLocalLaunchDefaults(identity, env) ? chromeStartupArgs : undefined;
110
111
  const configuredChromeLaunch = persistentChromeArgs !== undefined || [...LOCAL_VALUE_DEFAULTS, ...LOCAL_BOOLEAN_DEFAULTS, ...LOCAL_ARRAY_DEFAULTS, "hideScrollbars", "clearCaCert"].some(key => {
@@ -147,9 +148,9 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
147
148
  if (restoreEligible && bootstrap)
148
149
  restore = rootDefault;
149
150
  const profile = scanUpstreamGlobalFlagOccurrences(args, "--profile").at(-1)?.value
150
- ?? (bootstrap && restoreEligible && identity.profile === undefined && env.AGENT_BROWSER_PROFILE === undefined ? root?.profile : undefined);
151
+ ?? (bootstrap && chromeEngine && restoreEligible && identity.profile === undefined && env.AGENT_BROWSER_PROFILE === undefined ? root?.profile : undefined);
151
152
  const executablePath = scanUpstreamGlobalFlagOccurrences(args, "--executable-path").at(-1)?.value
152
- ?? (bootstrap && identity.executablePath === undefined && env.AGENT_BROWSER_EXECUTABLE_PATH === undefined ? root?.executablePath : undefined);
153
+ ?? (bootstrap && chromeEngine && identity.executablePath === undefined && env.AGENT_BROWSER_EXECUTABLE_PATH === undefined ? root?.executablePath : undefined);
153
154
  return withAgentBrowserProcessEnvironment({
154
155
  ...(restore !== undefined ? { AGENT_BROWSER_RESTORE: restore } : {}),
155
156
  ...(profile !== undefined ? { AGENT_BROWSER_PROFILE: profile } : {}),
@@ -157,7 +157,7 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
157
157
  }
158
158
  return text;
159
159
  }
160
- export function formatAgentBrowserRenderResult(result, options, theme, isError) {
160
+ function formatAgentBrowserRenderResult(result, options, theme, isError) {
161
161
  if (options.isPartial) {
162
162
  return theme.fg("warning", "Running agent-browser...");
163
163
  }
@@ -14,7 +14,7 @@ export const QUICK_START_GUIDELINES = [
14
14
  "Example agent_browser_code: { code: 'const result = await browser({args:[\"get\",\"title\"]}); if (!result.success) { emit(result); } else { emit({title:result.data}); }' }. Native batch still avoids extra CLI processes for fixed segments inside code. Explicit session/namespace controls select the browser for this cell without changing the default browser.",
15
15
  "Call agent_browser_tools with no fields for inventory, or { enable: [\"action\", \"qa\", \"electron\", \"source\", \"network\"] } for advanced native tools. It adds agent_browser_action, agent_browser_qa, agent_browser_electron, agent_browser_source, and agent_browser_network_source without disabling other tools. The browser instructions cover these capabilities; activation reveals their selected tool declarations.",
16
16
  "Use read <url> for readable web text; get title/url, get text/html/value/count <selector>, or get attr <selector> <name> for targeted state. For eval --stdin, put JavaScript in stdin and return the desired value. outputPath saves result data and must differ from screenshot/download/recording paths. Use --json only when JSON text is needed.",
17
- "For artifacts, verify artifactVerification and artifacts before reporting success. A pending recording is not verified output; install ffmpeg before starting and pair start with stop. Native download owns the click and saves Blob/redirect downloads. Close keeps explicitly saved files. Save promptGuard-required paths before close. Screenshot pixels are not browser CSS coordinates: use imageObservations geometry (dimensions, viewport, crop/origin, scroll and DPR) before any visual-coordinate action; full-page and element captures are not current-viewport coordinates, and Pi resizing only maps sent pixels to original image pixels.",
17
+ "For artifacts, verify artifactVerification and artifacts before reporting success. A pending recording is not verified output; install ffmpeg before starting and pair start with stop. Native download owns the click and saves Blob/redirect downloads. Close keeps explicitly saved files. Save promptGuard-required paths before close. Screenshot pixels are not browser CSS coordinates: use imageObservations geometry (dimensions, viewport, crop/origin, scroll and DPR) before any visual-coordinate action; full-page and element captures are not current-viewport coordinates, and Pi resizing only maps sent pixels to original image pixels. Lightpanda images are text-rendered, not graphical layout; rendering:text never supports CSS/mouse-coordinate input.",
18
18
  "Follow model-visible nextActions when present, including identity and stdin, instead of guessing commands. Treat native pending confirmations and unknown mutation outcomes as inspection boundaries. For dense snapshots, inspect Omitted high-value controls before reading a full spill.",
19
19
  ];
20
20
  export const ADVANCED_TOOL_PROMPT_GUIDELINES = {
@@ -61,6 +61,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
61
61
  "Cooperating Pi processes serialize each complete browser operation and code cell by native socket context, namespace, and session, including target/ref helpers and state updates. Different identities remain concurrent; namespace-wide close waits for matching operations. All participating Pi instances must load this release; humans, third-party clients, and older extensions are outside that coordination. It is not a transaction or rollback. For raw batches whose later content depends on navigation, use exact batch --bail or split the calls.",
62
62
  "After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume and live-checks the URL before later page reads/interactions because an attached browser can drift externally; caller config, file access, launch arguments, and environment pass through unchanged. A successful close clears the marker. When several named sessions share one Chrome, pass --pin-tab once (AGENT_BROWSER_PIN_TAB) so a closed bound tab fails as tab_gone instead of acting on a neighbor; recover with tab new or tab list. --no-pin-tab turns the sticky pin off. tab list includes each tab's CDP targetId, accepted as a tab ref.",
63
63
  `If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
64
+ "Chrome is the default. Use separately installed Lightpanda only when explicitly selected with --engine lightpanda, environment or native config; --engine is launch-scoped, so use sessionMode:fresh to change a managed browser. Lightpanda text-rendered screenshots do not prove graphical layout/visibility or mouse coordinates. Chrome profiles, headed mode, multi-tab, WebGPU and native WebMCP are not parity capabilities; choose Chrome explicitly when needed, never assume automatic fallback.",
64
65
  "For WebGPU pages, use args [\"--webgpu\", \"open\", \"<url>\"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.",
65
66
  "For experimental WebMCP page tools, use webmcp list, then webmcp invoke <tool> with --params and optional --frame/--detach/--timeout; use webmcp result or cancel for detached calls. Locally managed Chrome enables WebMCP by default; a positive navigation hint means the page has tools to list. --no-webmcp is launch-scoped and requires a fresh session; invoke/result/cancel can mutate or navigate, so refresh snapshot refs afterward.",
66
67
  "For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.",
@@ -110,20 +111,20 @@ export const RUNTIME_PROMPT_GUIDELINES = [
110
111
  "For agent_browser, use open → snapshot -i → current @refs; re-snapshot after changes. Batch same-snapshot fills before submit; split on rerenders/navigation. Native args, profiles, auth, providers, paths, config and environment pass through. Use requested/configured profiles and verify authentication after restart.",
111
112
  "agent_browser bare calls share a root Pi browser with descendants, not unrelated roots; coordinate navigation within the group. Native explicit sessions win. sessionMode:fresh intentionally launches a separate managed browser. Never close an unrelated browser.",
112
113
  "For agent_browser and agent_browser_code, follow visible nextActions. Inspect unknown mutation outcomes before retry. Use exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; close keeps files; waited:timeout proves nothing.",
113
- "For agent_browser screenshot coordinates, use imageObservations dimensions/viewport/crop/origin/scroll/DPR; full-page and element images are not viewport CSS coordinates. Pi image resizing maps sent pixels to original image pixels only. Prefer refs; inspect screenshots only when the task needs vision.",
114
+ "For agent_browser screenshot coordinates, use imageObservations dimensions/viewport/crop/origin/scroll/DPR; full-page and element images are not viewport CSS coordinates. Pi image resizing maps sent pixels to original image pixels only. Lightpanda rendering:text images never establish CSS/mouse coordinates. Prefer refs; inspect screenshots only when the task needs vision.",
114
115
  "In agent_browser workflows, ordinary requested non-destructive submissions may proceed in authenticated unattended/auto-approved flows. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
115
116
  "agent_browser: read <url> for text, get title/url, get text/html/value/count <selector>, get attr <selector> <name>; heed visibility warnings and Omitted high-value controls. Verify nested scrolling via snapshot/screenshot. Use --json only for JSON text; recording needs ffmpeg before start and a verified stop.",
116
117
  ];
117
118
  export function buildBrowserExecutablePathGuideline(executablePath) {
118
119
  if (!executablePath)
119
120
  return undefined;
120
- return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}. Global/override values bootstrap inactive automatic root browsers; bare calls preserve active native launch settings. Project-only values remain guidance for explicit --executable-path launches. Use sessionMode:fresh only for an intentionally separate managed browser, not routine root-group browsing. Upstream profiles lists Chrome profiles only; use a supported explicit profile path for other Chromium browsers.`;
121
+ return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}. Global/override values bootstrap inactive automatic root Chrome browsers, not Lightpanda; bare calls preserve active native launch settings. Project-only values remain guidance for explicit --executable-path launches. Use sessionMode:fresh only for an intentionally separate managed browser, not routine root-group browsing. Upstream profiles lists Chrome profiles only; use a supported explicit profile path for other Chromium browsers.`;
121
122
  }
122
123
  export function buildBrowserDefaultProfileGuideline(profile) {
123
124
  if (!profile || profile.policy === "explicit-only")
124
125
  return undefined;
125
126
  if (profile.policy === "always") {
126
- return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always. Global/override Chrome profile names bootstrap automatic root browsers through independent native copies with per-root restore; bare calls and helpers preserve the active native launch settings without resending bootstrap options. Paths and project-only settings stay advisory. Explicit unrelated sessions are unchanged. Verify actual app auth after restart: native JSON restore omits new IndexedDB and memory-only credentials; use a supported per-root persistent profile if needed. Profile content is model-visible user data.`;
127
+ return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always. Global/override Chrome profile names bootstrap automatic root Chrome browsers, not Lightpanda, through independent native copies with per-root restore; bare calls and helpers preserve the active native launch settings without resending bootstrap options. Paths and project-only settings stay advisory. Explicit unrelated sessions are unchanged. Verify actual app auth after restart: native JSON restore omits new IndexedDB and memory-only credentials; use a supported per-root persistent profile if needed. Profile content is model-visible user data.`;
127
128
  }
128
129
  return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)}; for signed-in/account-specific browser tasks, reuse a configured native shared session with bare calls, otherwise start with --profile ${JSON.stringify(profile.name)} plus sessionMode:fresh unless the user asks for a different profile.`;
129
130
  }
@@ -290,9 +290,6 @@ export async function getAgentBrowserSocketDirValidationError(socketDir, uid = t
290
290
  return `the directory could not be inspected (${error.code ?? "unknown error"})`;
291
291
  }
292
292
  }
293
- export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
294
- return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
295
- }
296
293
  export function getAgentBrowserSocketPathValidationError(options) {
297
294
  if ((options.platform ?? processPlatform) === "win32")
298
295
  return undefined;
@@ -7,8 +7,8 @@ export function isPendingRecordingArtifact(artifact) {
7
7
  return artifact.recordingState === "openRecording" || artifact.status === "pending"
8
8
  || (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
9
9
  }
10
- export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
- export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
10
+ const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
+ const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
12
12
  export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
13
13
  function parsePositiveSafeInteger(value) {
14
14
  if (value === undefined)
@@ -31,7 +31,7 @@ function getRouteDiagnosticReason(item, route) {
31
31
  return "unfulfilled-routed-request";
32
32
  return undefined;
33
33
  }
34
- export function getNetworkRouteMode(args) {
34
+ function getNetworkRouteMode(args) {
35
35
  if (args.includes("--abort"))
36
36
  return "abort";
37
37
  if (args.includes("--body"))
@@ -3,7 +3,7 @@ export function getStringRecordField(value, key) {
3
3
  const field = value[key];
4
4
  return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
5
5
  }
6
- export function getNetworkRequestUrlPath(url) {
6
+ function getNetworkRequestUrlPath(url) {
7
7
  if (!url)
8
8
  return undefined;
9
9
  try {
@@ -6,6 +6,7 @@ import { getExplicitArtifactDestination } from "../../orchestration/browser-run/
6
6
  import { isRecord, parsePositiveInteger } from "../../parsing.js";
7
7
  import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
8
8
  import { classifyAgentBrowserSuccessCategory } from "../categories.js";
9
+ import { LIGHTPANDA_IMAGE_REASON } from "./common.js";
9
10
  import { formatRecordingReceipt, getRecordingReceipt } from "../recording.js";
10
11
  const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
11
12
  const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
@@ -587,11 +588,16 @@ export async function attachInlineImage(presentation, imagePath, modelVisible =
587
588
  return presentation;
588
589
  const dimensions = getImageDimensions(header.toString("base64"), mimeType);
589
590
  presentation.imagePath = imagePath;
591
+ const lifecycle = isRecord(presentation.data) && isRecord(presentation.data.lifecycle) ? presentation.data.lifecycle : undefined;
592
+ const textRendered = isRecord(lifecycle?.effectiveLaunch) && lifecycle.effectiveLaunch.engine === "lightpanda";
590
593
  presentation.imageObservations = [{ path: imagePath, mimeType,
594
+ ...(textRendered ? { rendering: "text" } : {}),
591
595
  pixels: dimensions ? { width: dimensions.widthPx, height: dimensions.heightPx } : undefined,
592
- capture: "unknown", geometry: { status: "unknown", reason: dimensions ? "Capture geometry was not observed." : "Image dimensions could not be read from the bounded header." } }];
596
+ capture: "unknown", geometry: { status: "unknown", reason: textRendered ? LIGHTPANDA_IMAGE_REASON : dimensions ? "Capture geometry was not observed." : "Image dimensions could not be read from the bounded header." } }];
593
597
  if (!modelVisible)
594
598
  return presentation;
599
+ if (textRendered)
600
+ appendPresentationNotice(presentation, LIGHTPANDA_IMAGE_REASON);
595
601
  const inlineImageMaxBytes = getInlineImageMaxBytes();
596
602
  if (fileStats.size > inlineImageMaxBytes) {
597
603
  appendPresentationNotice(presentation, `Image attachment skipped: ${formatByteCount(fileStats.size)} exceeds the inline limit of ${formatByteCount(inlineImageMaxBytes)}.`);
@@ -68,7 +68,7 @@ function getStatefulCommandSensitiveValues(command) {
68
68
  return [];
69
69
  const tokens = extractUpstreamCommandTokens(command);
70
70
  const values = [];
71
- if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
71
+ if (tokens[0] === "cookies" && tokens[1] === "set" && !tokens.slice(2).includes("--curl") && tokens[3])
72
72
  values.push(tokens[3]);
73
73
  if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4])
74
74
  values.push(tokens[4]);
@@ -2,6 +2,7 @@ import { isRecord } from "../../parsing.js";
2
2
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { stringifyUnknown, truncateText } from "../text.js";
4
4
  const UNTITLED_PAGE_SUMMARY = "(untitled page)";
5
+ export const LIGHTPANDA_IMAGE_REASON = "Lightpanda screenshot is a text-rendered page representation, not a graphical capture; CSS/mouse coordinate mapping is unknown.";
5
6
  export function stringifyModelFacing(value) {
6
7
  return stringifyUnknown(redactSensitiveValue(value));
7
8
  }
@@ -507,7 +507,7 @@ export function formatNetworkRouteDiagnosticsText(diagnostics) {
507
507
  lines.push("If this route is intended as a mock, inspect the request/headers and treat failed, pending, or CORS-looking rows as unfulfilled until a mocked response is observed.");
508
508
  return lines.join("\n");
509
509
  }
510
- export function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
510
+ function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
511
511
  const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
512
512
  if (!diagnostic)
513
513
  return undefined;
@@ -19,7 +19,7 @@ function formatSemanticActionTarget(compiled) {
19
19
  const target = `${locator} ${quotedValue}`;
20
20
  return name ? `${target} (name ${JSON.stringify(name)})` : target;
21
21
  }
22
- export function formatSemanticActionCompactLine(compiled) {
22
+ function formatSemanticActionCompactLine(compiled) {
23
23
  const target = formatSemanticActionTarget(compiled);
24
24
  switch (compiled.action) {
25
25
  case "click":
@@ -34,7 +34,7 @@ export function formatSemanticActionCompactLine(compiled) {
34
34
  return `${compiled.action}: ${target}`;
35
35
  }
36
36
  }
37
- export function resolveSemanticPresentationCommand(compiled) {
37
+ function resolveSemanticPresentationCommand(compiled) {
38
38
  if (!compiled || !SEMANTIC_PRESENTATION_ACTIONS.has(compiled.action))
39
39
  return undefined;
40
40
  if (compiled.action === "select")
@@ -256,6 +256,6 @@ export function formatRichInputRecoveryText(diagnostic) {
256
256
  `- ${diagnostic.inputMethodHint}`,
257
257
  ].join("\n");
258
258
  }
259
- export function normalizeSemanticActionAccessibleName(name) {
259
+ function normalizeSemanticActionAccessibleName(name) {
260
260
  return name.replace(/\s+/g, " ").trim().toLowerCase();
261
261
  }
@@ -254,7 +254,7 @@ function redactBearerCredentials(text) {
254
254
  return formatRedactedCredential(label, credential, trailing);
255
255
  });
256
256
  }
257
- export function isSensitiveFieldName(key) {
257
+ function isSensitiveFieldName(key) {
258
258
  SENSITIVE_FIELD_NAME_PATTERN.lastIndex = 0;
259
259
  return SENSITIVE_FIELD_NAME_PATTERN.test(key);
260
260
  }
@@ -347,7 +347,9 @@ export function redactInvocationArgs(args) {
347
347
  }
348
348
  }
349
349
  }
350
- if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set" && redacted[commandStartIndex + 3] !== undefined) {
350
+ if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set"
351
+ && !extractUpstreamCommandTokens(args).slice(2).includes("--curl")
352
+ && redacted[commandStartIndex + 3] !== undefined) {
351
353
  redacted[commandStartIndex + 3] = "[REDACTED]";
352
354
  }
353
355
  if (commandStartIndex !== undefined
@@ -62,7 +62,7 @@ function extractStringResultField(data, fieldName) {
62
62
  const text = data[fieldName].trim();
63
63
  return text.length > 0 ? text : undefined;
64
64
  }
65
- export function extractSessionTabTargetFromData(data) {
65
+ function extractSessionTabTargetFromData(data) {
66
66
  const directTarget = normalizeSessionTabTarget({
67
67
  targetId: isRecord(data) && typeof data.targetId === "string" ? data.targetId : undefined,
68
68
  title: extractStringResultField(data, "title"),
@@ -5,20 +5,20 @@ import { redactSensitiveText } from "./runtime.js";
5
5
  import { StringEnum as localStringEnum } from "./string-enum-schema.js";
6
6
  import { DEFAULT_WEB_SEARCH_PROVIDER, EXA_SEARCH_TYPES, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
7
7
  export const AGENT_BROWSER_WEB_SEARCH_TOOL_NAME = "agent_browser_web_search";
8
- export const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
9
- export const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
10
- export const DEFAULT_SEARCH_RESULT_COUNT = 5;
11
- export const MAX_SEARCH_RESULT_COUNT = 10;
12
- export const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
13
- export const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
14
- export const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
15
- export const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
16
- export const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
8
+ const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
9
+ const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
10
+ const DEFAULT_SEARCH_RESULT_COUNT = 5;
11
+ const MAX_SEARCH_RESULT_COUNT = 10;
12
+ const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
13
+ const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
14
+ const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
15
+ const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
16
+ const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
17
17
  export const WEB_SEARCH_MIN_REQUEST_INTERVAL_MS = 1_100;
18
18
  export const EXA_SEARCH_SYSTEM_PROMPT = "Prefer primary, official sources. Respect any requested version or date. Avoid duplicate or equivalent results.";
19
19
  export { EXA_SEARCH_TYPES };
20
- export const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
21
- export const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
20
+ const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
21
+ const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
22
22
  const MAX_EXA_DOMAIN_FILTERS = 20;
23
23
  const MAX_EXA_ADDITIONAL_QUERIES = 10;
24
24
  export function createAgentBrowserWebSearchParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
@@ -193,7 +193,7 @@ export function cleanSearchText(value, maxLength = 500) {
193
193
  return cleaned;
194
194
  return `${cleaned.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
195
195
  }
196
- export function normalizeSearchUrl(value) {
196
+ function normalizeSearchUrl(value) {
197
197
  if (typeof value !== "string")
198
198
  return undefined;
199
199
  try {
@@ -258,7 +258,7 @@ export function normalizeExaSearchResult(result) {
258
258
  function getProviderLabel(provider) {
259
259
  return provider === "exa" ? "Exa" : "Brave";
260
260
  }
261
- export function formatSearchResults(provider, query, results) {
261
+ function formatSearchResults(provider, query, results) {
262
262
  const providerLabel = getProviderLabel(provider);
263
263
  if (results.length === 0) {
264
264
  return `No ${providerLabel} web results found for: ${query}`;
@@ -538,14 +538,14 @@ const EXA_WEB_SEARCH_ADAPTER = {
538
538
  };
539
539
  },
540
540
  };
541
- export const WEB_SEARCH_PROVIDER_ADAPTERS = {
541
+ const WEB_SEARCH_PROVIDER_ADAPTERS = {
542
542
  exa: EXA_WEB_SEARCH_ADAPTER,
543
543
  brave: BRAVE_WEB_SEARCH_ADAPTER,
544
544
  };
545
545
  export function getWebSearchProviderAdapter(provider) {
546
546
  return WEB_SEARCH_PROVIDER_ADAPTERS[provider];
547
547
  }
548
- export function dedupeSearchResults(results) {
548
+ function dedupeSearchResults(results) {
549
549
  const seen = new Set();
550
550
  return results.filter((result) => {
551
551
  if (seen.has(result.url))
@@ -46,15 +46,15 @@ The extension should:
46
46
 
47
47
  The child has an empty environment, Node permission mode without host capability allowlists, a 64 MiB heap ceiling, and a VM with string/WebAssembly code generation disabled. Null-prototype `browser`, `emit`, and `emitImage` functions use JSON-only IPC; parent promises/objects/functions are not exposed. There are no imports or host filesystem/network/process/timer APIs. Node's VM alone is not a security boundary. The retained child isolation is distinct from the selected browser's persistent authentication and native capabilities; no QuickJS, broker, second driver, or provider payload injection is involved.
48
48
 
49
- Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
49
+ Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `lib/input-modes/types.ts` owns the maximum shared by the public code schema and runner. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
50
50
 
51
51
  ### Ordered transitions and shared execution
52
52
 
53
- `lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier. Existing local queues and branch-generation checks still protect in-process ownership/replay. Independent identities remain concurrent. Direct calls bound coordination waits using the existing configured/command-aware timeout, then retain their native subprocess and Electron action watchdogs. Code owns a whole-cell deadline, including local queue waits. Independently bounded cleanup can borrow an already-held covered claim after cancellation, but cannot acquire a new identity or release the outer claim.
53
+ `lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier. `lib/orchestration/execution-queue.ts` owns the existing local managed/artifact queues and keyed namespace barriers; branch-generation checks still protect in-process ownership/replay. Independent identities remain concurrent. Direct calls bound coordination waits using the existing configured/command-aware timeout, then retain their native subprocess and Electron action watchdogs. Code owns a whole-cell deadline, including local queue waits. Independently bounded cleanup can borrow an already-held covered claim after cancellation, but cannot acquire a new identity or release the outer claim.
54
54
 
55
55
  All participating Pi processes must restart with the updated package. The lock is not a browser transaction, rollback, or a guarantee against humans, third-party native clients, older extensions, or separately named sessions attached to one external Chrome. Separate tool calls still need workflow coordination.
56
56
 
57
- `lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
57
+ `lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. `lib/orchestration/browser-run/artifact-merge.ts` preserves concurrent three-way manifest updates and recording lifecycle order. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
58
58
 
59
59
  `lib/browser-journal.ts` token-projects published envelopes and ancestry through public Pi file/header/leaf APIs, loading only winning snapshot definitions. It captures the selected leaf before asynchronous reads and validates that same branch through public `getEntry()` ancestry on both hosts. Capture-local admission reuses a validated append-only prefix while checking endpoint identity/parent and new suffixes; publication/durability checks remain independent. Startup/tree restoration uses one queued captured replay for branch state and cleanup leases, with one per-session launch-configuration fold. Stock official 1.0.0 still eagerly loads native history before extension startup. Legacy adapters live only in `lib/browser-session-conversion.ts`, which publishes a validated stopped separate copy while retaining the source. Native generation checks use PID/start and socket identity, never saved URL or delta revision alone. Filtered/helper captures share this check. Small owner-qualified launch receipts retain observed restore-key provenance; restart verifies the current native generation and key before treating that provenance as live. Full storage, migration and consumer-memory boundaries are documented in [`SESSION_STORAGE.md`](SESSION_STORAGE.md).
60
60
 
@@ -64,7 +64,7 @@ All participating Pi processes must restart with the updated package. The lock i
64
64
 
65
65
  Native tool declarations share Browser namespace metadata and `outputSchema`; `structuredContent` reuses the bounded redacted observation or its existing compact/spill object. Loader inventory and normalized provider search keep their existing shapes. Internal ownership/replay details are not exported wholesale. This interoperates with host-selected native codemode without auto-enabling it or replacing the Browser VM. A path/handle is not native image data; verified content image blocks remain separate. Final results return native `isError` and retain prose failure notices while explicit JSON stays parseable and unchanged. Per-row TUI memoization skips unchanged formatting/Text updates without caching browser state or observations across calls.
66
66
 
67
- `orchestration/browser-run/screenshot-observation.ts` brackets standalone screenshots with CSS viewport/document/scroll/DPR/frame samples. Existing Pi image-dimension primitives measure saved pixels. Matching samples and dimensions can establish a crop mapping; changed, unmeasured batch, scrolled/ambiguous element, zoom, or frame-uncertain captures remain unknown. These samples are not an atomic capture guarantee. Native mouse input uses current viewport CSS pixels, not image pixels; full-page/element origin and any Pi resize must be accounted for separately. Unchanged conditional screenshots attach nothing. See [image observations](TOOL_CONTRACT.md#image-observations).
67
+ `orchestration/browser-run/screenshot-observation.ts` brackets standalone screenshots with CSS viewport/document/scroll/DPR/frame samples. Existing Pi image-dimension primitives measure saved pixels. Matching samples and dimensions can establish a crop mapping; changed, unmeasured batch, scrolled/ambiguous element, zoom, or frame-uncertain captures remain unknown. These samples are not an atomic capture guarantee. Native mouse input uses current viewport CSS pixels, not image pixels; full-page/element origin and any Pi resize must be accounted for separately. Unchanged conditional screenshots attach nothing. Native Lightpanda engine metadata labels verified images as `rendering: "text"` through shared attachment handling, including batch/code. The existing standalone samples also recognize the Lightpanda browser brand without another probe or engine cache. Text-rendered images retain unknown geometry and no crop/scale mapping, even when synthetic dimensions match. See [image observations](TOOL_CONTRACT.md#image-observations).
68
68
 
69
69
  ### Agent-first UX
70
70
 
@@ -89,7 +89,7 @@ Config layers merge in that order: global, project, override. The shared policy
89
89
 
90
90
  `agent_browser_web_search` availability is conditional. Startup registration uses global, override, and environment fallback config without reading project-local config before Pi trust context exists; trusted project config can register the companion tool on `session_start`, and every execution reloads the final session config so `webSearch.enabled: false` still prevents a request even if a startup credential made the tool visible. A global disable is the normal user default and can still be overridden by project config or `PI_AGENT_BROWSER_CONFIG`; a project disable applies to one repo; an explicit `PI_AGENT_BROWSER_CONFIG` file with `webSearch.enabled: false` is the highest-priority hard disable for that run. Literal and env-backed sources must resolve before they make the tool available; command-backed sources are considered configured without running the command until tool execution, so secret managers do not slow startup or prompt unexpectedly. The tool resolves the selected key lazily, chooses Exa or Brave from available credentials (preferring Exa by default unless `webSearch.preferredProvider` says otherwise), then follows one provider-agnostic execution path through provider adapters for request building, HTTP JSON fetch, response normalization, and provider-specific detail fields. Exa search-type precedence is per-call value, merged config default, then `auto`; bounded Exa filters stay on the Exa adapter and explicit Exa-only options fail before a Brave request. The tool calls Exa `/search` with highlights plus a fixed primary-source/version/date/distinct-result system prompt, or Brave Search, then removes later exact normalized-URL duplicates while retaining provider order. It does not guess across distinct paths/query URLs or overfetch replacements; provider page dates remain clues rather than claimed crawl/version evidence. Compact result details report any `duplicatesRemoved` without exposing keys.
91
91
 
92
- Global/override package profile names with `policy: "always"` and executable defaults bootstrap inactive automatic root browsers through scoped native environment. Active roots retain native settings; paths, other policies, and project-only values remain advisory. Direct and code calls selecting that root share the same configuration. Unrelated explicit sessions and Electron launches retain their separate native/owned rules.
92
+ Global/override package profile names with `policy: "always"` and executable defaults bootstrap inactive automatic root browsers through scoped native environment only when CLI → environment → native config resolves to Chrome. Lightpanda receives no wrapper Chrome defaults; explicit native settings are untouched. Active roots retain native settings; paths, other policies, and project-only values remain advisory. Direct and code calls selecting that root share the same configuration. Unrelated explicit sessions and Electron launches retain their separate native/owned rules.
93
93
 
94
94
  ### Prompt guidance budget
95
95
 
@@ -138,7 +138,7 @@ The optional `pi-change-working-dir` event protocol supplies one invocation exec
138
138
 
139
139
  An explicit native `session` default (user/project JSON or `AGENT_BROWSER_SESSION`) selects a caller-owned browser across Pi sessions. Per-call session/namespace flags win. `lib/orchestration/native-session-defaults.ts` reads session/namespace and contributing launch defaults from native config paths and uses sessionless native `--config <path> --json session` to validate each contributing file and resolve its session. Native config continues to own launch/profile/storage settings; the adapter does not duplicate its schema. Explicit config and per-call idle settings are scoped to helpers through `lib/process-environment.ts`. No startup probe, persistent config cache, or new lifecycle service is added.
140
140
 
141
- When no native session, attachment, or explicit fresh launch is selected, `sessionMode: "auto"` uses `pi-root-<sha256(root Pi ID)[0:24]>` through the existing caller-owned named-session path. Descendants receive `PI_SUBAGENT_ROOT_SESSION_ID` from pi-subagents; root processes use `ctx.sessionManager.getSessionId()`. No cwd, human fork ancestry, run registry, or process-global assignment participates. The same name is the default native restore key. Existing native daemon inspection runs inside the existing identity queue: inactive roots receive bootstrap defaults, while active roots retain native launch settings and their current restore key. Scoped subprocess environment keeps helper settings consistent; no launch-settings journal is added. Root-name explicit follow-ups retain those defaults; unrelated explicit names do not. Global/override package `browser.defaultProfile` with `policy: "always"` supplies native Chrome profile names only, plus the configured executable. Paths and project-only profile settings stay advisory. Native config/environment and explicit launch settings win. The communal source is copied by native Chrome launch, never by the wrapper, and each root's restore writes remain separate. Native JSON restore omits IndexedDB, service workers, and page-memory credentials; application-level restart verification is required. Parent and child quit leave the root browser alive; shared execution locks coordinate updated cooperating processes without a broker. An active older/fresh managed session keeps its existing lifecycle. A configured session has the same precedence over `fresh` as a literal `--session`; code follows the selected ordinary browser lifecycle; Electron launch retains its owned lifecycle. Electron launch suppresses the native session environment default only while allocating its generated session; ordinary browser follow-ups still honor the configured native default, so use the returned Electron session explicitly.
141
+ When no native session, attachment, or explicit fresh launch is selected, `sessionMode: "auto"` uses `pi-root-<sha256(root Pi ID)[0:24]>` through the existing caller-owned named-session path. Descendants receive `PI_SUBAGENT_ROOT_SESSION_ID` from pi-subagents; root processes use `ctx.sessionManager.getSessionId()`. No cwd, human fork ancestry, run registry, or process-global assignment participates. The same name is the default native restore key. Existing native daemon inspection runs inside the existing identity queue: inactive roots receive bootstrap defaults, while active roots retain native launch settings and their current restore key. Scoped subprocess environment keeps helper settings consistent; no launch-settings journal is added. Root-name explicit follow-ups retain those defaults; unrelated explicit names do not. Global/override package `browser.defaultProfile` with `policy: "always"` supplies native Chrome profile names only, plus the configured executable, only when the effective engine is Chrome. Paths and project-only profile settings stay advisory. Native config/environment and explicit launch settings win. The communal source is copied by native Chrome launch, never by the wrapper, and each root's restore writes remain separate. Native JSON restore omits IndexedDB, service workers, and page-memory credentials; application-level restart verification is required. Parent and child quit leave the root browser alive; shared execution locks coordinate updated cooperating processes without a broker. An active older/fresh managed session keeps its existing lifecycle. A configured session has the same precedence over `fresh` as a literal `--session`; code follows the selected ordinary browser lifecycle; Electron launch retains its owned lifecycle. Electron launch suppresses the native session environment default only while allocating its generated session; ordinary browser follow-ups still honor the configured native default, so use the returned Electron session explicitly.
142
142
 
143
143
  Why:
144
144
  - works out of the box
@@ -224,7 +224,7 @@ Input resolution replaces URL-less `open` with native `get url` before planning,
224
224
  `agent-browser` startup flags are sticky once a session is already running.
225
225
  The extension should surface that clearly and avoid hidden restart behavior.
226
226
 
227
- That means explicit startup-scoping flags like `--allowed-domains`, `--auto-connect`, `--args`, `--user-agent`, `--cdp`, `--enable`, `--executable-path`, `--webgpu`, `--no-webmcp`, `--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.
227
+ That means explicit startup-scoping flags like `--allowed-domains`, `--auto-connect`, `--args`, `--user-agent`, `--cdp`, `--enable`, `--engine`, `--executable-path`, `--webgpu`, `--no-webmcp`, `--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.
228
228
 
229
229
  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 on supported native versions that do not inherit per-page CDP overrides. 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.
230
230
 
@@ -237,6 +237,7 @@ Implementation detail lives in `extensions/agent-browser/lib/launch-scoped-flags
237
237
  - **Command discovery:** Leading argv is scanned with a value-taking allowlist so known global flags and documented command flags consume their values before the upstream command word is identified. Missing-value prevalidation is intentionally limited to upstream global value flags; command-scoped flags and literal text are left to upstream parsing so values like `fill #field --password` are not rejected by wrapper heuristics before the CLI sees them. Upstream 0.35.0 and newer accept `--restore=<key>` but reject other global `--flag=value` tokens during normal command execution, so the wrapper rejects those unsupported assignments before they can be mistaken for command or artifact operands. Plain help/version inspection preserves exact caller argv because upstream accepts those top-level shapes. Nested batch rows do not parse global flags, so their equals forms never receive the top-level inspection or restore exceptions. When upstream adds new global flags that take values ahead of the command, extend both the command-discovery and prevalidation allowlists; when it adds command-specific flags, extend only command discovery/redaction as needed. A smaller set of global boolean flags may be followed by an optional `true`/`false` literal; when present, that literal is consumed as the flag value before command discovery continues.
238
238
  - **`--state` disambiguation:** Persisted browser `--state` before the command participates in launch-scoped validation and tab-correction hints. The same flag spelling after a `wait` command is excluded from startup-scoped detection so upstream help examples such as `wait @ref --state hidden` do not spuriously require `sessionMode: "fresh"` while an implicit session is active. As of the current upstream baseline, the parser still does not implement those `wait --state` examples as distinct wait modes, so agent-facing docs recommend `wait --fn` predicates for disappearance checks instead.
239
239
  - **`--auto-connect`:** Treated as launch-scoped only when enabled (`--auto-connect` bare or `true`). `--auto-connect false` is ignored for startup-scoped blocking so disabled attach hints do not force a fresh launch.
240
+ - **`--engine`:** Treated as launch-scoped for Chrome and Lightpanda, using the same fresh-session recovery instead of letting native engine changes replace a running managed page. Caller-owned sessions remain native-owned. Chrome stays default; Lightpanda is opt-in with no bundle, second driver, emulation or automatic fallback. See [engine limits](COMMAND_REFERENCE.md#lightpanda-engine).
240
241
  - **`--webgpu`:** Treated as launch-scoped for both enabled and explicit `false` values. Enabled WebGPU selects upstream's platform-specific local-launch preset; explicit false can override an environment/config default and still belongs to a fresh browser launch. Upstream rejects enabled WebGPU with CDP, auto-connect, or provider launches.
241
242
  - **`--no-webmcp`:** Treated as launch-scoped for bare/`true` and explicit `false` values because it selects whether upstream 0.36.0 enables experimental WebMCP for a locally managed Chrome launch.
242
243
  - **`--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.
@@ -266,7 +267,7 @@ Upstream restore-state persistence remains upstream-owned. The wrapper passes an
266
267
  - subprocess execution and JSON parsing through `buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`: copies the parent process environment so user-approved provider credentials and other runtime variables reach upstream, then applies wrapper overrides such as the managed socket directory and clamped default operation timeout
267
268
  - clear missing-binary errors
268
269
  - filtered `network requests --current-page` / `--current-origin` / `--current-url` preparation results use the same `redactPresentationData` credential redactor as ordinary diagnostics before populating `details.data` or writing `outputPath`
269
- - compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set`, and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors. Shared URL redaction covers SAMLRequest/SAMLResponse/RelayState and auth-context-only state/nonce at model/persisted-artifact boundaries while retaining exact internal page-target URLs; snapshot spills are redacted before disk writes
270
+ - compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set` while distinguishing native `--curl` file-import operands from inline cookie values (including failed batch diagnostics), and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors. Shared URL redaction covers SAMLRequest/SAMLResponse/RelayState and auth-context-only state/nonce at model/persisted-artifact boundaries while retaining exact internal page-target URLs; snapshot spills are redacted before disk writes
270
271
  - bounded machine-readable outcome metadata on tool `details` (`resultCategory`, `successCategory`, `failureCategory`, optional `nextActions`, optional `pageChangeSummary` with observed-vs-dispatched evidence and per-step summaries on `batch`, optional `artifactVerification` with the same shape on each successful `batchSteps[]` row) so agents can branch without parsing prose; enums, classifier precedence, and generic follow-up payloads are implemented under `extensions/agent-browser/lib/results/` in focused modules (`contracts.ts` for shared types, `categories.ts` for `classifyAgentBrowserSuccessCategory` / `classifyAgentBrowserFailureCategory` / `buildAgentBrowserResultCategoryDetails`, `action-recommendations.ts` for `buildAgentBrowserNextActions`, `next-actions.ts` for the `AgentBrowserNextAction` shape and merge helpers, `recovery-actions.ts` for recovery id registries and `buildRecoveryNextActions`, `network.ts` for `classifyNetworkRequestFailure` / `summarizeNetworkFailures`, and related helpers). Per-session tab target, `refSnapshot` alignment, invalidation, and tab pinning observations flow through `extensions/agent-browser/lib/session-page-state.ts` from `extensions/agent-browser/index.ts`. Compact page-change summaries and artifact verification rollups are built in `extensions/agent-browser/lib/results/presentation.ts` (`buildPageChangeSummary`, `buildArtifactVerificationSummary`), and the human contract lives in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). Final public results return native `isError` after all wrapper reclassifications, without a `tool_result` repair hook. Prose retains the visible category notice; explicit parseable JSON stays unchanged. `lib/native-output.ts` and `finalizeAgentBrowserFailure` in `lib/pi-tool-rendering.ts` keep structured observations, transcript status and TUI affordances aligned (transcript semantics in the same contract doc)
271
272
  - inline screenshots/images for the plain `screenshot` command; other image-like saves (for example `diff screenshot`) still appear in `details.artifacts` and summaries but are not auto-inlined as Pi image attachments (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details))
272
273
  - lightweight session convenience