pi-agent-browser-native 0.9.6 → 0.9.7

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 (34) hide show
  1. package/CHANGELOG.md +95 -1
  2. package/README.md +14 -6
  3. package/dist/extensions/agent-browser/index.js +1276 -350
  4. package/dist/packaged/lib/browser-session-conversion.js +157 -21
  5. package/dist/packaged/lib/playbook.js +42 -8
  6. package/dist/packaged/lib/results/snapshot.js +140 -11
  7. package/dist/packaged/lib/runtime.js +58 -10
  8. package/docs/ARCHITECTURE.md +19 -10
  9. package/docs/CODE_QUALITY.md +21 -5
  10. package/docs/COMMAND_REFERENCE.md +154 -38
  11. package/docs/ELECTRON.md +1 -1
  12. package/docs/PERFORMANCE.md +12 -3
  13. package/docs/REFERENCE.md +14 -14
  14. package/docs/RELEASE.md +41 -18
  15. package/docs/REQUIREMENTS.md +6 -0
  16. package/docs/SESSION_STORAGE.md +3 -3
  17. package/docs/SUPPORT_MATRIX.md +25 -23
  18. package/docs/TOOL_CONTRACT.md +37 -14
  19. package/docs/platform-smoke.md +18 -14
  20. package/package.json +6 -3
  21. package/platform-smoke.config.mjs +3 -2
  22. package/scripts/agent-browser-capability-baseline.mjs +16 -2
  23. package/scripts/agent-browser-target.mjs +1 -1
  24. package/scripts/capability-options.mjs +20 -0
  25. package/scripts/capability-samples.mjs +23 -10
  26. package/scripts/platform-smoke/commands.mjs +30 -4
  27. package/scripts/platform-smoke/doctor-support.mjs +11 -2
  28. package/scripts/platform-smoke/doctor-windows.mjs +1 -1
  29. package/scripts/platform-smoke/doctor.mjs +26 -11
  30. package/scripts/platform-smoke/linux-image/Dockerfile +6 -3
  31. package/scripts/platform-smoke/platform-build-windows.ps1 +23 -3
  32. package/scripts/platform-smoke/suite-checks.mjs +11 -1
  33. package/scripts/platform-smoke/targets.mjs +4 -3
  34. package/scripts/platform-smoke.mjs +68 -25
@@ -11,6 +11,11 @@ function isRecord(value) {
11
11
 
12
12
  // dist/extensions/agent-browser/lib/browser-journal-projection.js
13
13
  var ENVELOPE_MAX_BYTES = 16 * 1024 * 1024;
14
+ var MAX_NESTING = 1024;
15
+ var RESERVE_PER_BYTE = 16;
16
+ var NATIVE_RECORD_BYTES = ENVELOPE_MAX_BYTES / RESERVE_PER_BYTE;
17
+ var NATIVE_STRUCTURE_LIMIT = 64 * 1024;
18
+ var JOURNAL_CHUNK_BYTES = 64 * 1024;
14
19
  function matches(field, key) {
15
20
  return field === key || field === "*" && typeof key === "number";
16
21
  }
@@ -149,7 +154,7 @@ var JsonProjection = class {
149
154
  }
150
155
  }
