pi-agent-browser-native 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +17 -15
- package/dist/extensions/agent-browser/index.js +17 -6
- package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -7
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +32 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +4 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +18 -6
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +100 -9
- package/dist/extensions/agent-browser/lib/playbook.js +4 -4
- package/dist/extensions/agent-browser/lib/process.js +18 -5
- package/docs/ARCHITECTURE.md +10 -5
- package/docs/COMMAND_REFERENCE.md +6 -3
- package/docs/RELEASE.md +6 -4
- package/docs/SUPPORT_MATRIX.md +6 -2
- package/docs/TOOL_CONTRACT.md +21 -7
- package/docs/platform-smoke.md +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.6.13 - 2026-09-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Suppress Chrome's extra startup window using stock `--no-startup-window`, preserving headless defaults and composing caller launch arguments. URL-less `open` now uses native lazy URL reads so active pages keep their browser, profile, and URL across direct, batch, and script calls. External engines and attachments remain unchanged.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Give ordinary browser calls a native session and restore key per root Pi session. Parent/descendant groups share one browser without a global queue or child-exit teardown. Scope named Chrome profile bootstrap to automatic roots; preserve explicit browsers, script, and Electron isolation.
|
|
12
|
+
|
|
13
|
+
### Validation
|
|
14
|
+
|
|
15
|
+
- Start managed-lifecycle release checks explicitly in fresh mode; retain root-session ownership without leaving test browsers open.
|
|
16
|
+
|
|
17
|
+
## 0.6.12 - 2026-09-13
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Make QA non-pass when final page-error rows match a nonempty post-clear baseline. Report “page-error check could not be verified” instead of ignoring matches as unchanged; preserve separate novel-error counts, clean passes, and explicit `checkErrors: false` checks.
|
|
22
|
+
|
|
23
|
+
### Validation
|
|
24
|
+
|
|
25
|
+
- Keep real-browser fixture probes on the correct owned daemon and use matching private socket directories; preserve the cold first-snapshot and 12-second recording checks.
|
|
26
|
+
- Add a native hosted-Windows PR gate using the existing packed-Pi and browser suites. Document direct local macOS qualification without Remote Login; neither alternative is a Crabbox SSH/Parallels pass.
|
|
4
27
|
|
|
5
28
|
## 0.6.11 - 2026-09-12
|
|
6
29
|
|
package/README.md
CHANGED
|
@@ -317,6 +317,10 @@ Open a page and inspect it (first-call recipe: open → snapshot -i → interact
|
|
|
317
317
|
|
|
318
318
|
Chromium launch switches belong in the value of `--args`, for example `{ "args": ["--args", "--no-sandbox", "open", "https://example.com"], "sessionMode": "fresh" }` when that switch is needed. A bare `--no-sandbox` in the command slot is an unknown command; in an `open` / `goto` / `navigate` option position, upstream ignores it. The wrapper diagnoses those forms without rejecting literal text, select values or paths in other commands. For batches, put `--args` in top-level `args` before `batch`, not inside a row.
|
|
319
319
|
|
|
320
|
+
Local Chrome startup uses the stock binary's `--args --no-startup-window` capability to suppress Chrome's extra startup window. Headless remains the default. Caller launch arguments keep native CLI → environment → config precedence and follow this default; browsers started before this change with custom arguments may restart once when their native launch configuration changes. Ordinary follow-ups do not resend bootstrap settings. CDP, providers, Electron, and Lightpanda remain native-owned; no tabs are deleted or profiles edited by this behavior.
|
|
321
|
+
|
|
322
|
+
`open` without a URL uses native `get url`: it launches when needed and otherwise preserves the current page. This also applies to effective batch rows and script calls. `details.args` preserves the request, while `effectiveArgs`, command, URL data, and lifecycle describe the actual native operation. Use `open about:blank` only when you intend to navigate there. Help/version requests remain unchanged.
|
|
323
|
+
|
|
320
324
|
Watch a browser window during a demo, QA run, or user-completed login by adding upstream's global `--headed` flag on the first launch. Use `sessionMode: "fresh"` if a managed session may already exist, because headed/headless state is launch-scoped. A successful first/fresh local wrapper-managed headed launch, including a launch inside `batch`, returns `details.browserWindow = { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` and one visible handoff sentence; CDP, auto-connect, provider, and Electron attachments do not. This proves that the wrapper requested and upstream launched headed mode, not that the OS window is visible on the user's display; remote, container, or virtual-display setups can still hide it. After the user finishes in the window, continue with `sessionMode: "auto"`.
|
|
321
325
|
|
|
322
326
|
```json
|
|
@@ -540,7 +544,7 @@ After either path, use `qa: { "attached": true, ... }` for a current-session smo
|
|
|
540
544
|
|
|
541
545
|
### Lightweight QA preset
|
|
542
546
|
|
|
543
|
-
For a quick smoke/QA pass, use top-level `qa`. It compiles to the same batch path as `job` and uses `batch --bail` so failed readiness/text/selector assertions stop before slower diagnostics can burn the wrapper watchdog. The URL form clears enabled network/console buffers and snapshots any page-error residue after the unreliable upstream clear, then opens the target URL and gives immediate post-load console/page-error callbacks a bounded 150 ms settle, waits for page readiness, checks optional expected text or selector, inspects fresh network requests, console messages, and page errors when preceding assertions pass, and can capture an evidence screenshot. Successful reset rows are labeled as reset-scoped output.
|
|
547
|
+
For a quick smoke/QA pass, use top-level `qa`. It compiles to the same batch path as `job` and uses `batch --bail` so failed readiness/text/selector assertions stop before slower diagnostics can burn the wrapper watchdog. The URL form clears enabled network/console buffers and snapshots any page-error residue after the unreliable upstream clear, then opens the target URL and gives immediate post-load console/page-error callbacks a bounded 150 ms settle, waits for page readiness, checks optional expected text or selector, inspects fresh network requests, console messages, and page errors when preceding assertions pass, and can capture an evidence screenshot. Successful reset rows are labeled as reset-scoped output. Final page-error rows matching a nonempty post-clear baseline make QA non-pass: old residue and identical new errors cannot be distinguished, especially when the native buffer rolls over. `qaPreset.passed` is `false` with `failureCategory: "qa-failure"` and the reason “page-error check could not be verified”—not a claim that those rows prove new application errors. Definitely new rows, including matching errors after an empty baseline, still count as page errors. Clean checks with no ambiguous or new rows can pass; explicit `checkErrors: false` skips this check for a more limited smoke test. Expected text is checked with bounded visible-text `wait --fn … --timeout 5000` predicates after the requested load state so dense pages can pass on visible headings/copy and missing text becomes crisp QA evidence. The attached form (`qa: { "attached": true }`) runs checks against the current managed session, such as an attached Electron app, rejects `url`, and deliberately preserves existing diagnostics instead of clearing evidence; its diagnostic reads default off so stale buffers do not fail a current-page smoke unless `checkNetwork`, `checkConsole`, or `checkErrors` is explicitly `true`. `loadState` defaults to `"domcontentloaded"`; set it to `"load"` or `"networkidle"` only when the stricter state is useful and the site is not expected to keep background requests alive. For URL-opening QA, `checkNetwork`, `checkConsole`, and `checkErrors` default to true; set one to `false` to skip that diagnostic read. Network failures are classified by likely impact and failed rows are listed first in network previews: actionable document/script/API-style failures still fail QA, while some low-impact browser icon asset misses (for example certain `favicon` or `apple-touch-icon` paths when upstream marks the row failed and resource metadata looks image-like) surface only as warnings instead of failing an otherwise healthy smoke check (`details.qaPreset.warnings`, with human-readable `details.qaPreset.summary` when the preset still passes). Exact predicates live in [`docs/TOOL_CONTRACT.md`](docs/TOOL_CONTRACT.md#qa) and `classifyNetworkRequestFailure` in `extensions/agent-browser/lib/results/network.ts`.
|
|
544
548
|
|
|
545
549
|
```json
|
|
546
550
|
{
|
|
@@ -608,33 +612,31 @@ After a successful unnamed fresh launch, later default `sessionMode: "auto"` cal
|
|
|
608
612
|
|
|
609
613
|
### Shared browser defaults
|
|
610
614
|
|
|
611
|
-
|
|
615
|
+
Ordinary browser calls use one native named browser per **root Pi session**. A parent and its subagents share that browser; unrelated roots get different names and can browse concurrently. `pi-subagents` supplies `PI_SUBAGENT_ROOT_SESSION_ID` through its existing launch records, including detached runs, delegated forks, grandchildren, and revived children. Ordinary Pi new/fork/clone sessions get their own root identity; resuming the same root or changing its cwd keeps its browser name.
|
|
616
|
+
|
|
617
|
+
Coordinate multi-call navigation within the group using existing subagent/intercom tools. Native commands are serialized by each browser daemon; wrapper helper queues are process-local. There is no global focus queue. Parent or child exit does not close the group browser. Native idle policy applies; explicitly close only your group's browser when the group is finished.
|
|
618
|
+
|
|
619
|
+
To bootstrap new root browsers from an authenticated normal Chrome profile, set the existing **Pi package** config in `~/.pi/config/pi-agent-browser-native/config.json`:
|
|
612
620
|
|
|
613
621
|
```json
|
|
614
622
|
{
|
|
615
|
-
"
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
623
|
+
"browser": {
|
|
624
|
+
"defaultProfile": { "name": "Default", "policy": "always" },
|
|
625
|
+
"executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
626
|
+
}
|
|
619
627
|
}
|
|
620
628
|
```
|
|
621
629
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
Pi quit does not close a caller-owned shared browser or impose its implicit 15-minute idle timeout. Native idle policy applies, including its default headed-browser exemption; explicit `--idle-timeout` applies to every helper in that call too. Keep native idle settings consistent between calls because changing them can restart the daemon. `AGENT_BROWSER_SOCKET_DIR` is honored for caller-owned sessions; the wrapper-specific socket override still takes priority.
|
|
630
|
+
For inactive automatic roots, global/override `always` profile **names** and the configured executable bootstrap native launch through a scoped subprocess environment shared with helpers. Active roots retain native launch settings instead of resending defaults that could replace an explicitly profiled browser; their existing restore key is preserved, including follow-ups naming the same root browser. Native Chrome profile names use independent temporary copies, not the source profile's lock. Profile paths and other profile policies remain advisory; project guidance does not change a root's launch settings when a child changes cwd. Explicit unrelated sessions, native launch/config/environment choices, isolated `script`, and Electron launches do not receive the root bootstrap defaults. Do not set a global native profile merely to enable this feature: it can change the launch configuration of existing unrelated browsers.
|
|
625
631
|
|
|
626
|
-
|
|
632
|
+
Each automatic root has its own stable native restore key. Native save/restore preserves cookies, localStorage, and sessionStorage across normal close/restart; it does not merge auth between roots or save new IndexedDB-only credentials, service workers, or page-memory grants. Named profile copies can bootstrap existing IndexedDB, but are discarded on close and never write back to the communal profile. Verify the actual application after restart, not just a successful restore receipt. If an application needs unsupported state, use a supported persistent **per-root** profile and collect required sign-ins together; never point concurrent roots at one writable profile directory. Sites can expire sessions or require a fresh human challenge.
|
|
627
633
|
|
|
628
|
-
|
|
634
|
+
Explicit native `session` defaults (config or `AGENT_BROWSER_SESSION`) still override automatic root selection, including `sessionMode: "fresh"`. They intentionally share whichever browser the caller selected. Per-call session/namespace flags retain precedence. Ordinary launch options such as `--profile` or `--executable-path` still use the root-group name. Repeat explicit options when deliberately relaunching a persistent per-root profile after close. Explicit `fresh` and attachment choices retain their existing managed-session lifecycle; later auto calls follow that selected managed session. Use `session info` with the reported `sessionName` for read-only native daemon/browser evidence. Unknown native fields remain unknown.
|
|
629
635
|
|
|
630
636
|
Policy-required URL reads keep the two-call read → confirm/deny flow. Returned actions name the actual native namespace/session, including `default`; routing survives transcript resume and branch changes. Only explicit-read provenance plus native `capabilities.readRequiresConfirmation: true` permits matching confirm/deny without page helpers. This capability includes native confirmation-ID checking. Legacy prompts retain correct routing but normal page checks; DOM or content-shaped prompts never receive the exemption.
|
|
631
637
|
|
|
632
638
|
Full live identity, browser-independent native reads/confirmations and detailed recording receipts require the companion upstream fixes; the current recommended release does not yet supply all of them. Older supported versions remain accepted with unavailable evidence marked unknown. The extension does not upgrade or restart your installed browser to obtain these fields.
|
|
633
639
|
|
|
634
|
-
Coordinate tabs/navigation between agents sharing a browser; the wrapper's per-session queue is local to one Pi process. Persistent profile storage preserves browser data, not a promise of permanent website login: sites can expire or revoke sessions and require a new human challenge. Standing permission avoids repeated permission requests, but cannot change those site policies.
|
|
635
|
-
|
|
636
|
-
Before enabling global native profile defaults, update and fully restart every participating Pi runtime. Older loaded wrappers can still let disposable `script` launches read HOME config; editing this source or running `/reload` does not replace their cached code. Coordinate the browser cutover too: moving an existing daemon from the old wrapper's explicit 15-minute idle timeout to native idle policy changes its launch fingerprint and can restart it. Do not change that policy while another agent is using the browser.
|
|
637
|
-
|
|
638
640
|
### Profile selection
|
|
639
641
|
|
|
640
642
|
The wrapper does not clone profiles or hide what upstream Chrome/Chromium profile or executable you chose. Passing `--profile` or `--executable-path` is an explicit upstream `agent-browser` choice. Visible page content from real profiles is model-visible and may persist in transcripts or saved artifacts; redaction protects credential-like cookie/storage/auth values, not ordinary page text you asked the browser to read. Redactions use `[REDACTED]` (URL-encoded in parsed URLs); ordinary technical phrases such as `bearer token` stay intact. URL redaction includes `code`, `authorization_session_id`, and auth-context `state` / `nonce` across visible text, details, spills, and `outputPath` exports. URLs needing no redaction keep their original spelling.
|
|
@@ -1370,7 +1370,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1370
1370
|
}
|
|
1371
1371
|
if (resolvedInput.kind !== "script")
|
|
1372
1372
|
await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
|
|
1373
|
-
|
|
1373
|
+
const runtimeBrowserConfig = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
|
|
1374
|
+
const rootProfile = runtimeBrowserConfig.trustedBrowserDefaultProfile;
|
|
1375
|
+
const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
|
|
1376
|
+
const rootSessionId = process.env.PI_SUBAGENT_CHILD === "1" && process.env.PI_SUBAGENT_ROOT_SESSION_ID
|
|
1377
|
+
? process.env.PI_SUBAGENT_ROOT_SESSION_ID : ctx.sessionManager.getSessionId();
|
|
1378
|
+
return withNativeSessionDefaults(resolvedInput, ctx.cwd, signal, async (resolvedInput, withLaunchDefaults) => {
|
|
1374
1379
|
const readConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
|
|
1375
1380
|
if (readConfirmation)
|
|
1376
1381
|
resolvedInput = { ...resolvedInput, toolArgs: scopeReadConfirmationArgs(resolvedInput.toolArgs, readConfirmation) };
|
|
@@ -1614,7 +1619,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1614
1619
|
const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
|
|
1615
1620
|
? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
|
|
1616
1621
|
: undefined;
|
|
1617
|
-
const runBrowserCommand = async () => {
|
|
1622
|
+
const runBrowserCommand = async (daemonInactive) => {
|
|
1618
1623
|
flushRecordingReservations();
|
|
1619
1624
|
const branchRestoreGenerationAtStart = branchRestoreGeneration;
|
|
1620
1625
|
const generationAtStart = branchStateGeneration;
|
|
@@ -1664,6 +1669,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1664
1669
|
?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
|
|
1665
1670
|
const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
|
|
1666
1671
|
let result = await runAgentBrowserTool({
|
|
1672
|
+
daemonInactive,
|
|
1667
1673
|
ctx,
|
|
1668
1674
|
cwd: ctx.cwd,
|
|
1669
1675
|
electronPostCommandStatusSettleMs: ELECTRON_POST_COMMAND_STATUS_SETTLE_MS,
|
|
@@ -1771,6 +1777,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1771
1777
|
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
|
|
1772
1778
|
};
|
|
1773
1779
|
const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
|
|
1780
|
+
const runWithLaunchDefaults = () => withLaunchDefaults ? withLaunchDefaults(runBrowserCommand) : runBrowserCommand();
|
|
1774
1781
|
const runWithinSessionQueue = () => {
|
|
1775
1782
|
if (closesAllSessions)
|
|
1776
1783
|
return managedSessionExecutionQueue.run(() => {
|
|
@@ -1783,13 +1790,13 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1783
1790
|
sessionMode: params.sessionMode ?? "auto",
|
|
1784
1791
|
stdin: resolvedInput.toolStdin,
|
|
1785
1792
|
});
|
|
1786
|
-
return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace,
|
|
1793
|
+
return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runWithLaunchDefaults);
|
|
1787
1794
|
});
|
|
1788
1795
|
if (serializeBrowserCommand)
|
|
1789
|
-
return managedSessionExecutionQueue.run(
|
|
1796
|
+
return managedSessionExecutionQueue.run(runWithLaunchDefaults);
|
|
1790
1797
|
return callerOwnedSessionQueueKey
|
|
1791
|
-
? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace,
|
|
1792
|
-
:
|
|
1798
|
+
? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runWithLaunchDefaults)
|
|
1799
|
+
: runWithLaunchDefaults();
|
|
1793
1800
|
};
|
|
1794
1801
|
if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
|
|
1795
1802
|
return runWithinSessionQueue();
|
|
@@ -1814,6 +1821,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1814
1821
|
validationError: artifactValidationError,
|
|
1815
1822
|
}));
|
|
1816
1823
|
});
|
|
1824
|
+
}, managedSessionActive || freshSessionOrdinal > 0 || params.sessionMode === "fresh" || pendingReadConfirmation ? undefined : {
|
|
1825
|
+
id: rootSessionId,
|
|
1826
|
+
profile: rootProfile?.policy === "always" && !/[\\/~]/.test(rootProfile.name) ? rootProfile.name : undefined,
|
|
1827
|
+
executablePath: runtimeBrowserConfig.trustedBrowserExecutablePath,
|
|
1817
1828
|
});
|
|
1818
1829
|
},
|
|
1819
1830
|
};
|
|
@@ -257,7 +257,7 @@ export function buildQaCompactPassText(options) {
|
|
|
257
257
|
lines.push(`Page: ${pageParts.join(" — ")}`);
|
|
258
258
|
lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
|
|
259
259
|
if (options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
|
|
260
|
-
lines.push("Diagnostic isolation: URL QA clears enabled
|
|
260
|
+
lines.push("Diagnostic isolation: URL QA requests clears of enabled diagnostic buffers before opening the target.");
|
|
261
261
|
}
|
|
262
262
|
if (options.checks.attached && !options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
|
|
263
263
|
lines.push("Attached diagnostics: existing upstream session console/network/error buffers were preserved; rows may include events from before qa.attached started.");
|
|
@@ -368,17 +368,17 @@ function subtractQaBaselineErrors(errors, baselineErrors) {
|
|
|
368
368
|
const signature = qaErrorSignature(error);
|
|
369
369
|
baselineCounts.set(signature, (baselineCounts.get(signature) ?? 0) + 1);
|
|
370
370
|
}
|
|
371
|
-
let
|
|
371
|
+
let matchedCount = 0;
|
|
372
372
|
const novelErrors = errors.filter((error) => {
|
|
373
373
|
const signature = qaErrorSignature(error);
|
|
374
374
|
const count = baselineCounts.get(signature) ?? 0;
|
|
375
375
|
if (count === 0)
|
|
376
376
|
return true;
|
|
377
377
|
baselineCounts.set(signature, count - 1);
|
|
378
|
-
|
|
378
|
+
matchedCount += 1;
|
|
379
379
|
return false;
|
|
380
380
|
});
|
|
381
|
-
return {
|
|
381
|
+
return { matchedCount, novelErrors };
|
|
382
382
|
}
|
|
383
383
|
function isDiagnosticResetCommand(item) {
|
|
384
384
|
const command = item.command;
|
|
@@ -422,11 +422,11 @@ export function analyzeQaPresetResults(data, compiled) {
|
|
|
422
422
|
continue;
|
|
423
423
|
}
|
|
424
424
|
if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
425
|
-
const {
|
|
425
|
+
const { matchedCount, novelErrors } = subtractQaBaselineErrors(result.errors, baselineErrors);
|
|
426
426
|
if (novelErrors.length > 0)
|
|
427
427
|
failedChecks.push(`${novelErrors.length} page error(s)`);
|
|
428
|
-
if (
|
|
429
|
-
|
|
428
|
+
if (matchedCount > 0)
|
|
429
|
+
failedChecks.push(`page-error check could not be verified (${matchedCount} row(s) matched the post-clear baseline; old residue and identical new errors are indistinguishable)`);
|
|
430
430
|
}
|
|
431
431
|
if (commandName === "console" && Array.isArray(result?.messages)) {
|
|
432
432
|
const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { projectUpstreamGlobalFlags } from "../argv-grammar.js";
|
|
2
|
+
/** Bare open's native launch action drops prior launch options. URL reads lazily launch without navigating. */
|
|
3
|
+
export function normalizeUrlLessOpen(args, stdin, batchStep = false) {
|
|
4
|
+
const { tokens, indices } = batchStep ? { tokens: args, indices: args.map((_, index) => index) } : projectUpstreamGlobalFlags(args);
|
|
5
|
+
if (tokens[0] === "open" && !tokens.slice(1).some(token => !token.startsWith("--"))) {
|
|
6
|
+
const index = indices[0];
|
|
7
|
+
return { args: [...args.slice(0, index), "get", "url", ...args.slice(index + 1)], stdin };
|
|
8
|
+
}
|
|
9
|
+
if (tokens[0] !== "batch")
|
|
10
|
+
return { args, stdin };
|
|
11
|
+
const rawSteps = tokens.slice(1).flatMap((token, offset) => {
|
|
12
|
+
const step = token === "--bail" ? undefined : parseBatchCommandArgument(token).step;
|
|
13
|
+
return step ? [{ index: indices[offset + 1], step }] : [];
|
|
14
|
+
});
|
|
15
|
+
if (tokens.slice(1).some(token => token !== "--bail")) {
|
|
16
|
+
let normalized = args;
|
|
17
|
+
for (const { index, step } of rawSteps) {
|
|
18
|
+
const row = normalizeUrlLessOpen(step, undefined, true).args;
|
|
19
|
+
if (row === step)
|
|
20
|
+
continue;
|
|
21
|
+
if (normalized === args)
|
|
22
|
+
normalized = [...args];
|
|
23
|
+
normalized[index] = row.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
|
|
24
|
+
}
|
|
25
|
+
return { args: normalized, stdin };
|
|
26
|
+
}
|
|
27
|
+
const steps = parseUserBatchStdin(stdin).steps;
|
|
28
|
+
if (!steps?.length)
|
|
29
|
+
return { args, stdin };
|
|
30
|
+
const normalized = steps.map(step => normalizeUrlLessOpen(step, undefined, true).args);
|
|
31
|
+
return { args, stdin: normalized.some((step, index) => step !== steps[index]) ? JSON.stringify(normalized) : stdin };
|
|
32
|
+
}
|
|
1
33
|
const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
|
|
2
34
|
// Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
|
|
3
35
|
export function parseBatchCommandArgument(command) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
|
|
1
|
+
import { runAgentBrowserProcess, withAttachedBrowserSessionContext, withChromeStartupArgs } from "../../process.js";
|
|
2
2
|
import { isRecord } from "../../parsing.js";
|
|
3
3
|
import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
|
|
4
4
|
import { cleanupClickDispatchProbe } from "./click-dispatch.js";
|
|
@@ -9,7 +9,7 @@ import { processBrowserOutput } from "./process-output.js";
|
|
|
9
9
|
export { closeManagedSession } from "./managed-session-daemon-policy.js";
|
|
10
10
|
export { getSessionContextKey } from "./session-state.js";
|
|
11
11
|
export async function runAgentBrowserTool(options) {
|
|
12
|
-
const result = await withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options));
|
|
12
|
+
const result = await withChromeStartupArgs(options.input.persistentChromeArgs, () => withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options)));
|
|
13
13
|
const details = isRecord(result.details) ? result.details : undefined;
|
|
14
14
|
const sessionKey = getSessionContextKey(typeof details?.sessionName === "string" ? details.sessionName : undefined, typeof details?.namespace === "string" ? details.namespace : undefined);
|
|
15
15
|
const page = options.state.sessionPageState.get(sessionKey);
|
|
@@ -29,7 +29,7 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
29
29
|
return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
|
|
30
30
|
try {
|
|
31
31
|
const artifactRunStartedAtMs = Date.now();
|
|
32
|
-
const processResult = await runAgentBrowserProcess({
|
|
32
|
+
const processResult = await withChromeStartupArgs(prepared.chromeStartupArgs, () => runAgentBrowserProcess({
|
|
33
33
|
args: prepared.processArgs,
|
|
34
34
|
browserIndependentReadConfirmation: prepared.readConfirmation !== undefined,
|
|
35
35
|
cwd: options.cwd,
|
|
@@ -43,7 +43,7 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
43
43
|
signal: options.signal,
|
|
44
44
|
stdin: prepared.processStdin,
|
|
45
45
|
timeoutMs: prepared.processTimeoutMs,
|
|
46
|
-
});
|
|
46
|
+
}));
|
|
47
47
|
const missingBinaryResult = await buildMissingBinaryFailureResult({
|
|
48
48
|
compatibilityWorkaround: prepared.compatibilityWorkaround,
|
|
49
49
|
electronLaunch: prepared.electronLaunch,
|
|
@@ -2,7 +2,9 @@ import { copyFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { extractExplicitSessionName, getBooleanFlagValue, isUpstreamEnvFlagEnabled, projectUpstreamGlobalFlags, resolveAgentBrowserNamespace } from "../../argv-grammar.js";
|
|
4
4
|
import { isCloseCommand } from "../../command-taxonomy.js";
|
|
5
|
-
import { isBrowserIndependentRead } from "../../command-policy.js";
|
|
5
|
+
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
6
|
+
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
7
|
+
import { prepareAgentBrowserSpawnArgs, withChromeStartupArgs } from "../../process.js";
|
|
6
8
|
import { cleanupElectronLaunchResources } from "../../electron/cleanup.js";
|
|
7
9
|
import { launchElectronApp } from "../../electron/launch.js";
|
|
8
10
|
import { pathExists } from "../../fs-utils.js";
|
|
@@ -22,7 +24,7 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createFreshSe
|
|
|
22
24
|
import { buildOwnedManagedSessionRestoreContext, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
23
25
|
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
24
26
|
import { getExplicitSessionPageVerificationRequirement, getPageTargetValidationError, } from "../../page-target-validation.js";
|
|
25
|
-
import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError } from "./managed-session-daemon-policy.js";
|
|
27
|
+
import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
|
|
26
28
|
import { buildManagedSessionOutcome, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, findElectronLaunchRecordForSession, extractStringResultField, ensureSessionTabTarget, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
|
|
27
29
|
import { getUpstreamEffectiveBatchSteps, parseBatchStdinJsonArray } from "../batch-stdin.js";
|
|
28
30
|
import { buildElectronHostFailureResult, formatAgentBrowserNextActionsText, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
|
|
@@ -551,7 +553,16 @@ export async function prepareBrowserRun(options) {
|
|
|
551
553
|
};
|
|
552
554
|
}
|
|
553
555
|
}
|
|
554
|
-
|
|
556
|
+
let chromeStartupArgs;
|
|
557
|
+
if (options.input.chromeStartupArgs !== undefined && !options.preserveAttachedBrowserSession && !browserIndependent
|
|
558
|
+
&& !executionPlan.validationError && !isCloseCommand(executionPlan.commandInfo.command)
|
|
559
|
+
&& needsManagedSession(parseArgvDescriptor(preparedArgs.args), runtimeToolStdin)) {
|
|
560
|
+
const inactive = ownedManagedSession ? managedSessionDaemonInactive : options.daemonInactive
|
|
561
|
+
?? (executionPlan.sessionName !== undefined && (await inspectManagedSessionDaemon({ cwd, signal, sessionName: executionPlan.sessionName, namespace: executionPlan.namespace })).status === "inactive");
|
|
562
|
+
if (inactive || options.input.configuredChromeLaunch)
|
|
563
|
+
chromeStartupArgs = options.input.chromeStartupArgs;
|
|
564
|
+
}
|
|
565
|
+
return await withChromeStartupArgs(chromeStartupArgs, () => withOwnedManagedSessionContext(ownedManagedSession, async () => {
|
|
555
566
|
const managedSessionRestoreDisabled = () => state.managedSessionRestoreState.isDisabled(executionPlan.sessionName, executionPlan.namespace);
|
|
556
567
|
const sessionStateKey = getSessionContextKey(executionPlan.sessionName, executionPlan.namespace);
|
|
557
568
|
const priorSessionPageState = sessionPageState.get(sessionStateKey);
|
|
@@ -737,7 +748,7 @@ export async function prepareBrowserRun(options) {
|
|
|
737
748
|
requirement: livePageRequirement,
|
|
738
749
|
stdin: runtimeToolStdin,
|
|
739
750
|
});
|
|
740
|
-
const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
|
|
751
|
+
const redactedEffectiveArgs = redactInvocationArgs(prepareAgentBrowserSpawnArgs(executionPlan.effectiveArgs, undefined, options.preserveAttachedBrowserSession, chromeStartupArgs));
|
|
741
752
|
const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
|
|
742
753
|
const compatibilityWorkaround = executionPlan.compatibilityWorkaround;
|
|
743
754
|
const statePatch = executionPlan.managedSessionName === freshSessionName
|
|
@@ -1022,7 +1033,7 @@ export async function prepareBrowserRun(options) {
|
|
|
1022
1033
|
catch { }
|
|
1023
1034
|
}
|
|
1024
1035
|
const processTimeoutMs = options.params.timeoutMs ?? getDialogAwareProcessTimeoutMs(commandTokens, promptRefSnapshot, processStdin) ?? getCommandAwareProcessTimeoutMs(commandTokens, processStdin, readTimeoutPageUrl);
|
|
1025
|
-
const redactedProcessArgs = redactInvocationArgs(processArgs);
|
|
1036
|
+
const redactedProcessArgs = redactInvocationArgs(prepareAgentBrowserSpawnArgs(processArgs, ownedManagedSession?.compatibilityUserAgent, options.preserveAttachedBrowserSession, chromeStartupArgs));
|
|
1026
1037
|
const scrollAmount = Number(commandTokens.find((token) => /^\d+(?:\.\d+)?$/.test(token)));
|
|
1027
1038
|
const shouldProbeScrollNoop = executionPlan.commandInfo.command === "scroll" && executionPlan.startupScopedFlags.length === 0 && (state.managedSessionActive || sessionMode === "fresh") && (!Number.isFinite(scrollAmount) || scrollAmount >= 500);
|
|
1028
1039
|
const scrollPositionBefore = shouldProbeScrollNoop
|
|
@@ -1043,6 +1054,7 @@ export async function prepareBrowserRun(options) {
|
|
|
1043
1054
|
return {
|
|
1044
1055
|
kind: "ready",
|
|
1045
1056
|
prepared: {
|
|
1057
|
+
chromeStartupArgs,
|
|
1046
1058
|
commandTokens,
|
|
1047
1059
|
headedLaunch,
|
|
1048
1060
|
providerLaunch,
|
|
@@ -1091,7 +1103,7 @@ export async function prepareBrowserRun(options) {
|
|
|
1091
1103
|
userRequestedJson,
|
|
1092
1104
|
},
|
|
1093
1105
|
};
|
|
1094
|
-
});
|
|
1106
|
+
}));
|
|
1095
1107
|
}
|
|
1096
1108
|
finally {
|
|
1097
1109
|
if (!managedSessionPolicyLockTransferred)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgvDescriptor } from "../argv-descriptor.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeUrlLessOpen } from "./batch-stdin.js";
|
|
3
|
+
import { isPlainTextInspectionArgs, validateToolArgs, redactInvocationArgs, redactSensitiveText } from "../runtime.js";
|
|
3
4
|
import { buildAgentBrowserResultCategoryDetails } from "../results/categories.js";
|
|
4
5
|
import { compileAgentBrowserElectron } from "../input-modes/electron.js";
|
|
5
6
|
import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "../input-modes/job.js";
|
|
@@ -167,7 +168,8 @@ export function resolveAgentBrowserInput(options) {
|
|
|
167
168
|
const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
|
|
168
169
|
const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
|
|
169
170
|
const redactedCompiledSourceLookup = redactCompiledSourceLookup(compiledSourceLookup);
|
|
170
|
-
const
|
|
171
|
+
const normalized = validationError || isPlainTextInspectionArgs(toolArgs) ? { args: toolArgs, stdin: toolStdin } : normalizeUrlLessOpen(toolArgs, toolStdin);
|
|
172
|
+
const resolvedBase = { redactedArgs, toolArgs: normalized.args, toolStdin: normalized.stdin };
|
|
171
173
|
if (validationError) {
|
|
172
174
|
return {
|
|
173
175
|
...resolvedBase,
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, rm } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
5
|
+
import { extractExplicitSessionName, getBooleanFlagValue, isUpstreamEnvFlagEnabled, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
5
6
|
import { isRecord } from "../parsing.js";
|
|
7
|
+
import { parseArgvDescriptor } from "../argv-descriptor.js";
|
|
8
|
+
import { needsManagedSession } from "../command-policy.js";
|
|
9
|
+
import { getUpstreamEffectiveBatchSteps } from "./batch-stdin.js";
|
|
10
|
+
import { hasLaunchScopedFlagToken } from "../launch-scoped-flags.js";
|
|
6
11
|
import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../process-environment.js";
|
|
7
|
-
import { runAgentBrowserProcess } from "../process.js";
|
|
12
|
+
import { runAgentBrowserProcess, withChromeStartupArgs } from "../process.js";
|
|
8
13
|
import { parseAgentBrowserEnvelope } from "../results/envelope.js";
|
|
9
14
|
import { isPlainTextInspectionArgs } from "../runtime.js";
|
|
15
|
+
import { buildValidationFailureResult } from "./input-plan.js";
|
|
16
|
+
import { inspectManagedSessionDaemon } from "./browser-run/managed-session-daemon-policy.js";
|
|
17
|
+
import { buildMissingBinaryMessage } from "./browser-run/final-result.js";
|
|
18
|
+
// These native defaults already send a local launch command on every call; keep its args stable too.
|
|
19
|
+
const LOCAL_VALUE_DEFAULTS = ["executablePath", "profile", "state", "proxy", "args", "userAgent", "caCert", "colorScheme", "downloadPath", "engine", "allowedDomains"];
|
|
20
|
+
const LOCAL_BOOLEAN_DEFAULTS = ["headed", "allowFileAccess", "webgpu", "noWebmcp"];
|
|
21
|
+
const LOCAL_ARRAY_DEFAULTS = ["extensions", "initScripts", "enable"];
|
|
22
|
+
const nativeEnvName = (key) => `AGENT_BROWSER_${key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()}`;
|
|
23
|
+
function hasLocalLaunchDefaults(config, env) {
|
|
24
|
+
return LOCAL_VALUE_DEFAULTS.some(key => env[nativeEnvName(key)] !== undefined || config[key] !== undefined)
|
|
25
|
+
|| LOCAL_BOOLEAN_DEFAULTS.some(key => isUpstreamEnvFlagEnabled(env[nativeEnvName(key)]) || config[key] === true)
|
|
26
|
+
|| LOCAL_ARRAY_DEFAULTS.some(key => (env[nativeEnvName(key)]?.length ?? 0) > 0 || Array.isArray(config[key]) && config[key].length > 0)
|
|
27
|
+
|| (env.AGENT_BROWSER_CLEAR_CA_CERT !== undefined ? isUpstreamEnvFlagEnabled(env.AGENT_BROWSER_CLEAR_CA_CERT) : config.clearCaCert === true)
|
|
28
|
+
|| (env.AGENT_BROWSER_HIDE_SCROLLBARS !== undefined ? !isUpstreamEnvFlagEnabled(env.AGENT_BROWSER_HIDE_SCROLLBARS) : config.hideScrollbars === false)
|
|
29
|
+
|| ["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"].some(key => env[key] !== undefined);
|
|
30
|
+
}
|
|
31
|
+
function requestsConnection(tokens, stdin) {
|
|
32
|
+
return tokens[0] === "connect" || getUpstreamEffectiveBatchSteps(tokens, stdin).some(step => requestsConnection(step));
|
|
33
|
+
}
|
|
34
|
+
function rootBrowserSessionName(rootSessionId) {
|
|
35
|
+
return `pi-root-${createHash("sha256").update(rootSessionId).digest("hex").slice(0, 24)}`;
|
|
36
|
+
}
|
|
10
37
|
// Native `session` reports the resolved name, but not whether it was configured.
|
|
11
|
-
//
|
|
12
|
-
async function readNativeIdentity(path, cwd, signal) {
|
|
38
|
+
// Native validates its schema; retain configured launch defaults so root fallbacks never replace them.
|
|
39
|
+
async function readNativeIdentity(path, cwd, signal, rootFallback, browserCommand) {
|
|
13
40
|
let config;
|
|
14
41
|
try {
|
|
15
42
|
config = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -17,7 +44,11 @@ async function readNativeIdentity(path, cwd, signal) {
|
|
|
17
44
|
catch {
|
|
18
45
|
return {};
|
|
19
46
|
}
|
|
20
|
-
if (!isRecord(config)
|
|
47
|
+
if (!isRecord(config))
|
|
48
|
+
return {};
|
|
49
|
+
const rootLaunchConfig = browserCommand && (hasLocalLaunchDefaults(config, {}) || ["cdp", "autoConnect", "provider"].some((key) => config[key] !== undefined))
|
|
50
|
+
|| rootFallback && ["restore", "sessionName", "state", "allowedDomains", "profile", "executablePath"].some((key) => config[key] !== undefined);
|
|
51
|
+
if (!rootLaunchConfig && typeof config.session !== "string" && typeof config.namespace !== "string")
|
|
21
52
|
return {};
|
|
22
53
|
const result = await runAgentBrowserProcess({ args: ["--config", path, "--json", "session"], cwd, signal, timeoutMs: 5_000 });
|
|
23
54
|
try {
|
|
@@ -29,7 +60,9 @@ async function readNativeIdentity(path, cwd, signal) {
|
|
|
29
60
|
const data = parsed.envelope?.data;
|
|
30
61
|
if (!isRecord(data) || typeof data.session !== "string")
|
|
31
62
|
throw new Error("Native agent-browser session inspection returned no session name; the browser command was not run.");
|
|
63
|
+
const { session: _session, namespace: _namespace, ...defaults } = config;
|
|
32
64
|
return {
|
|
65
|
+
...defaults,
|
|
33
66
|
...(typeof config.session === "string" ? { session: data.session } : {}),
|
|
34
67
|
...(typeof config.namespace === "string" ? { namespace: config.namespace } : {}),
|
|
35
68
|
};
|
|
@@ -39,7 +72,7 @@ async function readNativeIdentity(path, cwd, signal) {
|
|
|
39
72
|
await rm(result.stdoutSpillPath, { force: true });
|
|
40
73
|
}
|
|
41
74
|
}
|
|
42
|
-
export async function withNativeSessionDefaults(input, cwd, signal, run) {
|
|
75
|
+
export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
|
|
43
76
|
if (input.kind === "electron" && input.compiledElectron.action === "launch")
|
|
44
77
|
return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
|
|
45
78
|
if (input.kind === "script" || input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
|
|
@@ -50,10 +83,40 @@ export async function withNativeSessionDefaults(input, cwd, signal, run) {
|
|
|
50
83
|
const paths = configPath !== undefined
|
|
51
84
|
? [resolve(cwd, configPath)]
|
|
52
85
|
: [join(homedir(), ".agent-browser", "config.json"), join(cwd, "agent-browser.json")];
|
|
86
|
+
const rootName = root && rootBrowserSessionName(root.id);
|
|
87
|
+
const explicitSession = extractExplicitSessionName(input.toolArgs);
|
|
88
|
+
const rootFallback = root !== undefined && env.AGENT_BROWSER_SESSION === undefined && (explicitSession === undefined || explicitSession === rootName)
|
|
89
|
+
&& needsManagedSession(parseArgvDescriptor(input.toolArgs), input.toolStdin);
|
|
53
90
|
let identity = {};
|
|
91
|
+
const browserCommand = needsManagedSession(parseArgvDescriptor(input.toolArgs), input.toolStdin);
|
|
54
92
|
for (const path of paths)
|
|
55
|
-
identity = { ...identity, ...await readNativeIdentity(path, cwd, signal) };
|
|
56
|
-
|
|
93
|
+
identity = { ...identity, ...await readNativeIdentity(path, cwd, signal, rootFallback, browserCommand) };
|
|
94
|
+
// Native ORs environment/config booleans; only CLI false overrides configured auto-connect.
|
|
95
|
+
const autoConnect = getBooleanFlagValue(input.toolArgs, "--auto-connect")
|
|
96
|
+
?? (isUpstreamEnvFlagEnabled(env.AGENT_BROWSER_AUTO_CONNECT) || identity.autoConnect === true);
|
|
97
|
+
const attachment = hasLaunchScopedFlagToken(input.toolArgs, "--cdp")
|
|
98
|
+
|| env.AGENT_BROWSER_CDP !== undefined || identity.cdp !== undefined || autoConnect
|
|
99
|
+
|| parseArgvDescriptor(input.toolArgs).upstreamCommandTokens[0] === "connect";
|
|
100
|
+
const configuredSession = env.AGENT_BROWSER_SESSION ?? identity.session;
|
|
101
|
+
const launchArgs = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--args").at(-1)?.value ?? env.AGENT_BROWSER_ARGS ?? identity.args;
|
|
102
|
+
const engine = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--engine").at(-1)?.value ?? env.AGENT_BROWSER_ENGINE ?? identity.engine;
|
|
103
|
+
const provider = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--provider").at(-1)?.value ?? scanUpstreamGlobalFlagOccurrences(input.toolArgs, "-p").at(-1)?.value ?? env.AGENT_BROWSER_PROVIDER ?? identity.provider;
|
|
104
|
+
const tokens = parseArgvDescriptor(input.toolArgs).upstreamCommandTokens;
|
|
105
|
+
const batchAttaches = requestsConnection(tokens, input.toolStdin);
|
|
106
|
+
const localChrome = browserCommand && !attachment && !batchAttaches && provider === undefined
|
|
107
|
+
&& (engine === undefined || engine === "chrome");
|
|
108
|
+
const chromeStartupArgs = localChrome ? ["--no-startup-window", ...(typeof launchArgs === "string" ? [launchArgs] : [])].join(",") : undefined;
|
|
109
|
+
const persistentChromeArgs = hasLocalLaunchDefaults(identity, env) ? chromeStartupArgs : undefined;
|
|
110
|
+
const configuredChromeLaunch = persistentChromeArgs !== undefined || [...LOCAL_VALUE_DEFAULTS, ...LOCAL_BOOLEAN_DEFAULTS, ...LOCAL_ARRAY_DEFAULTS, "hideScrollbars", "clearCaCert"].some(key => {
|
|
111
|
+
const flag = { extensions: "--extension", initScripts: "--init-script", clearCaCert: "--no-ca-cert" }[key] ?? `--${key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
|
|
112
|
+
return scanUpstreamGlobalFlagOccurrences(input.toolArgs, flag).length > 0;
|
|
113
|
+
});
|
|
114
|
+
const rootDefault = rootName && configuredSession === undefined && rootFallback && !attachment
|
|
115
|
+
? rootName : undefined;
|
|
116
|
+
const session = configuredSession ?? rootDefault;
|
|
117
|
+
const restoreEligible = !["--restore", "--session-name", "--state", "--allowed-domains", "--provider", "-p", "--device"].some((flag) => hasLaunchScopedFlagToken(input.toolArgs, flag))
|
|
118
|
+
&& !["restore", "sessionName", "state", "allowedDomains", "provider"].some((key) => identity[key] !== undefined)
|
|
119
|
+
&& !["AGENT_BROWSER_RESTORE", "AGENT_BROWSER_SESSION_NAME", "AGENT_BROWSER_STATE", "AGENT_BROWSER_ALLOWED_DOMAINS", "AGENT_BROWSER_PROVIDER"].some((key) => env[key] !== undefined);
|
|
57
120
|
const namespace = env.AGENT_BROWSER_NAMESPACE ?? identity.namespace;
|
|
58
121
|
let args = input.toolArgs;
|
|
59
122
|
if (session !== undefined && extractExplicitSessionName(args) === undefined)
|
|
@@ -64,5 +127,33 @@ export async function withNativeSessionDefaults(input, cwd, signal, run) {
|
|
|
64
127
|
...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
|
|
65
128
|
...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
|
|
66
129
|
...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
|
|
67
|
-
}, () => run({ ...input, toolArgs: args })
|
|
130
|
+
}, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun) => {
|
|
131
|
+
const daemon = await inspectManagedSessionDaemon({ cwd, signal, sessionName: rootDefault,
|
|
132
|
+
namespace: scanUpstreamGlobalFlagOccurrences(args, "--namespace").at(-1)?.value ?? namespace, timeoutMs: 5_000 });
|
|
133
|
+
if (daemon.status === "missing-binary")
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: buildMissingBinaryMessage() }],
|
|
136
|
+
details: { agentBrowserStarted: false, args: input.redactedArgs, sessionName: rootDefault, resultCategory: "failure", failureCategory: "missing-binary" },
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
if (input.kind === "qa" && input.compiledQaPreset.checks.attached && daemon.status !== "active") {
|
|
140
|
+
return buildValidationFailureResult({ ...input, attemptedKind: "qa", kind: "invalid", status: "invalid", validationError: "qa.attached requires an active attached session. Open the root browser first, or select an existing native session." });
|
|
141
|
+
}
|
|
142
|
+
// An active daemon already owns its launch settings. Re-sending defaults can replace an explicitly profiled browser.
|
|
143
|
+
const bootstrap = daemon.status === "inactive";
|
|
144
|
+
let restore;
|
|
145
|
+
if (restoreEligible && daemon.status === "active")
|
|
146
|
+
restore = daemon.restoreKey ?? undefined;
|
|
147
|
+
if (restoreEligible && bootstrap)
|
|
148
|
+
restore = rootDefault;
|
|
149
|
+
const profile = scanUpstreamGlobalFlagOccurrences(args, "--profile").at(-1)?.value
|
|
150
|
+
?? (bootstrap && restoreEligible && identity.profile === undefined && env.AGENT_BROWSER_PROFILE === undefined ? root?.profile : undefined);
|
|
151
|
+
const executablePath = scanUpstreamGlobalFlagOccurrences(args, "--executable-path").at(-1)?.value
|
|
152
|
+
?? (bootstrap && identity.executablePath === undefined && env.AGENT_BROWSER_EXECUTABLE_PATH === undefined ? root?.executablePath : undefined);
|
|
153
|
+
return withAgentBrowserProcessEnvironment({
|
|
154
|
+
...(restore !== undefined ? { AGENT_BROWSER_RESTORE: restore } : {}),
|
|
155
|
+
...(profile !== undefined ? { AGENT_BROWSER_PROFILE: profile } : {}),
|
|
156
|
+
...(executablePath !== undefined ? { AGENT_BROWSER_EXECUTABLE_PATH: executablePath } : {}),
|
|
157
|
+
}, () => withChromeStartupArgs(bootstrap || configuredChromeLaunch ? chromeStartupArgs : undefined, () => browserRun(bootstrap)));
|
|
158
|
+
}));
|
|
68
159
|
}
|
|
@@ -30,7 +30,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
30
30
|
"For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard type for framework-controlled editors that require real key events. keyboard inserttext is paste-like and can change a DOM value without updating application state, so use it only when later application-state evidence proves the edit was accepted. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
|
|
31
31
|
"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.",
|
|
32
32
|
"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.",
|
|
33
|
-
"
|
|
33
|
+
"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.",
|
|
34
34
|
`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.`,
|
|
35
35
|
"Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.",
|
|
36
36
|
"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.",
|
|
@@ -80,7 +80,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
|
|
|
80
80
|
export const RUNTIME_PROMPT_GUIDELINES = [
|
|
81
81
|
"Use agent_browser with one input mode: script, args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin: batch/eval/auth/wrapper batch only; electron rejects it; never pass --json.",
|
|
82
82
|
"For agent_browser, use open → snapshot -i → @refs; re-snapshot after changes. In authenticated unattended/auto-approved employee flows, ordinary requested non-destructive submissions may proceed. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
|
|
83
|
-
"agent_browser
|
|
83
|
+
"agent_browser bare calls share a root Pi browser with descendants, not unrelated roots; coordinate within the group. Native explicit sessions win. Use sessionMode=fresh for an intentional separate launch. Use requested/configured profiles only; verify auth after restart. Script is disposable.",
|
|
84
84
|
"agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; ffmpeg before recording; close keeps files; waited:timeout proves nothing.",
|
|
85
85
|
"When agent_browser details.nextActions exists, use them. Check Omitted high-value controls in dense snapshots. Dashboards: verify scroll via screenshot/snapshot.",
|
|
86
86
|
"agent_browser: read <url> for docs/text or active DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>. Batch 3+ getters; heed visibility warnings.",
|
|
@@ -88,13 +88,13 @@ export const RUNTIME_PROMPT_GUIDELINES = [
|
|
|
88
88
|
export function buildBrowserExecutablePathGuideline(executablePath) {
|
|
89
89
|
if (!executablePath)
|
|
90
90
|
return undefined;
|
|
91
|
-
return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}
|
|
91
|
+
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.`;
|
|
92
92
|
}
|
|
93
93
|
export function buildBrowserDefaultProfileGuideline(profile) {
|
|
94
94
|
if (!profile || profile.policy === "explicit-only")
|
|
95
95
|
return undefined;
|
|
96
96
|
if (profile.policy === "always") {
|
|
97
|
-
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always
|
|
97
|
+
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.`;
|
|
98
98
|
}
|
|
99
99
|
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.`;
|
|
100
100
|
}
|
|
@@ -5,7 +5,7 @@ import { dirname, isAbsolute, join } from "node:path";
|
|
|
5
5
|
import { env as processEnv, platform as processPlatform } from "node:process";
|
|
6
6
|
import { spawn as crossSpawn } from "cross-spawn";
|
|
7
7
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
8
|
-
import { extractExplicitSessionName, resolveAgentBrowserNamespace } from "./argv-grammar.js";
|
|
8
|
+
import { extractExplicitSessionName, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./argv-grammar.js";
|
|
9
9
|
import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
10
10
|
import { getPageTargetValidationError, } from "./page-target-validation.js";
|
|
11
11
|
import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
|
|
@@ -27,6 +27,10 @@ const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
|
27
27
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
28
28
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
29
29
|
const attachedBrowserSessionContext = new AsyncLocalStorage();
|
|
30
|
+
const chromeStartupArgsContext = new AsyncLocalStorage();
|
|
31
|
+
export function withChromeStartupArgs(args, run) {
|
|
32
|
+
return chromeStartupArgsContext.run(args ?? chromeStartupArgsContext.getStore(), run);
|
|
33
|
+
}
|
|
30
34
|
export function withAttachedBrowserSessionContext(preserve, run) {
|
|
31
35
|
return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
|
|
32
36
|
}
|
|
@@ -34,10 +38,19 @@ function appendTail(text, addition, maxChars) {
|
|
|
34
38
|
const combined = text + addition;
|
|
35
39
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
36
40
|
}
|
|
37
|
-
export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
|
|
38
|
-
if (preserveAttachedBrowserSession
|
|
41
|
+
export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false, startupArgs) {
|
|
42
|
+
if (preserveAttachedBrowserSession)
|
|
43
|
+
return args;
|
|
44
|
+
const occurrence = scanUpstreamGlobalFlagOccurrences(args, "--args").at(-1);
|
|
45
|
+
const customArgs = wrapperCompatibilityUserAgent && !occurrence
|
|
46
|
+
? `${startupArgs ?? "--no-startup-window"},--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}` : startupArgs;
|
|
47
|
+
if (customArgs === undefined)
|
|
39
48
|
return args;
|
|
40
|
-
|
|
49
|
+
if (!occurrence)
|
|
50
|
+
return ["--args", customArgs, ...args];
|
|
51
|
+
const normalized = [...args];
|
|
52
|
+
normalized[occurrence.index + 1] = customArgs;
|
|
53
|
+
return normalized;
|
|
41
54
|
}
|
|
42
55
|
function terminateSpawnedChild(child, signal) {
|
|
43
56
|
if (processPlatform === "win32" && child.pid) {
|
|
@@ -472,7 +485,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
472
485
|
return;
|
|
473
486
|
}
|
|
474
487
|
const spawnBrowser = processPlatform === "win32" ? crossSpawn : spawn;
|
|
475
|
-
const child = spawnBrowser("agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession), {
|
|
488
|
+
const child = spawnBrowser("agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
|
|
476
489
|
cwd,
|
|
477
490
|
env: childEnv,
|
|
478
491
|
stdio: ["pipe", "pipe", "pipe"],
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -45,7 +45,7 @@ The extension should:
|
|
|
45
45
|
- accept an optional top-level `script` string as a mutually exclusive one-shot orchestration mode for loops, conditional page branches, and multi-page aggregation. Source runs in a separate permissioned Node child with a constrained VM context; only null-prototype `browser({ args, stdin?, timeoutMs? })` and `emit(value)` task functions cross a bounded JSON-lines IPC bridge. The parent serializes at most one inner call at a time through the same full ordinary tool executor, clears ambient upstream launch/proxy controls across helpers and cleanup, caps calls/source/post-redaction output/time, injects one unique restore-disabled wrapper-owned session, writes a strict Pi custom-entry cleanup lease before first launch, closes in `finally`, aborts and awaits active-script cleanup on branch change/shutdown, and recovers exact non-closed active-branch leases afterward. Script requires Pi session persistence and exposes no profile/attachment/session-control, host API, named recipe, import, or persistent workflow-state surface (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#script)).
|
|
46
46
|
- accept an optional native `semanticAction` object as a mutually exclusive alternative to `args` on a single tool call (and to `script`, `job`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call), compile locator actions into upstream `find` argv, direct selector/ref click/check/fill into upstream command argv, and native dropdown selection into upstream `select <selector> <value...>` argv (with optional `semanticAction.session` expanding to a leading `--session <name>` before the compiled command when targeting a named upstream browser instead of the managed default), and echo the compiled shape in `details.compiledSemanticAction` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#semanticaction))
|
|
47
47
|
- accept an optional native `job` object (mutually exclusive with `script`, `args`, `semanticAction`, `qa`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) with a small fixed step vocabulary that compiles only to existing upstream `batch` argv rows, generates the JSON batch stdin string internally, defaults to fail-fast `batch --bail` unless `failFast:false` is explicit, supports semantic locator fields on constrained `click`/`fill` steps by reusing the top-level semantic-action compiler, supports human-paced `type` by expanding to a bounded set of existing focus/keyboard/wait/press rows while compacting model-visible batch text, and echoes `details.compiledJob` for observability (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#job))
|
|
48
|
-
- accept an optional native `qa` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same fail-fast `batch --bail` path as `job`, runs a fixed diagnostic smoke sequence with bounded visible-text predicates for `expectedText`, preserves existing diagnostics for `qa.attached` while
|
|
48
|
+
- accept an optional native `qa` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `sourceLookup`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same fail-fast `batch --bail` path as `job`, runs a fixed diagnostic smoke sequence with bounded visible-text predicates for `expectedText`, preserves existing diagnostics for `qa.attached` while requesting clears only for URL-opening QA, and echoes `details.compiledQaPreset` plus structured `details.qaPreset` pass/fail evidence. Native clearing remains upstream-owned; the wrapper makes matched final rows from a nonempty post-clear error baseline non-pass through the existing `failedChecks` / `qa-failure` path because old residue and identical new errors are indistinguishable. Definitely new errors are counted separately, clean evidence can pass, and explicit `checkErrors: false` omits the check without changing browser lifecycle (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#qa))
|
|
49
49
|
- accept an optional native `sourceLookup` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `networkSourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, gathers evidence-backed local source *candidates* for a selector/fiber/component name, and echoes `details.compiledSourceLookup` plus structured `details.sourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sourcelookup)); unlike `qa`, it never applies a second pass/fail layer that marks the tool failed when upstream already reported batch success—failed upstream steps still fail the invocation normally, and `details.sourceLookup` may still be present for partial evidence
|
|
50
50
|
- accept an optional native `networkSourceLookup` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `electron` on the same call) that compiles to the same `batch` path, correlates failed network requests with initiator metadata and bounded workspace URL literals, and echoes `details.compiledNetworkSourceLookup` plus structured `details.networkSourceLookup` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#networksourcelookup)); like `sourceLookup`, it never flips a successful upstream batch to failed solely because no source candidates were found
|
|
51
51
|
- accept an optional native `electron` object (mutually exclusive with `script`, `args`, `semanticAction`, `job`, `qa`, `sourceLookup`, and `networkSourceLookup` on the same call) for bounded desktop Electron lifecycle: `list` scans the host for install candidates, `launch` creates a wrapper-owned isolated profile plus OS-chosen remote-debugging port, then attaches through upstream `connect` with `sessionMode: "fresh"`, cancellation prevents host spawn or interrupts readiness polling with process/profile cleanup, and `status` / `cleanup` / `probe` operate only on wrapper-tracked launches; host-side spawn and CDP discovery live in `extensions/agent-browser/lib/electron/discovery.ts`, `launch.ts`, and `cleanup.ts`, while compilation, transcript restore for `launchId` records, handoff probes, and merged `details.electron*` fields live under `extensions/agent-browser/lib/orchestration/electron-host/` and `extensions/agent-browser/lib/orchestration/browser-run/` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
|
|
@@ -141,9 +141,9 @@ The default extension factory optionally accepts `{ beforeExecute }`. `index.ts`
|
|
|
141
141
|
|
|
142
142
|
### Default
|
|
143
143
|
|
|
144
|
-
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
|
|
144
|
+
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.
|
|
145
145
|
|
|
146
|
-
When no native session
|
|
146
|
+
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; existing per-session queues remain process-local, with no global broker. An active older/fresh managed session keeps its existing lifecycle. A configured session has the same precedence over `fresh` as a literal `--session`; script and Electron launch keep their separate wrapper-owned lifecycles. 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.
|
|
147
147
|
|
|
148
148
|
Why:
|
|
149
149
|
- works out of the box
|
|
@@ -166,8 +166,9 @@ The tool should also expose a first-class `sessionMode: "fresh"` escape hatch so
|
|
|
166
166
|
|
|
167
167
|
### Ownership
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
-
|
|
169
|
+
Ownership rule:
|
|
170
|
+
- automatic root sessions use native caller-owned lifecycle; group members coordinate and close them explicitly
|
|
171
|
+
- older implicit auto-generated sessions are extension-managed convenience sessions
|
|
171
172
|
- unnamed `sessionMode: "fresh"` launches rotate that extension-managed session to a new upstream browser
|
|
172
173
|
- explicit/user-managed sessions are not auto-managed by default
|
|
173
174
|
- extension-managed sessions should be reusable during an active `pi` session and across `/reload`, exact-session relaunch, `/resume`, and Pi branch-tree transitions, while still being cleaned up predictably
|
|
@@ -208,6 +209,10 @@ This is primarily about ownership clarity and avoiding surprise, not adding a he
|
|
|
208
209
|
|
|
209
210
|
### Launch flags
|
|
210
211
|
|
|
212
|
+
`native-session-defaults.ts` reads launch-argument/engine fields through its existing native-validated config discovery. Local Chrome composes `--no-startup-window` with the highest-priority caller arguments. Root bootstrap retains its existing helper scope; owned/fresh preparation reuses daemon-policy inspection, while explicit local callers use the same session-info probe. `process.ts` applies the scoped arguments after managed restore-policy checks, composing the existing compatibility user agent without overriding caller Chrome arguments. Configured environment/file arguments remain consistent across helpers; ordinary active calls do not receive a new launch configuration. This adds no browser launcher, profile mutation, tab cleanup, binary distribution, or persistent launch-settings state. Older custom-argument browsers can undergo native's normal hash-change restart on first use of the new default.
|
|
213
|
+
|
|
214
|
+
Input resolution replaces URL-less `open` with native `get url` before planning, retaining requested argv separately. Effective raw/stdin batch rows use the existing native argument parser and raw-over-stdin precedence; only changed rows are rewritten. Native URL reads provide lazy launch and real lifecycle/URL output without navigating an existing page. Explicit navigation and help/version input are unchanged.
|
|
215
|
+
|
|
211
216
|
`agent-browser` startup flags are sticky once a session is already running.
|
|
212
217
|
The extension should surface that clearly and avoid hidden restart behavior in v1.
|
|
213
218
|
|
|
@@ -218,6 +218,7 @@ Tool parameters (use exactly one of `script`, `args`, `semanticAction`, `job`, `
|
|
|
218
218
|
- `outputPath`: optional wrapper-owned local file sink for successful results and recording receipts, including failed, pending, timed-out or recovered stops. Recording exports preserve an envelope with original attempt status and native receipt/verification (`details.outputFile.source: "recording-receipt"`); unrelated failed extractions remain unwritten. Use it for durable `eval`, `get`, `snapshot`, or diagnostic outputs, not as the destination for screenshots, downloads, recordings, or other browser artifacts; if the paths resolve to the same file, the browser artifact is preserved and the result-data write fails validation. If presentation compacted a large direct result, a result row, or the whole `batch`, the writer copies each full command-redacted pre-compaction value only from its matching live wrapper-manifest spill; if any required spill is unavailable or untrusted, it fails without writing compact metadata. `details.outputFile` reports the saved path and byte count. If caller argv includes upstream `--json`, the visible JSON content stays parseable and the save notice is only in `details.outputFile`.
|
|
219
219
|
- `timeoutMs`: optional per-call wrapper subprocess watchdog override in milliseconds for the requested browser CLI process. Managed-session policy inspection can independently consume up to 35 seconds before that process; this preflight is intentionally not shortened by `timeoutMs` because a busy but valid daemon must remain distinguishable from an unverifiable one.
|
|
220
220
|
- `sessionMode`:
|
|
221
|
+
- Unconfigured ordinary calls select a root-Pi named browser with native restore; descendants share it, unrelated roots do not wait on each other, and Pi quit leaves it running. Existing managed sessions and explicit fresh launches retain their lifecycle.
|
|
221
222
|
- Native configured `session` / `AGENT_BROWSER_SESSION` defaults select a caller-owned shared browser for ordinary calls, just like explicit `--session`, without requiring repeated flags. Such a selection wins over both modes and is not closed on Pi quit. See [shared browser defaults](../README.md#shared-browser-defaults).
|
|
222
223
|
- `"auto"` reuses the extension-managed session when no native session is selected.
|
|
223
224
|
- `"fresh"` rotates that managed session to a fresh upstream launch so launch-scoped flags (`--allowed-domains`, `--auto-connect`, `--args`, `--ca-cert`, `--no-ca-cert`, `--cdp`, `--enable`, `--executable-path`, `--webgpu`, `--init-script`, `--idle-timeout`, `--user-agent`, `--headed`, `--device`, `--namespace`, `--profile`, `--provider`, `-p`, `--restore`, `--restore-save`, `--restore-check-url`, `--restore-check-text`, `--restore-check-fn`, `--session-name`, `--state`) apply.
|
|
@@ -439,9 +440,9 @@ On app pages that expose a native dropdown, add a `select` step such as `{ "acti
|
|
|
439
440
|
|
|
440
441
|
Use raw `args: ["batch"]` with `stdin` when you need arbitrary upstream commands, flags, or batch failure policies outside the constrained schema. Do not pass `stdin` with `job`, `qa`, `sourceLookup`, `networkSourceLookup`, or `electron`; those modes generate or manage their own input.
|
|
441
442
|
|
|
442
|
-
For quick smoke/QA checks, use top-level `qa`. It clears enabled network/console buffers and snapshots any page-error residue after the unreliable upstream clear, then opens the target URL and gives immediate post-load console/page-error callbacks a bounded 150 ms settle, waits for page readiness, checks expected text/selector, then inspects fresh network requests, console messages, and page errors only if preceding assertions pass, and can capture an evidence screenshot. Successful reset rows are labeled as reset-scoped diagnostic output.
|
|
443
|
+
For quick smoke/QA checks, use top-level `qa`. It clears enabled network/console buffers and snapshots any page-error residue after the unreliable upstream clear, then opens the target URL and gives immediate post-load console/page-error callbacks a bounded 150 ms settle, waits for page readiness, checks expected text/selector, then inspects fresh network requests, console messages, and page errors only if preceding assertions pass, and can capture an evidence screenshot. Successful reset rows are labeled as reset-scoped diagnostic output. If final page-error rows match a nonempty post-clear baseline, QA is non-pass with “page-error check could not be verified”: identical new errors can replace old rows in the native FIFO buffer without changing the result. Those matches are not proof of new application errors. Definitely new rows, including errors after an empty baseline, count separately as page errors; a clean final buffer can pass. Explicit `checkErrors: false` omits this check for a more limited smoke test. The preset compiles to `batch --bail` so a missing text/selector assertion fails crisply instead of letting slower diagnostics burn the wrapper watchdog. Expected text compiles to bounded visible-text `wait --fn … --timeout 5000` predicates after load so dense pages can pass on visible headings/copy without dumping `body` text; missing text reports a crisp QA failure. The readiness wait defaults to `loadState: "domcontentloaded"`; set `loadState` to `"load"` or `"networkidle"` only when that stricter state is useful and the site is not expected to keep background requests alive. QA network diagnostics classify failed requests by likely impact and list failed rows first in the network preview: actionable document/script/API-style failures fail the preset, while common low-impact browser icon misses such as `favicon.ico` are surfaced as warnings (`qaPreset.warnings`) so they do not fail an otherwise healthy page. Successful QA with no failed checks returns compact model-visible prose (page URL/title when known, checks run, optional screenshot verification) while keeping the full step matrix in `details.qaPreset` and `details.batchSteps`. Failed QA presets report `details.resultCategory: "failure"`, `failureCategory: "qa-failure"`, show compact failure reasons with the full matrix in `details.batchSteps`, and real Pi sessions treat the diagnostic as a failed tool result. Prose output also gets a model-visible result-category line including `Pi tool isError: true`; caller-requested `--json` output keeps the JSON string parseable and relies on the patched `isError` plus `details` fields.
|
|
443
444
|
|
|
444
|
-
The same classification drives plain `network requests` presentation: when any row counts as failed (HTTP status ≥ 400, `failed: true`, or a string `error`), model-facing text starts with a line like `Network failure summary: 0 actionable, 1 benign low-impact (1 total).`, and each preview line can end with an impact tag such as `[benign: low-impact browser icon asset]` or `[actionable: document, script, API, or non-benign request failure]`. When safe request IDs are present, `details.nextActions` adds bounded read-only follow-ups such as `network request <id>`, `networkSourceLookup` for actionable failed rows, `network requests --filter <path>`, `network requests --clear` before a repro, and `network har start`; prefer those payloads over rebuilding request-id commands from prose. For aggregate buffers, the wrapper accepts `network requests --current-page` / `--current-origin` to render only rows matching the active page origin, or `--current-url` for exact active document URL matching; it strips those wrapper-only flags before upstream spawn and reports counts in `details.networkRequestsPageFilter`. If the wrapper has seen a prior `network route` in the same session, matching failed, pending, or CORS-looking fetch/XHR rows add `details.networkRouteDiagnostics` plus executable route-mock follow-ups (`inspect-routed-network-request` and `start-network-har-capture-for-route-mock`) so agents do not mistake an unfulfilled mock for a fulfilled mock; same-origin/CORS fixture retry guidance stays in visible prose. `network requests` also hides `data:image` screenshot/artifact noise from the compact preview by default while preserving raw rows in `details.data.requests`. Rules live in `classifyNetworkRequestFailure` / `summarizeNetworkFailures` in `extensions/agent-browser/lib/results/network.ts`; QA aggregation is `analyzeQaPresetResults` in `extensions/agent-browser/
|
|
445
|
+
The same classification drives plain `network requests` presentation: when any row counts as failed (HTTP status ≥ 400, `failed: true`, or a string `error`), model-facing text starts with a line like `Network failure summary: 0 actionable, 1 benign low-impact (1 total).`, and each preview line can end with an impact tag such as `[benign: low-impact browser icon asset]` or `[actionable: document, script, API, or non-benign request failure]`. When safe request IDs are present, `details.nextActions` adds bounded read-only follow-ups such as `network request <id>`, `networkSourceLookup` for actionable failed rows, `network requests --filter <path>`, `network requests --clear` before a repro, and `network har start`; prefer those payloads over rebuilding request-id commands from prose. For aggregate buffers, the wrapper accepts `network requests --current-page` / `--current-origin` to render only rows matching the active page origin, or `--current-url` for exact active document URL matching; it strips those wrapper-only flags before upstream spawn and reports counts in `details.networkRequestsPageFilter`. If the wrapper has seen a prior `network route` in the same session, matching failed, pending, or CORS-looking fetch/XHR rows add `details.networkRouteDiagnostics` plus executable route-mock follow-ups (`inspect-routed-network-request` and `start-network-har-capture-for-route-mock`) so agents do not mistake an unfulfilled mock for a fulfilled mock; same-origin/CORS fixture retry guidance stays in visible prose. `network requests` also hides `data:image` screenshot/artifact noise from the compact preview by default while preserving raw rows in `details.data.requests`. Rules live in `classifyNetworkRequestFailure` / `summarizeNetworkFailures` in `extensions/agent-browser/lib/results/network.ts`; QA aggregation is `analyzeQaPresetResults` in `extensions/agent-browser/lib/input-modes/job.ts`.
|
|
445
446
|
|
|
446
447
|
```json
|
|
447
448
|
{ "qa": { "url": "https://example.com", "expectedText": "Example Domain", "screenshotPath": ".dogfood/qa-example.png" } }
|
|
@@ -1015,7 +1016,7 @@ Example config:
|
|
|
1015
1016
|
}
|
|
1016
1017
|
```
|
|
1017
1018
|
|
|
1018
|
-
Browser
|
|
1019
|
+
Browser defaults are scoped: global/override `defaultProfile` with `policy: "always"` and a Chrome profile name (not a path), plus `executablePath`, become native subprocess defaults only for automatic root browsers and their helpers. Roots have distinct named sessions and restore keys; parents and descendants share one root through `PI_SUBAGENT_ROOT_SESSION_ID`. These defaults never apply to unrelated explicit sessions, script, or Electron launches. Profile paths, other policies, and project-only guidance remain advisory. Native restore keeps cookies/localStorage/sessionStorage, not new IndexedDB, service workers, or memory-only grants; verify app access after a real restart. Avoid global native profile changes or a shared writable profile path; see [shared browser defaults](../README.md#shared-browser-defaults). Configure profile/executable guidance globally, in trusted project config, or through `PI_AGENT_BROWSER_CONFIG`. Ask the agent to run `profiles` and `doctor` when profile resolution fails, then use the reported Chrome profile directory name, a full profile/user-data directory path if upstream accepts one, or the configured `browser.executablePath` with top-level `sessionMode: "fresh"`.
|
|
1019
1020
|
|
|
1020
1021
|
## Important global flags, config, and environment
|
|
1021
1022
|
|
|
@@ -1039,6 +1040,8 @@ Browser default config is conservative: it adds agent guidance for signed-in/acc
|
|
|
1039
1040
|
|
|
1040
1041
|
### Browser launch and runtime flags
|
|
1041
1042
|
|
|
1043
|
+
The extension composes stock `--args --no-startup-window` for local Chrome startup without changing the headless default or external browser engines/attachments. Caller arguments retain native precedence. Plain active follow-ups do not repeat bootstrap settings; older custom-argument sessions can restart once when the new argument changes native launch identity. URL-less `open` runs native `get url` to lazily launch or preserve the active page, including batch/script calls; the result exposes the actual command and URL, not a fabricated launch receipt. See [the startup contract](TOOL_CONTRACT.md#headed-and-local-fixture-limits).
|
|
1044
|
+
|
|
1042
1045
|
- `--executable-path <path>`: custom Chromium-compatible browser executable, such as Brave/Edge/Arc/Vivaldi when upstream can launch that binary. Environment: `AGENT_BROWSER_EXECUTABLE_PATH`.
|
|
1043
1046
|
- `--extension <path>`: load browser extensions; repeatable. Environment: `AGENT_BROWSER_EXTENSIONS`.
|
|
1044
1047
|
- `--args <args>`: browser launch args, comma or newline separated. Chromium switches belong in this value, for example `{ "args": ["--args", "--no-sandbox", "open", "https://example.com"], "sessionMode": "fresh" }`. A bare `--no-sandbox` is diagnosed when it occupies the command slot (unknown command) or an `open` / `goto` / `navigate` option position (ignored by upstream); literal operands in other commands are left alone. For batches, put `--args` before `batch` in top-level `args`, not inside a row. Environment: `AGENT_BROWSER_ARGS`.
|
package/docs/RELEASE.md
CHANGED
|
@@ -47,6 +47,8 @@ npm run verify -- pre-pr
|
|
|
47
47
|
3. `npm run verify -- package-pi`, which first validates package contents via `npm pack --json --dry-run` and then smoke-loads the packed package in Pi isolation
|
|
48
48
|
4. `npm run smoke:platform:doctor` and the full Crabbox matrix from [`platform-smoke.md`](platform-smoke.md): macOS SSH, Ubuntu local-container, and native Windows Parallels targets running fast target-local `platform-build` plus `browser-dogfood-smoke`
|
|
49
49
|
|
|
50
|
+
The 0.6.13 release uses the documented [local macOS and hosted native-Windows alternatives](platform-smoke.md#alternate-native-transports) instead of SSH and Parallels. Run and inspect those target-local suites separately alongside the remaining release checks. The default composed gate is unchanged; alternate evidence must not be labeled a passing Crabbox matrix.
|
|
51
|
+
|
|
50
52
|
`npm publish` runs npm’s `prepublishOnly` script from `package.json`, which executes the same `npm run verify -- release` gate and then `npm pack --dry-run`. That concatenated gate is everything in the default `npm run verify` step (generated playbook drift, clean `dist/` build, TypeScript, the unit/fake suite, generated command-reference blocks, and live upstream command-reference sampling against the targeted `agent-browser` on `PATH`), the configured-source lifecycle harness, the packaged Pi smoke in `package-pi`, and the release-blocking Crabbox platform matrix. Using `npm publish --ignore-scripts` skips that contract intentionally.
|
|
51
53
|
|
|
52
54
|
`prepublishOnly` intentionally does **not** run the standalone host-only `npm run verify -- startup-profile`, `npm run verify -- real-upstream`, `npm run verify -- dogfood` modes; those remain separate `npm run verify` modes in [`scripts/project.mjs`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/project.mjs). The platform matrix includes its own fast target-local build/package gate and browser dogfood suite, and is automated through the `release` slice.
|
|
@@ -219,7 +221,7 @@ Run the automated harness for deterministic configured-source lifecycle regressi
|
|
|
219
221
|
npm run verify -- lifecycle
|
|
220
222
|
```
|
|
221
223
|
|
|
222
|
-
The harness creates an isolated `PI_CODING_AGENT_DIR`, writes settings with exactly one temporary configured package source, runs `pi` in `tmux` with `--approve`, default model **`zai/glm-5.2`**, and a deterministic `--session-id`, puts a deterministic fake `agent-browser` first on `PATH`, drives `/reload`, closes Pi, and relaunches with the same exact session id instead of typing `/resume`. It also asserts the JSONL session header id, same-page managed-session continuity, compiled JS code pickup after full process relaunch, persisted spill reachability, and real Pi `tool_result` failure-patch semantics for a QA reclassification. Per-step tmux waits default to **180000 ms** (three minutes) in [`scripts/verify-lifecycle.mjs`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/verify-lifecycle.mjs) (`DEFAULT_TIMEOUT_MS`); override with `--timeout-ms <ms>` when slower models or cold starts need more headroom. Override the model when needed:
|
|
224
|
+
The harness creates an isolated `PI_CODING_AGENT_DIR`, writes settings with exactly one temporary configured package source, runs `pi` in `tmux` with `--approve`, default model **`zai/glm-5.2`**, and a deterministic `--session-id`, puts a deterministic fake `agent-browser` first on `PATH`, starts an explicit `sessionMode: "fresh"` managed browser, drives `/reload`, closes Pi, and relaunches with the same exact session id instead of typing `/resume`. It also asserts the JSONL session header id, same-page managed-session continuity, compiled JS code pickup after full process relaunch, persisted spill reachability, and real Pi `tool_result` failure-patch semantics for a QA reclassification. Per-step tmux waits default to **180000 ms** (three minutes) in [`scripts/verify-lifecycle.mjs`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/verify-lifecycle.mjs) (`DEFAULT_TIMEOUT_MS`); override with `--timeout-ms <ms>` when slower models or cold starts need more headroom. Override the model when needed:
|
|
223
225
|
|
|
224
226
|
```bash
|
|
225
227
|
npm run verify -- lifecycle --model openai-codex/gpt-5.5:minimal
|
|
@@ -272,8 +274,8 @@ This suite requires the installed stable `agent-browser --version` to meet the m
|
|
|
272
274
|
- **Navigation metadata and native tab setup:** plain pages stay quiet; a page with tools retains positive native `data.webmcp` and shows the `webmcp list` hint. New tabs inherit configured headers before their first request, and clearing headers removes them on the next tab. No wrapper inheritance code is involved.
|
|
273
275
|
- **Recording FPS paths and pinning:** the opt-in `test/agent-browser.batch-fidelity.test.ts` native case checks direct/raw/stdin preflight before dispatch, actual start/restart destinations and pinned targets with leading `--fps`, conservative start-ref protection, FPS-only restart continuity, and valid 12-fps WebM output after explicitly logged 12-second fixture captures. Run it with `PI_AGENT_BROWSER_REAL_UPSTREAM=1`; it requires native 0.37 or newer. Keep focused older-native controls for fresh-page starts and the start-then-ref batch latch when changing that policy.
|
|
274
276
|
- **Managed session core and safe diagnostic matrix:** fresh `open` on the contract fixture, then implicit reuse across `eval --stdin`, `snapshot -i`, interaction commands (`click`, `dblclick`, `fill`, `type`, `type --clear --delay`, `focus`, `keyboard` with `type` / `inserttext`, `press`, `hover`, `check`, `uncheck`, `select`, failed `select` no-match, `upload`, `drag`, `mouse`, `scroll`, off-viewport click, `scrollintoview`, `wait` on selectors in the main frame and a selected iframe), extraction (`get` variants, `is` variants, `find label … fill` via native `<label>`, `aria-label`, and `aria-labelledby`, inline `eval`), file outputs (`screenshot`, `pdf`), navigation (`back`, `forward`, `reload`, `tab list`, another `open` to the same fixture), `batch` stdin, `pushstate`, `vitals … --json`, network route/requests/HAR, diff snapshot/screenshot/url, trace/profiler, console/errors/highlight, stream enable/status/disable, and `cookies set --curl`.
|
|
275
|
-
- **Managed restore correctness and persistence:** while the restore-enabled managed daemon is active, assert raw argument and stdin batches containing nested `connect` fail before upstream spawn; a new
|
|
276
|
-
- **Cold first-read continuity:** the focused `contract suite matches cold URL reopen after quit` case runs actual quit cleanup, waits for the exact old daemon to exit, reloads the saved branch, and requests `snapshot -i` before any explicit navigation. Empty storage and origin storage must both reach their remembered non-root URL, report fresh refs in the main frame, and retain storage without pretending to retain unsaved forms or JavaScript memory. Separate deterministic cold-resume/boundary tests cover first `get url` and history commands, non-page daemon-starting calls and batch prefixes, hash routes, pending-state transcript replay, live missing tabs versus explicit URL destinations, close retirement, and caller-owned/attached/restore-disabled boundaries. Those fake-upstream checks do not replace native-browser or real Pi lifecycle qualification of the complete candidate.
|
|
277
|
+
- **Managed restore correctness and persistence:** while the restore-enabled managed daemon is active, assert raw argument and stdin batches containing nested `connect` fail before upstream spawn; a new harness replaying the owned launch result must also reject incompatible reuse of that live same-name daemon. Seed a cookie plus localStorage/sessionStorage, close the first managed browser while a conflicting parent namespace is set, verify the default-namespace daemon actually closed, create a new extension harness with the same cwd, reopen the fixture with `sessionMode: "fresh"`, and assert all three values restore before closing the second browser. On POSIX, separate isolated real-browser launches assert automatic restore stays disabled and no snapshot is written through either a symlinked `sessions` directory or a file symlink in `sessions/.tmp`; a relative `HOME`, untrusted writable HOME ancestry, and a non-Git cwd must fail closed. Verify a checkout rename preserves its generation identity but starts a fresh composite restore key (fail-closed, because the cwd-derived managed-session base name changes), a copied or path-replacement checkout gets a new key, and changing the Git-generation marker between planning and spawn prevents agent-browser from starting. Run two same-identity harnesses concurrently so a compatible launch publishes its daemon policy before a waiting incompatible launch re-inspects and fails without reaching its main spawn; also fail a fresh non-batch command after daemon creation and verify shutdown closes the retained identity.
|
|
278
|
+
- **Cold first-read continuity:** the focused `contract suite matches cold URL reopen after quit` case starts an explicit `sessionMode: "fresh"` managed browser, runs actual quit cleanup, waits for the exact old daemon to exit, reloads the saved branch, and requests `snapshot -i` before any explicit navigation. Empty storage and origin storage must both reach their remembered non-root URL, report fresh refs in the main frame, and retain storage without pretending to retain unsaved forms or JavaScript memory. Separate deterministic cold-resume/boundary tests cover first `get url` and history commands, non-page daemon-starting calls and batch prefixes, hash routes, pending-state transcript replay, live missing tabs versus explicit URL destinations, close retirement, and caller-owned/attached/restore-disabled boundaries. Those fake-upstream checks do not replace native-browser or real Pi lifecycle qualification of the complete candidate.
|
|
277
279
|
- **Failure shape:** `react tree` on a page opened with `--enable react-devtools` but without a React app (expects a clear missing-renderer error with session-bound `details`).
|
|
278
280
|
- **Async download:** `open` on the `/download` fixture, anchor-triggered export, then `wait --download <path>` metadata and wrapper artifact reporting for the requested path.
|
|
279
281
|
|
|
@@ -289,7 +291,7 @@ For a root-check change, run the identical final test against both the pre-fix a
|
|
|
289
291
|
|
|
290
292
|
- **Focused and broad cases:** `test/agent-browser.real-upstream-contract.test.ts` keeps the broad command matrix in one 180-second case and separate 60-second cold-reopen, duplicate-name click-mutation, navigation-availability/tab-setup, and sessionless-plugin cases. The `real-upstream` facade's `contract suite matches` selection includes the broad matrix, cold reopen, click mutation, and navigation setup. The click case uses two native DOM buttons to prove a stale duplicate ordinal cannot contradict trusted target events, while exact XPath probes still detect missing events and native clicks remain dispatch-only evidence.
|
|
291
293
|
- **Output-shape locking:** Expected `details` / `data` keys per step live in `test/fixtures/agent-browser-real-output-shapes.json`, keyed by logical groups (`version`, `rootHelp`, `commandHelp`, `skillsList`, `skillsGetFull`, `skillsPath`, `open`, `eval`, `snapshot`, `coreCommand`, `coreSubcommand`, `coreFileArtifact`, `batch`, `pushstate`, `vitals`, `networkRoute`, `nonCoreStatus`, `nonCoreArtifact`, `diffScreenshotArtifact`, `streamControl`, `streamStatus`, `cookiesCurl`, `reactMissingRenderer`, `waitDownload`). Keep `targetVersion` in that file aligned with `scripts/agent-browser-capability-baseline.mjs`, and extend entries whenever the suite starts asserting on new presentation fields.
|
|
292
|
-
- **Isolation:** The harness
|
|
294
|
+
- **Isolation:** The harness uses a throwaway HOME and screenshot directory, and pairs `PI_AGENT_BROWSER_SOCKET_DIR` with `AGENT_BROWSER_SOCKET_DIR` in a short private socket directory. This keeps owned commands and native policy probes on the same daemon without exceeding Unix socket path limits. Raw click/FPS helpers retain owned-session context; cold-resume observers use the registered tool so they do not restart the browser they are measuring. The harness serves loopback fixtures, closes its sessions and removes only its own directories; it does not reuse your normal profile or socket locations.
|
|
293
295
|
- **React DevTools branch:** After the core matrix, the suite performs another `open` with `--enable react-devtools` and `sessionMode: "fresh"`, then expects `react tree` to fail with a missing-renderer style error on the same non-React contract page. The following download fixture + `wait --download` assertions run against whichever managed session is current after that fresh `open` (typically the React DevTools session), not the original pre-matrix session name.
|
|
294
296
|
|
|
295
297
|
**Troubleshooting**
|
package/docs/SUPPORT_MATRIX.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Current upstream support matrix
|
|
2
2
|
|
|
3
|
+
Root browser groups use native named sessions plus per-root restore, with scoped Chrome profile-name bootstrap from existing package config. `test/agent-browser.shared-defaults.test.ts` covers root separation, descendant routing, exact root-name follow-ups, helper launch consistency, explicit overrides, and quit ownership; the companion pi-subagents tests cover foreground/detached launch and saved revive identity. Native JSON restore supports cookies and web storage, not new IndexedDB-only or memory credentials. Real SDK/native-browser restart and concurrency evidence is required for this boundary; a profile metadata match alone is not login proof.
|
|
4
|
+
|
|
3
5
|
Related docs:
|
|
4
6
|
- [`../README.md`](../README.md)
|
|
5
7
|
- [`../AGENTS.md`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/AGENTS.md) (rebaselining and verification stack)
|
|
@@ -60,15 +62,17 @@ Current summary:
|
|
|
60
62
|
| RQ-0142 | An optional host [`beforeExecute` callback](TOOL_CONTRACT.md#host-execution-hook) uses Pi sequential scheduling and the existing inner-call queue to save state before dispatch, retaining the outer tool-call ID and per-dispatch cancellation (`test/agent-browser.pi-pipeline.test.ts`). Top-level `script` provides bounded one-shot JavaScript loops, branches, and multi-page aggregation through the complete ordinary browser executor. A permissioned child exposes only null-prototype `browser` / `emit` task functions, receives no user-visible host capabilities, serializes at most 25 calls, and uses a unique restore-disabled wrapper session with a durable pre-spawn Pi lease, finally-close, active branch/shutdown abort-and-wait cleanup (including upstream-version preflight), and exact lease recovery. Script-owned helpers/cleanup case-insensitively clear ambient upstream launch/profile/restore/attachment and proxy controls and bypass user/project config; inner `--config` is rejected before dispatch while ordinary `args` retains it; final data is compact-serialized with a second post-redaction byte/depth guard; rehydrated spills reserve IPC-envelope headroom, inner summary/text are bounded, complete response envelopes are size-checked, and script-visible next actions are policy-compatible after isolated identity removal; malformed bridge calls and missing workers fail structurally; policy-rejection and dispatched-failure counters are disjoint. The collapsed Pi call row shows a bounded terminal-safe source preview with visible line-break markers and its expanded state shows the full terminal-safe source with JavaScript line terminators preserved and removed controls marked. It requires persisted Pi sessions, classifies uncaught source exceptions as `script-error`, confirms successful cleanup in compact prose, and deliberately has no named recipe/import/shared-state surface. The same dogfood pass compacted common/QA output, made failure `nextActions` model-visible and exact-namespace/session-stable (including explicit empty namespace overrides), limited close cleanup guidance to existing explicit artifacts, clarified raw batch stdin, normalized empty semantic names, and added bounded timeout/navigation recovery actions. | [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#script), [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#one-shot-script-isolation), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#one-shot-code-mode), `test/agent-browser.script.test.ts` |
|
|
61
63
|
| RQ-0143 | Browser-backed calls require a stable upstream version at or above the configured 0.35.0 floor, report minimum/observed version mismatch before launch, and keep stateless/local diagnostics available; the recommended target and minimum are shared by runtime, docs verification, and package contents. | [`scripts/agent-browser-target.mjs`](../scripts/agent-browser-target.mjs), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0360-rebaseline), `test/agent-browser.upstream-version.test.ts` |
|
|
62
64
|
| RQ-0144 | Managed restore keys are scoped to both checkout generation and Pi transcript so upstream 0.33.2's newest-file-wins loading cannot let concurrent chats clobber or inherit cookies/storage. Fresh rotations, reload, restart, and `/resume` of one transcript retain continuity; after a confirmed inactive daemon, a branch-restored compatible owned session retains its pending reopen through daemon-starting non-page calls and transcript replay, then reopens the complete recorded URL (including its fragment) before current-page operations such as `get url` or `reload`, verifies the actual target and invalidates old refs. This reloads a URL, not forms, JavaScript memory, or history; live wrong-tab recovery never auto-navigates. Passive and explicit upstream configs pass through, while explicit config selection suppresses the wrapper's automatic restore injection. | [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), `test/agent-browser.managed-session-restore.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
63
|
-
| RQ-0145 | Mega-dogfood correctness closes false-positive success paths: URL QA
|
|
65
|
+
| RQ-0145 | Mega-dogfood correctness closes false-positive success paths: URL QA makes matched post-clear page-error evidence non-pass as unverified, counts definitely new errors separately, and waits 150 ms for immediate post-load diagnostics, stale pre-existing artifacts and missing/stale recordings finalized by restart fail verification, aliased destinations (including dangling symlinks, full macOS/Windows Unicode folds, final short/long diff output flags, and same-call `outputPath` aliases) and active recording-path reuse through artifacts, path-bearing HAR stops, long/short or reordered explicit download waits, interspersed-global command forms, Electron cleanup output, or normalized output writes is rejected through an atomic process-wide, transcript-backed namespace/session reservation index independent of bounded manifest eviction; cross-branch close tombstones survive reload and each direct or ordered nested close retires exact identity at that lifecycle point while terminal closes clear attached/ref/page/route state, later lifecycle-proven browser launches—including failed post-close rows and `record stop`—can reactivate/rebuild it without stale pre-close `about:blank` recovery while bounded launch evidence survives failed-step presentation and transcript replay, explicitly non-launching diagnostics preserve the close, missing evidence stays conservatively active even on the first managed call, every successful direct/nested close clears wrapper trace/profiler ownership before later successful rows can rebuild it, and namespace-scoped `close --all` clears all matching managed/attached/page/ref/route/trace/recording ownership, only the newest pending recording path remains authoritative even across same-timestamp restart rows, current recording transitions replay directly without artifact-manifest compatibility inference, definitive direct/batch no-recording stop failures retire stale state at their ordered step, recording starts after nested close are rejected, pending rows coalesce into terminal batch outcomes without retaining an intermediate close-abandoned duplicate after a later saved stop, and any later same-session failure retains exact cleanup actions, same-page getter refs are refreshed, locator select resolves exact current native controls, no-movement scroll fails honestly, and script cleanup always attempts fail-closed close. | [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md), `test/agent-browser.extension-input-modes.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.presentation.test.ts`, `test/agent-browser.presentation-artifacts-batch.test.ts`, `test/agent-browser.recording-reservations.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.script.test.ts` |
|
|
64
66
|
| RQ-0146 | Upstream `agent-browser 0.34.0` rebaseline adds sticky `--pin-tab` / `--no-pin-tab` (`AGENT_BROWSER_PIN_TAB`) for shared CDP/auto-connect sessions, CDP target ids as tab refs, and `tab_gone` recovery data (`data.targetId`, optional `data.lastUrl`). The wrapper parses those flags as optional global booleans rather than launch-scoped, classifies `tab_gone` as `failureCategory: "tab-gone"` with `list-tabs-after-tab-gone` / `open-tab-after-tab-gone`, and shows `targetId` in `tab list` presentation. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0340-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), `test/agent-browser.results.test.ts`, `test/agent-browser.runtime.test.ts` |
|
|
65
|
-
| RQ-0147 | Native follow-ups preserve profiled and other launch-configured browser pages by omitting wrapper compatibility launch arguments while the daemon is active; daemon inspection reapplies the retained compatibility user agent only when relaunch is required. Caller `--args
|
|
67
|
+
| RQ-0147 | Local Chrome composes stock `--no-startup-window` with native launch defaults while headless remains the default; URL-less open uses native lazy URL reads across direct/batch/script paths. Existing root/owned daemon probes and explicit-local session info gate bootstrap, without tab cleanup or a launcher/state journal. See `test/agent-browser.stock-startup.test.ts` for stock browser PID/page/storage checks. Native follow-ups preserve profiled and other launch-configured browser pages by omitting wrapper compatibility launch arguments while the daemon is active; daemon inspection reapplies the retained compatibility user agent only when relaunch is required. Caller `--args` retain their values after the startup default and native precedence; config, file access, and explicit local navigation remain native-owned, and cancellation wins over policy validation before spawn. | [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), `test/agent-browser.runtime.test.ts`, `test/agent-browser.process.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
66
68
|
| RQ-0148 | Upstream 0.35.0 adds Linux Chromium private CA trust and the `protected-vercel-deployments` skill. The wrapper parses CA set/clear globals, requires fresh managed launches, suppresses automatic managed restore for CA-enabled sessions, passes caller certificate paths upstream unchanged, and uses 0.35.0 as its runtime floor. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0350-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), `test/agent-browser.runtime.test.ts`, `test/agent-browser.managed-session-restore.test.ts`, `test/agent-browser.page-target-validation.test.ts` |
|
|
67
69
|
| RQ-0149 | Upstream 0.35.1 fixes snapshot-diff ref lifecycle, active-main-frame stream URL updates, and Windows ARM64 launcher selection without adding commands or flags. The wrapper preserves the 0.35.0 stable floor and accepts newer stable versions without version-specific shims. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0351-rebaseline), [`scripts/agent-browser-target.mjs`](../scripts/agent-browser-target.mjs), `test/agent-browser.upstream-version.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
68
70
|
| RQ-0150 | Upstream 0.35.2 hardens standalone dashboard origin access and fixes root remote CDP WebSocket query URLs. The wrapper parses `--allowed-origins` as a value and keeps explicit or implicit dashboard-start lifecycle calls sessionless; upstream owns origin validation, tokens, and the CDP fix. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0352-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#dashboard-reverse-proxy-origins), `test/agent-browser.runtime.test.ts` |
|
|
69
71
|
| RQ-0151 | Upstream 0.36.0 adds experimental WebMCP page tools, default local-Chrome enablement with launch-scoped `--no-webmcp`, an opt-in MCP profile, and the `webmcp-gen` skill. The wrapper passes list/invoke/result/cancel through, preserves params/frame/detach/timeout arguments, derives watchdogs from effective direct/raw/stdin commands, keeps pending detached targets unverified, and restores batch target/ref state only after URL verification and a fresh snapshot, while keeping 0.35.0 as the stable floor. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0360-rebaseline), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md#experimental-webmcp), `test/agent-browser.runtime.test.ts`, `test/agent-browser.wait-timeouts.test.ts`, `test/agent-browser.process.test.ts`, `test/agent-browser.page-target-validation.test.ts`, `test/agent-browser.extension-ref-guards.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
70
72
|
| RQ-0152 | Agent feedback closes recovery and evidence gaps without a new runtime layer: unknown-target timeouts return a visibly executable `get url` → `snapshot -i` fail-fast action while dialog recovery remains allowed; successful tab selection/close live-verifies URL/fresh titles and retains deliberate and post-close blank tabs; compacted direct or batched `outputPath` writes use only full command-redacted live wrapper spills and fail rather than save metadata; explicit URL reads/all-read batches avoid browser helpers and managed replacement, while proven native read confirmations preserve exact session routing and capability-gated browser independence; single session-info preflights distinguish daemon/browser/native/Pi ownership; native recording receipts expose capture versus encoded measurements and survive failure/timeout exports, with bounded identity-matched recovery and no file-only success; direct or batched first/fresh local headed launches expose an unverified `browserWindow` handoff while attachments do not; snapshot spills and SAML/OAuth URL values are redacted at model/persistence boundaries; unsupported direct/batch `scrollintoview text=...` fails before upstream false success with visible native recovery payloads while help passes through; and web search sends Exa primary/version guidance, removes exact normalized-URL duplicates without overfetch, and labels provider page dates without claiming crawl/version proof. | [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md), [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), `test/agent-browser.extension-errors-artifacts.test.ts`, `test/agent-browser.extension-tabs.test.ts`, `test/agent-browser.extension-validation.test.ts`, `test/agent-browser.page-target-validation.test.ts`, `test/agent-browser.runtime.test.ts`, `test/agent-browser.snapshot-presentation.test.ts`, `test/agent-browser.web-search.test.ts`, `test/agent-browser.real-upstream-contract.test.ts` |
|
|
71
73
|
|
|
74
|
+
QA truthfulness (#121, RQ-0145) is covered by the registered-tool controls in `test/agent-browser.extension-input-modes.test.ts` and the strict `contract suite matches QA non-pass after same-URL error-buffer rollover` case in `test/agent-browser.real-upstream-contract.test.ts`. The real fixture fills the native error FIFO with identical rows, then verifies that a new document actually throws again at the same URL and that QA is always non-pass. Matched rows fail as an unverified check, not an application-error assertion. Empty/cleared and explicit `checkErrors: false` controls preserve valid passes; the wrapper does not fix upstream `errors --clear` or accept a warning-only pass as resolution.
|
|
75
|
+
|
|
72
76
|
Browser evidence (RQ-0152) is covered by `test/agent-browser.browser-evidence.test.ts`, `test/agent-browser.read-confirmation.test.ts`, `test/agent-browser.recording-receipts.test.ts` and `test/agent-browser.recording-recovery.test.ts`, alongside existing presentation, reservation/replay and lifecycle checks. They cover cold/shared/unknown/invalid/timeout reads, native-default and shared confirmation routing, capability absence and script identity boundaries, separate daemon/browser/Pi identity, sparse/legacy capture metrics, native failure retention, stale/same-path mismatches, one-query stop recovery, error exports and artifact aliases. Full live identity, receipt lookup and browser-independent native read/confirm handling require companion upstream fixes; missing fields remain unknown, and native receipts do not survive daemon exit. These source checks do not replace paired native/Pi runtime verification.
|
|
73
77
|
|
|
74
78
|
Contributor fixes #133/#152 remove unused prompt suffix entries without changing runtime guidance and diagnose bare `--no-sandbox` only in the command slot or navigation option positions. Native `--args` values and literal operands remain intact; batch checks use raw effective rows without treating row-local `--args` as a launch setting. `test/agent-browser.chromium-args.test.ts` covers pre-dispatch rejection, literal/flag-value controls, inspection, raw/stdin batch precedence and dispatcher outcomes. Existing prompt/grammar checks remain; source checks do not replace native-product gates.
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -174,6 +174,10 @@ Upstream 0.35.2 adds `dashboard start --allowed-origins <origins>` and `AGENT_BR
|
|
|
174
174
|
|
|
175
175
|
## Headed and local fixture limits
|
|
176
176
|
|
|
177
|
+
Local Chrome uses stock `--args --no-startup-window` at bootstrap and composes it consistently with caller-configured Chrome arguments (CLI overrides environment, which overrides native config). Plain active-session follow-ups do not resend bootstrap settings. A pre-existing custom-argument browser may restart once because stock Chrome launch arguments form part of native session identity; there is no migration or launch-settings journal. Headless remains the default. CDP/auto-connect, providers, Electron, and Lightpanda do not receive this Chrome default. The wrapper does not delete tabs or edit profiles.
|
|
178
|
+
|
|
179
|
+
URL-less `open` is executed as native `get url`, including effective raw/stdin batch rows and script calls. This lazily opens a missing browser without navigating an existing page or issuing a second incomplete launch. `details.args` retains the request; `effectiveArgs`, `command`, native URL data and lifecycle show what ran. No launch receipt is synthesized. Explicit URLs, required-URL errors for `goto`/`navigate`, and top-level help/version argv keep their native meanings. Ignored batch stdin remains unchanged.
|
|
180
|
+
|
|
177
181
|
- Upstream 0.35.0 and newer require separate `args` entries for global flag values. The wrapper rejects `--flag=value` global tokens before normal command dispatch, including trailing tokens; `--restore=<key>` is the explicit upstream-supported exception. Plain help/version inspection preserves exact caller argv, matching upstream. These exceptions are top-level only: global flags for `batch` belong before `batch`, and row-local equals forms fail validation before dispatch.
|
|
178
182
|
- A bare `--no-sandbox` in the command slot, or an option position after `open` / `goto` / `navigate`, fails validation before dispatch with Chromium launch-argument guidance. The former is an unknown upstream command; navigation ignores the latter. Pass the switch as a separate `--args` value and use `sessionMode: "fresh"` for launch changes. For native batches, `--args` belongs on the outer call before `batch`, not in a row. Other commands keep their native literal text, script, select, path and option-value handling; top-level help/version inspection remains unchanged.
|
|
179
183
|
- `--headed` is an upstream global flag passed through `args` (for example `{ "args": ["--headed", "open", "https://example.com"], "sessionMode": "fresh" }`). Use it on the first launch for demos, human-observed QA, or a user-completed login. If a managed browser session already exists, use `sessionMode: "fresh"` so the launch-scoped headed/headless choice is not ignored. Wrapper-owned headed launches default `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` to `0` because upstream 0.33.2's multi-origin restore collector opens visible temporary tabs and can delay session policy inspection. The wrapper records the effective launch-time interval and reapplies it to every helper and follow-up subprocess, still-owned off-current session, Electron cleanup close, and transcript reload/resume 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 an explicit interval before launch when periodic preservation matters. Because upstream reads it when the daemon starts, changing the recorded effective interval in either direction on a running wrapper-owned headed session is rejected until close plus a fresh launch.
|
|
@@ -197,7 +201,7 @@ Upstream 0.35.2 adds `dashboard start --allowed-origins <origins>` and `AGENT_BR
|
|
|
197
201
|
- For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard type for framework-controlled editors that require real key events. keyboard inserttext is paste-like and can change a DOM value without updating application state, so use it only when later application-state evidence proves the edit was accepted. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.
|
|
198
202
|
- 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.
|
|
199
203
|
- 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.
|
|
200
|
-
-
|
|
204
|
+
- 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.
|
|
201
205
|
- When using launch-scoped flags (--auto-connect, --allowed-domains, --namespace, --cdp, --ca-cert, --no-ca-cert, --enable, --executable-path, --webgpu, --no-webmcp, --init-script, --idle-timeout, --args, --user-agent, --headed, --device, --profile, --provider, -p, --session-name, --restore, --restore-save, --restore-check-url, --restore-check-text, --restore-check-fn, --state), 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.
|
|
202
206
|
- Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.
|
|
203
207
|
- 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.
|
|
@@ -433,15 +437,15 @@ Because `job` still executes as upstream `batch` with generated stdin, the same
|
|
|
433
437
|
- type: object with either required `url` (normal URL-opening QA) or `attached: true` (current attached-session QA)
|
|
434
438
|
- optional; mutually exclusive with `script`, `args`, `semanticAction`, `job`, `sourceLookup`, `networkSourceLookup`, and `electron`
|
|
435
439
|
- lightweight preset built on the same batch compiler path as `job`, using `batch --bail` so missing readiness/text/selector assertions stop before slower diagnostics can burn the wrapper watchdog
|
|
436
|
-
- URL form: clears enabled network/console diagnostic buffers first and snapshots any page-error residue after `errors --clear` (
|
|
440
|
+
- URL form: clears enabled network/console diagnostic buffers first and snapshots any page-error residue after `errors --clear` (native clear can leave residue), then opens `url`, waits with `wait --load <state>` using the resolved `loadState`, adds a bounded 150 ms diagnostic settle when console or page-error checks are enabled so immediate post-load callbacks can report, optionally asserts `expectedText` (string or string array, compiled to bounded visible-text `wait --fn … --timeout 5000` predicates after load) and/or `expectedSelector` (each may be omitted for a load-plus-diagnostics-only smoke), then runs enabled diagnostics: `network requests`, `console`, and `errors` only if preceding batch steps pass. Successful reset-step rows are labeled as reset output. Final page-error rows that match a nonempty post-clear baseline make the check unverified and fail QA; identical rows do not prove that no new error occurred. Multiset subtraction still identifies definitely new rows, which count as page errors. Failed reset commands still fail the batch.
|
|
437
441
|
- URL QA uses its explicitly requested navigation without requiring the previous tab; diagnostic clears remain before `open`. This does not reopen remembered pages for ordinary reads or `qa.attached`.
|
|
438
442
|
- attached form: `qa: { attached: true, expectedText?, expectedSelector?, screenshotPath?, checkNetwork?, checkConsole?, checkErrors?, loadState? }` runs the same waits (including the bounded diagnostic settle when needed), optional assertions, diagnostics, and screenshot against the current attached managed session without opening a URL. It rejects `url` and cannot be used with `sessionMode: "fresh"`; attach first with `electron.launch` or raw `args: ["connect", "<port-or-url>"]`, then run `qa.attached`. Before spawning the diagnostic batch, the wrapper preflights the attached session: `get url` must succeed and return a non-empty page URL. Missing URLs and read failures fail fast with `failureCategory: "validation-error"`, `details.validationError`, and recovery `nextActions` such as `list-tabs-before-qa-attached` and `snapshot-before-qa-attached` instead of running the full QA batch. `file:`, custom-scheme, and other attached targets are accepted. Attached QA does **not** run `network requests --clear`, `console --clear`, or `errors --clear`; `details.compiledQaPreset.checks.diagnosticsResetAtStart` is `false`. Visible text warns that existing diagnostic buffers were preserved only when `checkNetwork`, `checkConsole`, or `checkErrors` is enabled, and those diagnostics may include events from before the QA check.
|
|
439
443
|
- `loadState` is optional and must be `domcontentloaded`, `load`, or `networkidle`; it defaults to `domcontentloaded` so analytics-heavy or long-polling pages do not hang routine QA. Use `networkidle` only when the site is expected to go fully quiet.
|
|
440
|
-
- `checkNetwork`, `checkConsole`, and `checkErrors` default to `true` for URL-opening QA; for `qa.attached` they default to `false` because preserved upstream buffers may predate the current check. Set a field to `true` on `qa.attached` to opt into preserved-buffer diagnostics.
|
|
444
|
+
- `checkNetwork`, `checkConsole`, and `checkErrors` default to `true` for URL-opening QA; for `qa.attached` they default to `false` because preserved upstream buffers may predate the current check. Set a field to `true` on `qa.attached` to opt into preserved-buffer diagnostics. Explicit `checkErrors: false` skips error clearing, baseline collection, and the final error read; an otherwise healthy limited check can pass.
|
|
441
445
|
- optional `screenshotPath` adds an evidence screenshot step
|
|
442
446
|
- reports `details.compiledQaPreset` with the compiled batch plan and resolved `loadState`, plus `details.qaPreset` with `{ passed, failedChecks, warnings, summary }`
|
|
443
|
-
- on success with no failed checks, model-visible prose collapses to a compact pass summary (current page URL/title when known, checks run, optional screenshot path plus artifact verification, pointer to `details.qaPreset` and `details.batchSteps` for the full matrix). Failed QA
|
|
444
|
-
- fails the native tool result with `failureCategory: "qa-failure"` when diagnostics report page errors, console error messages, actionable failed network requests, any batch step failure, or missing expected text after the requested load state. Expected-text QA checks use bounded visible-element predicates plus fail-fast batch behavior instead of reading all body text, so dense pages can pass on visible headings/copy and missing text reports `expected text not found: …` rather than burning the full wrapper watchdog. Benign classification (implementation: `classifyNetworkRequestFailure` → `isBenignAssetFailure` in `extensions/agent-browser/lib/results/network.ts`) applies only when the row is already treated as failed (`status >= 400`, `failed: true`, or a string `error`—see `isFailedNetworkRequest`), the URL path’s last segment matches the icon basename heuristic (`favicon` plus `.ico`/`.png`/`.svg`, or `apple-touch-icon` plus `.png`, each allowing an optional `[-.\w]*` stem suffix before the extension), **and** at least one of `status === 404`, `failed === true`, or `typeof request.error === "string"` holds (so a **status-only** failure such as `500` on that path with neither `failed` nor a string `error` stays actionable). It also requires the upstream `resourceType` / `mimeType` (whichever is present) to be absent or look image-like: `image`, `img`, `other`, or a value starting with `image/`. Those rows are counted in `qaPreset.warnings` (for example `N benign network request failure(s) ignored`) and omitted from the actionable failed-network tally; every other failed request stays actionable.
|
|
447
|
+
- on success with no failed checks, model-visible prose collapses to a compact pass summary (current page URL/title when known, checks run, optional screenshot path plus artifact verification, pointer to `details.qaPreset` and `details.batchSteps` for the full matrix). Failed QA uses a compact failure summary with reasons in `failedChecks`; the full diagnostic matrix remains in `details.batchSteps`.
|
|
448
|
+
- fails the native tool result with `failureCategory: "qa-failure"` when diagnostics report definitely new page errors, an unverified page-error check, console error messages, actionable failed network requests, any batch step failure, or missing expected text after the requested load state. Expected-text QA checks use bounded visible-element predicates plus fail-fast batch behavior instead of reading all body text, so dense pages can pass on visible headings/copy and missing text reports `expected text not found: …` rather than burning the full wrapper watchdog. Benign classification (implementation: `classifyNetworkRequestFailure` → `isBenignAssetFailure` in `extensions/agent-browser/lib/results/network.ts`) applies only when the row is already treated as failed (`status >= 400`, `failed: true`, or a string `error`—see `isFailedNetworkRequest`), the URL path’s last segment matches the icon basename heuristic (`favicon` plus `.ico`/`.png`/`.svg`, or `apple-touch-icon` plus `.png`, each allowing an optional `[-.\w]*` stem suffix before the extension), **and** at least one of `status === 404`, `failed === true`, or `typeof request.error === "string"` holds (so a **status-only** failure such as `500` on that path with neither `failed` nor a string `error` stays actionable). It also requires the upstream `resourceType` / `mimeType` (whichever is present) to be absent or look image-like: `image`, `img`, `other`, or a value starting with `image/`. Those rows are counted in `qaPreset.warnings` (for example `N benign network request failure(s) ignored`) and omitted from the actionable failed-network tally; every other failed request stays actionable.
|
|
445
449
|
|
|
446
450
|
Example:
|
|
447
451
|
|
|
@@ -449,6 +453,16 @@ Example:
|
|
|
449
453
|
{ "qa": { "url": "https://example.com", "expectedText": "Example Domain", "screenshotPath": ".dogfood/qa-example.png" } }
|
|
450
454
|
```
|
|
451
455
|
|
|
456
|
+
A nonempty post-clear baseline is not reliable event identity. A full native FIFO buffer can replace old errors with identical new errors while both reads remain identical. Matched final rows therefore add “page-error check could not be verified” to `failedChecks`, set `qaPreset.passed: false`, and take the existing `qa-failure` / Pi `isError: true` path. This is an evidence failure, not proof that the application emitted those matched rows during QA; even a genuinely clean page can be non-pass when residue cannot be cleared. The wrapper does not reset the browser, add event-correlation heuristics, or repair native clearing.
|
|
457
|
+
|
|
458
|
+
| Error evidence with `checkErrors` enabled | QA outcome when other checks pass |
|
|
459
|
+
| --- | --- |
|
|
460
|
+
| Empty post-clear baseline and no final errors | Pass |
|
|
461
|
+
| No final errors, even if the baseline was nonempty | Pass |
|
|
462
|
+
| Final rows not accounted for by the baseline | Non-pass: page errors |
|
|
463
|
+
| Final rows match the nonempty baseline | Non-pass: page-error check could not be verified |
|
|
464
|
+
| Both novel and matched final rows | Non-pass: page errors and an unverified check |
|
|
465
|
+
|
|
452
466
|
Use custom `job` or raw `batch` for QA flows that need custom commands, flags, auth setup, HAR capture, or project-specific assertions.
|
|
453
467
|
|
|
454
468
|
### `electron`
|
|
@@ -694,7 +708,7 @@ Example:
|
|
|
694
708
|
|
|
695
709
|
### `sessionMode`
|
|
696
710
|
|
|
697
|
-
A native `session` default from user/project JSON or `AGENT_BROWSER_SESSION` selects a caller-owned browser for `args`, `semanticAction`, `job`, `qa`, and lookups. Per-call flags still win. Like literal `--session`, this selection takes precedence over `fresh`; it reports `usedImplicitSession: false` and is not closed when Pi quits. Without a selected native session, the
|
|
711
|
+
A native `session` default from user/project JSON or `AGENT_BROWSER_SESSION` selects a caller-owned browser for `args`, `semanticAction`, `job`, `qa`, and lookups. Per-call flags still win. Like literal `--session`, this selection takes precedence over `fresh`; it reports `usedImplicitSession: false` and is not closed when Pi quits. Without a selected native session, attachment, or explicit fresh launch, auto calls select a stable root-Pi named session and root-specific native restore key. `PI_SUBAGENT_ROOT_SESSION_ID` carries the parent identity to descendants; ordinary new/fork/clone roots differ, while resume/cwd changes keep identity. Root sessions report `usedImplicitSession: false` and survive parent/child quit. Global/override package profile names with `policy: "always"` and executable defaults bootstrap only inactive automatic roots. Active roots preserve native launch settings/current restore key, including matching root-name follow-ups; unrelated explicit browsers are unchanged. Profile paths and project-only profile config remain advisory. Native restore covers cookies and web storage, not newly created IndexedDB or memory-only authentication. See the README for communal bootstrap and persistent-profile fallback. Active older/fresh managed sessions retain the lifecycle below. `qa.attached` can inspect this shared current session using the same live target checks. See [shared browser defaults](../README.md#shared-browser-defaults).
|
|
698
712
|
|
|
699
713
|
- type: `"auto" | "fresh"`
|
|
700
714
|
- optional
|
|
@@ -896,7 +910,7 @@ Additional structured fields can appear when relevant:
|
|
|
896
910
|
- `compiledSourceLookup` when the call used `sourceLookup`: `{ args: ["batch"], stdin, steps, query }` with the generated local-evidence plan and original query fields (`selector?`, `reactFiberId?`, `componentName?`, `includeDomHints?`, `maxWorkspaceFiles?`).
|
|
897
911
|
- `sourceLookup` when the call used `sourceLookup`: `{ status, candidates, limitations, summary, workspaceRoot?, electronContext? }`; wrapper-tracked packaged Electron no-candidate diagnostics may carry `workspaceRoot` plus `electronContext` and live Electron nextActions without marking the successful batch as a tool failure.
|
|
898
912
|
- `compiledNetworkSourceLookup` / `networkSourceLookup` when the call used `networkSourceLookup`: the generated batch plan plus bounded failed-request/candidate evidence as described above.
|
|
899
|
-
- `qaPreset` when the call used `qa`: `{ passed, failedChecks, warnings, summary }`. Network rows inside the `network requests` batch step use `summarizeNetworkFailures` / `classifyNetworkRequestFailure` in `network.ts`: actionable failures appear in `failedChecks` (and fail the tool when the upstream batch still succeeded); benign icon-classified failures appear only in `warnings` and in `summary` as `QA preset passed with warnings: …` when nothing else failed.
|
|
913
|
+
- `qaPreset` when the call used `qa`: `{ passed, failedChecks, warnings, summary }`. `failedChecks` includes “page-error check could not be verified” when final errors match a nonempty post-clear baseline; this sets `passed: false` without claiming those matched rows are new application errors. Definitely new rows are counted separately as page errors. Network rows inside the `network requests` batch step use `summarizeNetworkFailures` / `classifyNetworkRequestFailure` in `network.ts`: actionable failures appear in `failedChecks` (and fail the tool when the upstream batch still succeeded); benign icon-classified failures appear only in `warnings` and in `summary` as `QA preset passed with warnings: …` when nothing else failed.
|
|
900
914
|
- `networkRouteDiagnostics` after successful `network requests` when the wrapper has observed active `network route` patterns for that session and a matching request row is pending/no-status or carries CORS/preflight-looking error text. Each row includes `reason` (`"pending-routed-request"` or `"cors-likely-routed-request"`), `routePattern`, `mode`, optional `requestId`, optional `requestUrl`, and `summary`; visible text starts with `Network route diagnostics`, and `details.nextActions` prepends executable route-mock inspection/HAR follow-ups before generic request follow-ups.
|
|
901
915
|
- `compiledElectron` when the call used `electron`: redacted action plan for `list`, `launch`, `status`, `cleanup`, or `probe`.
|
|
902
916
|
- `electron` when the call used `electron`: action-specific lifecycle, discovery, probe, and cleanup data; see the `electron` section below.
|
package/docs/platform-smoke.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-agent-browser-native` uses a Crabbox-backed local platform smoke gate to prove the package on macOS, Ubuntu Linux, and native Windows before release.
|
|
4
4
|
|
|
5
|
-
This is a release-blocking gate. Missing
|
|
5
|
+
This is a release-blocking gate. Missing setup is not a skipped pass. When a maintainer approves an [alternate native transport](#alternate-native-transports), it must prove the same target-local suites; missing upstream `agent-browser` or browser dependencies still blocks that target.
|
|
6
6
|
|
|
7
7
|
## Required release gate
|
|
8
8
|
|
|
@@ -36,6 +36,14 @@ npm run verify -- platform-smoke run --target ubuntu --suite platform-build
|
|
|
36
36
|
| `ubuntu` | `local-container` | POSIX shell in a Docker-compatible local container | Required |
|
|
37
37
|
| `windows-native` | `parallels` | native Windows PowerShell over OpenSSH | Required |
|
|
38
38
|
|
|
39
|
+
## Alternate native transports
|
|
40
|
+
|
|
41
|
+
For the 0.6.13 release, local macOS execution replaces localhost SSH, and the [Native Windows workflow](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/windows-native.yml) runs the existing Windows suites on a GitHub-hosted Windows runner. These are native platform checks, not Crabbox SSH or Parallels passes. The default `release` and `prepublishOnly` commands still select the Crabbox matrix; record alternate suite evidence separately rather than reporting those commands as passed.
|
|
42
|
+
|
|
43
|
+
On macOS, run the unchanged commands returned by `buildPlatformBuildCommand('macos', 'pi-agent-browser-native', 24)` and `buildBrowserDogfoodCommand('macos', '0.37.0', true)` in [`scripts/platform-smoke/targets.mjs`](../scripts/platform-smoke/targets.mjs), serially in a clean private source copy. Use private HOME, npm and Pi settings, and headless browser profiles. No Remote Login or host security change is required.
|
|
44
|
+
|
|
45
|
+
Retain the exact source head/tree, package and tool versions, native OS/architecture, generated commands, stdout/stderr, every `PLATFORM_*` exit marker, actual Pi registration output, dogfood JSON and verified screenshot. Check the suite assertions below and clean up only the run's processes and temporary files. Hosted Windows uploads this evidence from one bounded job; it does not upload installed projects, dependencies or browser profiles. These alternatives do not waive Ubuntu or any non-platform release check.
|
|
46
|
+
|
|
39
47
|
## Required environment
|
|
40
48
|
|
|
41
49
|
Install Crabbox on the macOS maintainer host and keep it on `PATH`:
|
|
@@ -105,7 +113,7 @@ Each required target runs `platform-build` and `browser-dogfood-smoke` on one Cr
|
|
|
105
113
|
### `browser-dogfood-smoke`
|
|
106
114
|
|
|
107
115
|
1. Run `npm ci` in the synced checkout if needed.
|
|
108
|
-
2. Run the deterministic model-free browser smoke through `scripts/verify-agent-browser-dogfood.ts
|
|
116
|
+
2. Run the deterministic model-free browser smoke through `scripts/verify-agent-browser-dogfood.ts`, starting with `sessionMode: "fresh"` so every browser stays in the test-owned managed lifecycle.
|
|
109
117
|
3. Exercise native wrapper surfaces against the deterministic loopback HTTP fixture from `scripts/verify-agent-browser-dogfood.ts`: top-level `qa`, `semanticAction`, constrained `job`, screenshot artifact verification, and session close.
|
|
110
118
|
4. Persist the dogfood JSON report and stdout/stderr evidence.
|
|
111
119
|
5. Fail on missing browser artifacts, failed tool calls, leaked secrets, or unclosed sessions.
|
package/package.json
CHANGED