151
156
  startContainer(name) {
152
- if (this.stack.length >= 1024) {
157
+ if (this.stack.length >= MAX_NESTING) {
153
158
  throw new Error("Journal JSON nesting exceeds 1024 levels.");
154
159
  }
155
160
  const currentPath = this.excluded() ? [] : this.path();
@@ -214,14 +219,110 @@ async function projectJson(input, fields, maxBytes = ENVELOPE_MAX_BYTES) {
214
219
  consume(chain.none);
215
220
  return projection.result();
216
221
  }
222
+ function stringEnd(bytes, open2) {
223
+ for (let quote = bytes.indexOf(34, open2 + 1); quote !== -1; quote = bytes.indexOf(34, quote + 1)) {
224
+ let escapes = 0;
225
+ while (bytes[quote - 1 - escapes] === 92) {
226
+ escapes++;
227
+ }
228
+ if (escapes % 2 === 0) {
229
+ return quote;
230
+ }
231
+ }
232
+ return -1;
233
+ }
234
+ var DEPTH_STEP = new Int8Array(256);
235
+ var STRUCTURE_STEP = new Uint8Array(256);
236
+ for (const byte of [91, 123]) {
237
+ DEPTH_STEP[byte] = 1;
238
+ STRUCTURE_STEP[byte] = 1;
239
+ }
240
+ for (const byte of [93, 125]) {
241
+ DEPTH_STEP[byte] = -1;
242
+ }
243
+ for (const byte of [44, 58]) {
244
+ STRUCTURE_STEP[byte] = 1;
245
+ }
246
+ function fitsNativeParse(bytes) {
247
+ let depth = 0;
248
+ let structure = 0;
249
+ for (let index = 0; index < bytes.length; index++) {
250
+ const byte = bytes[index];
251
+ if (byte === 34) {
252
+ index = stringEnd(bytes, index);
253
+ if (index === -1) {
254
+ return false;
255
+ }
256
+ continue;
257
+ }
258
+ depth += DEPTH_STEP[byte];
259
+ structure += STRUCTURE_STEP[byte];
260
+ if (depth > MAX_NESTING || structure > NATIVE_STRUCTURE_LIMIT) {
261
+ return false;
262
+ }
263
+ }
264
+ return true;
265
+ }
266
+ function selectValue(value, path, fields) {
267
+ if (typeof value !== "object" || value === null || fields.some((field) => field.every((key, index) => matches(key, path[index])))) {
268
+ return value;
269
+ }
270
+ if (Array.isArray(value)) {
271
+ const selected2 = [];
272
+ for (const [index, item] of value.entries()) {
273
+ const itemPath = [...path, index];
274
+ if (selectedPath(itemPath, fields)) {
275
+ selected2[index] = selectValue(item, itemPath, fields);
276
+ }
277
+ }
278
+ return selected2;
279
+ }
280
+ const selected = {};
281
+ for (const [key, item] of Object.entries(value)) {
282
+ const itemPath = [...path, key];
283
+ if (selectedPath(itemPath, fields)) {
284
+ Object.defineProperty(selected, key, {
285
+ value: selectValue(item, itemPath, fields),
286
+ writable: true,
287
+ enumerable: true,
288
+ configurable: true
289
+ });
290
+ }
291
+ }
292
+ return selected;
293
+ }
294
+ function nativeProjection(bytes, fields) {
295
+ if (!fitsNativeParse(bytes)) {
296
+ return void 0;
297
+ }
298
+ let value;
299
+ try {
300
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
301
+ } catch {
302
+ return void 0;
303
+ }
304
+ if (!isRecord(value)) {
305
+ return void 0;
306
+ }
307
+ const selected = selectValue(value, [], fields);
308
+ return isRecord(selected) ? selected : void 0;
309
+ }
310
+ async function* chunks(bytes) {
311
+ for (let start = 0; start < bytes.length; start += JOURNAL_CHUNK_BYTES) {
312
+ yield bytes.subarray(start, start + JOURNAL_CHUNK_BYTES);
313
+ }
314
+ }
315
+ async function projectJsonBytes(bytes, fields, maxBytes = ENVELOPE_MAX_BYTES) {
316
+ const native = bytes.length * RESERVE_PER_BYTE <= maxBytes ? nativeProjection(bytes, fields) : void 0;
317
+ return native ?? projectJson(chunks(bytes), fields, maxBytes);
318
+ }
217
319
 
218
320
  // dist/extensions/agent-browser/lib/browser-journal-reader.js
219
- var CHUNK_BYTES = 64 * 1024;
220
321
  async function* readRange(file, range) {
221
322
  let position = range.offset;
222
323
  const end = range.offset + range.length;
223
324
  while (position < end) {
224
- const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, end - position));
325
+ const buffer = Buffer.allocUnsafe(Math.min(JOURNAL_CHUNK_BYTES, end - position));
225
326
  const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
226
327
  if (bytesRead === 0) {
227
328
  throw new Error("Journal changed or truncated during read.");
@@ -230,35 +331,69 @@ async function* readRange(file, range) {
230
331
  yield buffer.subarray(0, bytesRead);
231
332
  }
232
333
  }
334
+ function hasContent(buffer, start, end) {
335
+ for (let index = start; index < end; index++) {
336
+ const byte = buffer[index];
337
+ if (byte !== 9 && byte !== 13 && byte !== 32) {
338
+ return true;
339
+ }
340
+ }
341
+ return false;
342
+ }
233
343
  var JournalLineFramer = class {
234
344
  start;
235
345
  nonblank = false;
346
+ parts = [];
347
+ held = 0;
236
348
  constructor(offset) {
237
349
  this.start = offset;
238
350
  }
239
351
  *accept(buffer, position) {
240
- for (let index = 0; index < buffer.length; index++) {
241
- const byte = buffer[index];
242
- if (byte === 10) {
243
- if (this.nonblank) {
244
- yield { offset: this.start, length: position + index + 1 - this.start };
245
- }
246
- this.start = position + index + 1;
247
- this.nonblank = false;
248
- } else if (![9, 13, 32].includes(byte)) {
249
- this.nonblank = true;
352
+ let index = 0;
353
+ while (index < buffer.length) {
354
+ const newline = buffer.indexOf(10, index);
355
+ const end = newline === -1 ? buffer.length : newline;
356
+ this.nonblank ||= hasContent(buffer, index, end);
357
+ this.hold(buffer.subarray(index, newline === -1 ? end : end + 1));
358
+ if (newline === -1) {
359
+ return;
360
+ }
361
+ const record = this.take(position + newline + 1);
362
+ if (record) {
363
+ yield record;
250
364
  }
365
+ index = newline + 1;
251
366
  }
252
367
  }
253
368
  final(end, sealed) {
254
- return this.nonblank && sealed ? { offset: this.start, length: end - this.start } : void 0;
369
+ return sealed ? this.take(end) : void 0;
370
+ }
371
+ hold(part) {
372
+ this.held += part.length;
373
+ if (this.held > NATIVE_RECORD_BYTES) {
374
+ this.parts = void 0;
375
+ } else {
376
+ this.parts?.push(part);
377
+ }
378
+ }
379
+ take(next) {
380
+ let record;
381
+ if (this.nonblank) {
382
+ const range = { offset: this.start, length: next - this.start };
383
+ record = this.parts ? { ...range, bytes: Buffer.concat(this.parts) } : range;
384
+ }
385
+ this.start = next;
386
+ this.nonblank = false;
387
+ this.parts = [];
388
+ this.held = 0;
389
+ return record;
255
390
  }
256
391
  };
257
392
  async function* journalRanges(file, end, sealed = false, offset = 0) {
258
393
  let position = offset;
259
394
  const framer = new JournalLineFramer(offset);
260
395
  while (position < end) {
261
- const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, end - position));
396
+ const buffer = Buffer.allocUnsafe(Math.min(JOURNAL_CHUNK_BYTES, end - position));
262
397
  const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
263
398
  if (bytesRead === 0) {
264
399
  throw new Error("Journal truncated during read.");
@@ -286,14 +421,14 @@ var STRUCTURAL_FIELDS = [
286
421
  ["message", "details", "artifactManifest", "version"],
287
422
  ["message", "details", "electron", "action"]
288
423
  ];
289
- async function scanJournalMetadata(file, end, sealed = false) {
424
+ async function scanJournalMetadata(file, end, sealed = false, offset = 0) {
290
425
  const entries = [];
291
- for await (const range of journalRanges(file, end, sealed)) {
292
- const value = await projectJson(readRange(file, range), STRUCTURAL_FIELDS);
426
+ for await (const range of journalRanges(file, end, sealed, offset)) {
427
+ const value = range.bytes ? await projectJsonBytes(range.bytes, STRUCTURAL_FIELDS) : await projectJson(readRange(file, range), STRUCTURAL_FIELDS);
293
428
  if (!value) {
294
429
  throw new Error(`Expected a journal object at byte ${range.offset}.`);
295
430
  }
296
- entries.push({ ...range, value });
431
+ entries.push({ offset: range.offset, length: range.length, value });
297
432
  }
298
433
  return entries;
299
434
  }
@@ -866,6 +1001,7 @@ var GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = /* @__PURE__ */ new Set([
866
1001
  "-q",
867
1002
  "--verbose",
868
1003
  "-v",
1004
+ "--use-system-ca",
869
1005
  "--webgpu"
870
1006
  ]);
871
1007
  function optionalGlobalValueFlagConsumesNext(flag, nextToken) {
@@ -2562,8 +2698,8 @@ async function sourceChecksum(file, size) {
2562
2698
  }
2563
2699
  return hash.digest("hex");
2564
2700
  }
2565
- async function writeBytes(file, chunks) {
2566
- for await (const chunk of chunks) {
2701
+ async function writeBytes(file, chunks2) {
2702
+ for await (const chunk of chunks2) {
2567
2703
  const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
2568
2704
  let offset = 0;
2569
2705
  while (offset < buffer.length) {
@@ -531,6 +531,39 @@ var LAUNCH_SCOPED_FLAG_DEFINITIONS = [
531
531
  ];
532
532
  var LAUNCH_SCOPED_FLAGS = LAUNCH_SCOPED_FLAG_DEFINITIONS.map((definition) => definition.flag);
533
533
  var LAUNCH_SCOPED_FLAG_LABEL = LAUNCH_SCOPED_FLAGS.join(", ");
534
+ var MANAGED_RESTORE_INCOMPATIBLE_FLAGS = [
535
+ "--restore",
536
+ "--restore-save",
537
+ "--restore-check-url",
538
+ "--restore-check-text",
539
+ "--restore-check-fn",
540
+ "--allowed-domains",
541
+ "--profile",
542
+ "--state",
543
+ "--cdp",
544
+ "--auto-connect",
545
+ "--session-name",
546
+ "--provider",
547
+ "-p",
548
+ "--executable-path",
549
+ "--extension",
550
+ "--init-script",
551
+ "--enable",
552
+ "--args",
553
+ "--user-agent",
554
+ "--proxy",
555
+ "--proxy-bypass",
556
+ "--ca-cert",
557
+ "--ignore-https-errors",
558
+ "--allow-file-access",
559
+ "--webgpu",
560
+ "--device",
561
+ "--engine"
562
+ ];
563
+ var HELPER_INHERITED_LAUNCH_FLAGS = /* @__PURE__ */ new Set(["--namespace", "--idle-timeout"]);
564
+ var BROWSER_LAUNCH_FLAGS = [
565
+ .../* @__PURE__ */ new Set([...LAUNCH_SCOPED_FLAGS, ...MANAGED_RESTORE_INCOMPATIBLE_FLAGS])
566
+ ].filter((flag) => !HELPER_INHERITED_LAUNCH_FLAGS.has(flag));
534
567
 
535
568
  // dist/extensions/agent-browser/lib/playbook.js
536
569
  var PROJECT_RULE_PROMPT = "Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
@@ -542,14 +575,14 @@ function buildInstalledDocsGuideline(paths) {
542
575
  }
543
576
  var QUICK_START_GUIDELINES = [
544
577
  "Choose agent_browser for one native command, native batch --bail with JSON-array stdin for a known fixed sequence, or agent_browser_code for loops, branches, and aggregation. Return to the model when a fresh observation needs judgment. Neither tool is a named recipe registry.",
545
- 'agent_browser takes args (argv after the binary), optional stdin, outputPath, timeoutMs, and sessionMode. Start with { args: ["open", "<url>"] }, then { args: ["snapshot", "-i"] }; act on current @refs and re-snapshot after navigation or DOM changes. Native commands, providers, plugins, profiles, auth, file access, and caller paths remain available through args.',
578
+ 'agent_browser takes args (argv after the binary), optional stdin, outputPath, timeoutMs, and sessionMode; sessionMode:fresh intentionally launches a separate managed browser. Start with { args: ["open", "<url>"] }, then { args: ["snapshot", "-i"] }; act on current @refs and re-snapshot after navigation or DOM changes. Native commands, providers, plugins, profiles, auth, file access, and caller paths remain available through args; native config and environment pass through. Use requested/configured profiles and verify authentication after restart.',
546
579
  'Use { args: ["batch", "--bail"], stdin: "[[\\"fill\\",\\"@e1\\",\\"text\\"],[\\"click\\",\\"@e2\\"]]" } for same-snapshot form fields followed by submit. Split if a fill rerenders later fields. Add native condition waits after navigation-prone actions; a click alone does not prove the destination loaded. Avoid blind mutation retries after timeout.',
547
580
  "agent_browser_code takes { code, session?, namespace?, timeoutMs?, outputPath? }. JavaScript globals are fresh for every call; the selected browser persists and is shared with agent_browser. Use await browser({ args, stdin?, timeoutMs? }), inspect result.success, and emit selected JSON instead of whole envelopes. Use emitImage(result.imageObservations[0]) only when image inspection is needed; image handles belong to the current code call. No imports or host filesystem/network/process APIs are exposed.",
548
581
  `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.`,
549
582
  '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.',
550
583
  "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.",
551
- "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.",
552
- "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."
584
+ "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. Prefer refs; inspect screenshots only when the task needs vision.",
585
+ "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. Native outcome_unknown means a command may have run and was not retried; inspect before any repeat. For dense snapshots, inspect Omitted high-value controls before reading a full spill."
553
586
  ];
554
587
  var ADVANCED_TOOL_PROMPT_GUIDELINES = {
555
588
  action: [
@@ -591,11 +624,11 @@ var SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
591
624
  "Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
592
625
  "For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data. On macOS, copied Chrome profiles may omit encrypted cookies, so profile selection alone is not proof of authentication; verify the target page and use a user-approved headed login once when needed. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
593
626
  "Ordinary bare calls share one native browser within your root Pi parent/subagent group; unrelated roots browse independently. Coordinate multi-call navigation inside your group with existing subagent/intercom tools. Explicit native session defaults still win. Pi quit does not close group browsers; never close an unrelated browser.",
594
- `When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.`,
627
+ `When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups. If you already used the implicit session and now need launch-scoped flags, 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.`,
595
628
  "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.",
596
629
  "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.",
597
- `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.`,
598
- "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.",
630
+ "Chrome is the default. Select separately installed Lightpanda or experimental Obscura explicitly with --engine lightpanda|obscura, environment or native config; use sessionMode:fresh to change a managed engine. Lightpanda text-rendered screenshots never establish graphical layout or mouse coordinates. Obscura has accessibility, iframe and screenshot gaps; read its command-reference limits first. Profiles, headed mode, multi-tab, WebGPU and WebMCP are not parity capabilities; choose Chrome when needed, never assume automatic fallback.",
631
+ "For CLI HTTPS trust on read/install/upgrade/doctor, use --ca-cert <path> or --use-system-ca; --use-system-ca false disables OS roots and SSL_CERT_FILE is a fallback. These do not disable certificate verification or configure remote CDP trust. Browser CA trust remains Linux-Chromium-only and launch-scoped.",
599
632
  '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.',
600
633
  "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.",
601
634
  "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.",
@@ -603,7 +636,7 @@ var SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
603
636
  "For first-navigation setup, use open without a URL plus network route --resource-type <csv>, cookies set --curl <file>, or --init-script/--enable before navigate/opening the target page.",
604
637
  "For stateful browser context work, use auth save --password-stdin with the tool stdin field for credentials, auth list/show/delete/remove for local auth-profile maintenance, auth login when you need the browser to fill a saved profile, and state save/load/list/show/rename/clear/clean for upstream saved-state lifecycle. State paths, restore identifiers, wrapper-prefixed sessions, and all upstream list rows remain available; credential values inside cookie/storage/auth payloads are still redacted from presentation.",
605
638
  "Upstream restore sessions periodically autosave cookies and localStorage while the browser stays open, including page-driven background changes; AGENT_BROWSER_AUTOSAVE_INTERVAL_MS controls the interval (30000 by default; 0 disables periodic saves but keeps save-on-close), while the never value for --restore-save disables automatic saves for that restore session. For wrapper-owned headed launches, the wrapper defaults the interval to 0 because upstream 0.33.2 collects multi-origin storage through visible temporary tabs, then records and reapplies the effective launch-time value to helpers and follow-ups so daemon configuration remains stable. Native close still saves, but direct window close can lose newer state because upstream exempts headed browsers from idle shutdown; set AGENT_BROWSER_AUTOSAVE_INTERVAL_MS before launch when periodic preservation matters, because changing it on a running wrapper-owned headed session requires close plus a fresh launch.",
606
- "For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command, redacted result or scrubbed error text) built from the same presentation rules as standalone calls.",
639
+ "For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command or native --omit-command index, redacted result or scrubbed error text) built from the same presentation rules as standalone calls. Indexed receipts preserve original input positions, including skipped empty rows, without echoing commands.",
607
640
  "For non-core families, pass current upstream commands through the native tool directly: network requests, network route <url>, network har start/stop [path], diff snapshot, diff screenshot --baseline <file>, diff url <u1> <u2>, trace start, trace stop [path], profiler start, profiler stop [path], record start <path>, record stop, console/errors [--clear], highlight <selector>, inspect, clipboard read, clipboard write <text>, clipboard copy/paste, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat <message>. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request agent_browser_network_source, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
608
641
  'For Electron desktop apps, enable agent_browser_electron through agent_browser_tools for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: "tabs" is the safer diagnostic starting point, use agent_browser_electron probe or snapshot -i/agent_browser_qa attached:true for current-session state, and always cleanup the returned launchId when done. agent_browser_electron launch uses an isolated temporary profile; it does not reuse the app\'s normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, run get url to verify the active target before page-content reads, inspect tab list, select the stable tab id such as tab t2, verify it again with get url, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.',
609
642
  "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox|derive-client through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Use derive-client when recording HAR traffic to generate a standalone API client; prefer network har start (text bodies by default) or network har start --content all|none before multi-step capture. For accessibility audits use a11y or a11y --tags wcag2a,wcag2aa (CDP browsers only). Hosted sandbox workflows should use upstream @agent-browser/sandbox helpers outside this wrapper. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
@@ -621,6 +654,7 @@ var SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
621
654
  "For evidence-only screenshots, QA captures, or other audit artifacts, save to an explicit path and branch on details.artifactVerification plus details.artifacts before reporting PASS/FAIL; do not require vision review of inline image attachments unless the user asked for visual inspection.",
622
655
  "Respect explicit user stop boundaries yourself. When the surrounding authenticated employee or automation context is explicitly unattended/auto-approved, ordinary non-destructive form submissions within the requested flow may proceed without separate confirmation. Still require explicit authorization for purchases, production-control actions, destructive or irreversible actions, and account, security, or privacy changes. The wrapper does not infer broad business intent from prompt text; details.promptGuard is reserved for concrete artifact-before-close checks.",
623
656
  "Recording needs ffmpeg on PATH before start. Current upstream checks it at startup; older natives may defer failure. A pending recording is not verified output.",
657
+ "Native 0.41 mcp call and stdin-driven MCP can finish normally, but this wrapper currently rejects all non-help mcp invocations. Session/namespace coordination and credential/image presentation are not integrated; use ordinary agent_browser commands.",
624
658
  "Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary."
625
659
  ];
626
660
  var TOOL_PROMPT_GUIDELINES_SUFFIX = [
@@ -669,7 +703,7 @@ function buildToolPromptGuidelines(options) {
669
703
  return [
670
704
  ...options.includeWebSearch ? ["Prefer agent_browser_web_search for facts; agent_browser for pages."] : TOOL_PROMPT_GUIDELINES_PREFIX,
671
705
  ...options.docs ? [buildInstalledDocsGuideline(options.docs)] : [],
672
- ...RUNTIME_PROMPT_GUIDELINES,
706
+ ...options.compact ? RUNTIME_PROMPT_GUIDELINES : [],
673
707
  ...browserExecutablePathGuideline !== void 0 ? [browserExecutablePathGuideline] : [],
674
708
  ...browserDefaultProfileGuideline !== void 0 ? [browserDefaultProfileGuideline] : [],
675
709
  TOOL_PROMPT_GUIDELINES_SUFFIX[0]
@@ -1912,17 +1912,77 @@ import { tmpdir } from "node:os";
1912
1912
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
1913
1913
 
1914
1914
  // dist/extensions/agent-browser/lib/temp-root-ownership.js
1915
- import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
1915
+ import { chmod, readFile as readFile2, readdir, rm, stat, writeFile } from "node:fs/promises";
1916
1916
  import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
1917
1917
 
1918
1918
  // dist/extensions/agent-browser/lib/process-identity.js
1919
1919
  import { execFile } from "node:child_process";
1920
+ import { readFile } from "node:fs/promises";
1920
1921
  import { dirname, join, win32 } from "node:path";
1922
+
1923
+ // dist/extensions/agent-browser/lib/browser-execution-policy-debug.js
1924
+ import { AsyncLocalStorage } from "node:async_hooks";
1925
+ import { debuglog } from "node:util";
1926
+ var debug = debuglog("pi-agent-browser-policy");
1927
+ var policyDebugEnabled = debug.enabled;
1928
+ var attempts = policyDebugEnabled ? new AsyncLocalStorage() : void 0;
1929
+ var syscalls = /* @__PURE__ */ new Set([
1930
+ "mkdir",
1931
+ "lstat",
1932
+ "stat",
1933
+ "scandir",
1934
+ "open",
1935
+ "read",
1936
+ "write",
1937
+ "rename",
1938
+ "rmdir",
1939
+ "unlink",
1940
+ "rm",
1941
+ "kill",
1942
+ "spawn",
1943
+ "connect",
1944
+ "listen"
1945
+ ]);
1946
+ function debugPolicyFailure(stage, reason, error) {
1947
+ if (!policyDebugEnabled) {
1948
+ return;
1949
+ }
1950
+ const context = attempts?.getStore();
1951
+ const code = nativeField(error, "code");
1952
+ const syscall = nativeField(error, "syscall");
1953
+ const exit = isRecord(error) && typeof error.code === "number" && Number.isSafeInteger(error.code) ? error.code : "-";
1954
+ debug("attempt=%s phase=%s stage=%s reason=%s code=%s syscall=%s exit=%s", context?.attempt ?? 0, context?.phase ?? "standalone", stage, reason, code, syscall, exit);
1955
+ }
1956
+ function nativeField(error, field) {
1957
+ const value = isRecord(error) ? error[field] : void 0;
1958
+ if (typeof value !== "string") {
1959
+ return "-";
1960
+ }
1961
+ if (field === "code") {
1962
+ return /^(?:E[A-Z0-9]{1,24}|ABORT_ERR|ERR_[A-Z_]{1,48})$/.test(value) ? value : "unknown";
1963
+ }
1964
+ if (value.startsWith("spawn ")) {
1965
+ return "spawn";
1966
+ }
1967
+ return syscalls.has(value) ? value : "unknown";
1968
+ }
1969
+
1970
+ // dist/extensions/agent-browser/lib/process-identity.js
1921
1971
  var WINDOWS_PROCESS_START_IDENTITY_PREFIX = "win32-powershell-ticks-v1:";
1972
+ var POSIX_PROCESS_START_IDENTITY_PREFIX = "posix-ps-utc-v1:";
1973
+ var LINUX_PROCESS_START_IDENTITY_PREFIX = "linux-proc-boot-ticks-v1:";
1974
+ var PROCESS_START_IDENTITY_FORMATS = [
1975
+ WINDOWS_PROCESS_START_IDENTITY_PREFIX,
1976
+ POSIX_PROCESS_START_IDENTITY_PREFIX,
1977
+ LINUX_PROCESS_START_IDENTITY_PREFIX
1978
+ ];
1922
1979
  var PROCESS_START_IDENTITY_TIMEOUT_MS = process.platform === "win32" ? 1e4 : 5e3;
1923
1980
  var DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
1981
+ var LINUX_BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id";
1982
+ var LINUX_BOOT_ID_PATTERN = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
1983
+ var LINUX_STAT_START_TICKS_INDEX = 22 - 3;
1924
1984
  function buildProcessStartIdentityCommand(pid, platform = process.platform) {
1925
- if (!Number.isSafeInteger(pid) || pid <= 0) {
1985
+ if (!Number.isSafeInteger(pid) || pid <= 0 || platform === "linux") {
1926
1986
  return void 0;
1927
1987
  }
1928
1988
  const configuredSystemRoot = process.env.SystemRoot;
@@ -1932,11 +1992,12 @@ function buildProcessStartIdentityCommand(pid, platform = process.platform) {
1932
1992
  "-NoProfile",
1933
1993
  "-NonInteractive",
1934
1994
  "-Command",
1935
- `$p = Get-Process -Id ${pid} -ErrorAction Stop; Write-Output ("${WINDOWS_PROCESS_START_IDENTITY_PREFIX}" + $p.StartTime.ToUniversalTime().Ticks)`
1995
+ `$ErrorActionPreference = 'Stop'; $p = $null; try { $p = [System.Diagnostics.Process]::GetProcessById(${pid}); [long]$ticks = $p.StartTime.ToUniversalTime().Ticks; if ($ticks -le 0) { throw 'Process start ticks unavailable' }; [Console]::Out.WriteLine("${WINDOWS_PROCESS_START_IDENTITY_PREFIX}" + $ticks.ToString([Globalization.CultureInfo]::InvariantCulture)) } finally { if ($null -ne $p) { $p.Dispose() } }`
1936
1996
  ],
1937
1997
  file: win32.join(windowsSystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
1938
1998
  } : {
1939
1999
  args: ["-p", String(pid), "-o", "lstart="],
2000
+ env: { ...process.env, TZ: "UTC0", LC_ALL: "C" },
1940
2001
  file: platform === "android" ? join(dirname(process.execPath), "ps") : "/bin/ps"
1941
2002
  };
1942
2003
  }
@@ -1965,14 +2026,22 @@ function normalizeProcessStartIdentity(stdout) {
1965
2026
  }
1966
2027
  var currentProcessStartIdentityPromise;
1967
2028
  var currentProcessStartIdentity;
2029
+ function debugIdentityBudget(budget) {
2030
+ debugPolicyFailure("probe.budget", budget?.signal?.aborted === true ? "cancelled" : "deadline");
2031
+ }
1968
2032
  async function executeProcessStartIdentityCommand(command, budget = {}) {
1969
- if (budget.signal?.aborted === true || budget.deadline !== void 0 && Date.now() >= budget.deadline) {
2033
+ if (budget.signal?.aborted === true || budget.deadline !== void 0 && performance.now() >= budget.deadline) {
2034
+ debugIdentityBudget(budget);
1970
2035
  return void 0;
1971
2036
  }
1972
- const timeout = Math.max(1, Math.min(PROCESS_START_IDENTITY_TIMEOUT_MS, (budget.deadline ?? Infinity) - Date.now()));
2037
+ const timeout = Math.max(1, Math.ceil(Math.min(PROCESS_START_IDENTITY_TIMEOUT_MS, (budget.deadline ?? Infinity) - performance.now())));
1973
2038
  return await new Promise((resolve3) => {
1974
- execFile(command.file, command.args, { timeout, signal: budget.signal }, (error, stdout) => {
1975
- resolve3(error ? void 0 : normalizeProcessStartIdentity(stdout));
2039
+ execFile(command.file, command.args, { env: command.env, timeout, signal: budget.signal }, (error, stdout) => {
2040
+ const identity = error ? void 0 : normalizeProcessStartIdentity(stdout);
2041
+ if (identity === void 0) {
2042
+ debugPolicyFailure("probe.execute", error ? "native-error" : "invalid-output", error);
2043
+ }
2044
+ resolve3(identity);
1976
2045
  });
1977
2046
  });
1978
2047
  }
@@ -1985,14 +2054,67 @@ async function resolveProcessStartIdentityFromCommands(commands, execute = execu
1985
2054
  }
1986
2055
  return void 0;
1987
2056
  }
2057
+ async function readLinuxProcfsText(path, budget) {
2058
+ if (identityBudgetExpired(budget)) {
2059
+ debugIdentityBudget(budget);
2060
+ return void 0;
2061
+ }
2062
+ try {
2063
+ return await readFile(path, { encoding: "latin1", signal: budget.signal });
2064
+ } catch (error) {
2065
+ debugPolicyFailure("probe.read", "native-error", error);
2066
+ return void 0;
2067
+ }
2068
+ }
2069
+ async function readLinuxBootId(budget) {
2070
+ const bootId = (await readLinuxProcfsText(LINUX_BOOT_ID_PATH, budget))?.replace(/\n$/, "");
2071
+ if (bootId === void 0 || !LINUX_BOOT_ID_PATTERN.test(bootId)) {
2072
+ debugPolicyFailure("probe.boot-id", bootId === void 0 ? "unavailable" : "invalid-output");
2073
+ return void 0;
2074
+ }
2075
+ return bootId;
2076
+ }
2077
+ function parseLinuxStartTicks(stat4, pid) {
2078
+ const header = `${String(pid)} (`;
2079
+ const commEnd = stat4.lastIndexOf(")");
2080
+ if (!stat4.startsWith(header) || commEnd < header.length || stat4[commEnd + 1] !== " ") {
2081
+ return void 0;
2082
+ }
2083
+ const ticks = stat4.slice(commEnd + 2).replace(/\n$/, "").split(" ").at(LINUX_STAT_START_TICKS_INDEX);
2084
+ return ticks !== void 0 && /^[0-9]+$/.test(ticks) ? ticks : void 0;
2085
+ }
2086
+ async function readLinuxProcessStartIdentity(pid, budget = {}) {
2087
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
2088
+ return void 0;
2089
+ }
2090
+ const bootId = await readLinuxBootId(budget);
2091
+ if (bootId === void 0) {
2092
+ return void 0;
2093
+ }
2094
+ const stat4 = await readLinuxProcfsText(`/proc/${String(pid)}/stat`, budget);
2095
+ if (stat4 === void 0) {
2096
+ return void 0;
2097
+ }
2098
+ const ticks = parseLinuxStartTicks(stat4, pid);
2099
+ if (ticks === void 0) {
2100
+ debugPolicyFailure("probe.stat", "invalid-output");
2101
+ return void 0;
2102
+ }
2103
+ return `${LINUX_PROCESS_START_IDENTITY_PREFIX}${bootId}:${ticks}`;
2104
+ }
1988
2105
  async function readUncachedProcessStartIdentity(pid, platform, budget) {
1989
- return await resolveProcessStartIdentityFromCommands(buildProcessStartIdentityCommands(pid, platform), (command) => executeProcessStartIdentityCommand(command, budget));
2106
+ if (platform === "linux") {
2107
+ return await readLinuxProcessStartIdentity(pid, budget);
2108
+ }
2109
+ const identity = await resolveProcessStartIdentityFromCommands(buildProcessStartIdentityCommands(pid, platform), (command) => executeProcessStartIdentityCommand(command, budget));
2110
+ return identity === void 0 || platform === "win32" ? identity : `${POSIX_PROCESS_START_IDENTITY_PREFIX}${identity}`;
1990
2111
  }
1991
2112
  function identityBudgetExpired(budget) {
1992
- return budget?.signal?.aborted === true || budget?.deadline !== void 0 && Date.now() >= budget.deadline;
2113
+ return budget?.signal?.aborted === true || budget?.deadline !== void 0 && performance.now() >= budget.deadline;
1993
2114
  }
1994
2115
  async function readProcessStartIdentity(pid, platform = process.platform, budget) {
1995
2116
  if (identityBudgetExpired(budget)) {
2117
+ debugIdentityBudget(budget);
1996
2118
  return void 0;
1997
2119
  }
1998
2120
  if (pid !== process.pid || platform !== process.platform) {
@@ -2022,6 +2144,10 @@ async function readCachedProcessStartIdentity(pid, platform) {
2022
2144
  return await currentProcessStartIdentityPromise;
2023
2145
  }
2024
2146
  function processStartIdentitiesMatch(recorded, current) {
2147
+ const format = PROCESS_START_IDENTITY_FORMATS.find((prefix) => current.startsWith(prefix));
2148
+ if (format === void 0 || !recorded.startsWith(format)) {
2149
+ return void 0;
2150
+ }
2025
2151
  return recorded === current;
2026
2152
  }
2027
2153
 
@@ -2088,7 +2214,7 @@ function validOwnershipAndChildren(value) {
2088
2214
  }
2089
2215
  async function readTempRootOwnershipMarker(tempRoot) {
2090
2216
  try {
2091
- const markerText = await readFile(join2(tempRoot, TEMP_ROOT_MARKER_FILE_NAME), "utf8");
2217
+ const markerText = await readFile2(join2(tempRoot, TEMP_ROOT_MARKER_FILE_NAME), "utf8");
2092
2218
  const parsed = JSON.parse(markerText);
2093
2219
  return isTempRootOwnershipRecord(parsed) ? parsed : void 0;
2094
2220
  } catch {
@@ -2172,7 +2298,7 @@ async function refreshSecureTempRootLease(tempRoot) {
2172
2298
  const currentProcessStartIdentity2 = await getProcessStartIdentity(process.pid);
2173
2299
  if (ownershipMarker.ownerProcessStartIdentity !== void 0 && currentProcessStartIdentity2 !== void 0) {
2174
2300
  const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity2);
2175
- if (!identitiesMatch) {
2301
+ if (identitiesMatch !== true) {
2176
2302
  return;
2177
2303
  }
2178
2304
  }
@@ -2207,6 +2333,9 @@ async function getMarkerOwnerLiveness(ownershipMarker) {
2207
2333
  return "unknown";
2208
2334
  }
2209
2335
  const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity2);
2336
+ if (identitiesMatch === void 0) {
2337
+ return "unknown";
2338
+ }
2210
2339
  return identitiesMatch ? "alive" : "dead";
2211
2340
  }
2212
2341