pi-agent-browser-native 0.6.15 → 0.6.16
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 +22 -0
- package/README.md +12 -0
- package/dist/extensions/agent-browser/index.js +79 -4
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +21 -6
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +47 -11
- package/dist/extensions/agent-browser/lib/process-identity.js +2 -1
- package/dist/extensions/agent-browser/lib/process.js +56 -20
- package/dist/extensions/agent-browser/lib/runtime.js +1 -1
- package/dist/extensions/agent-browser/lib/windows-stock-launcher.js +70 -0
- package/docs/ARCHITECTURE.md +12 -1
- package/docs/ELECTRON.md +4 -2
- package/docs/RELEASE.md +35 -1
- package/docs/SUPPORT_MATRIX.md +12 -2
- package/docs/TOOL_CONTRACT.md +2 -0
- package/docs/platform-smoke.md +5 -3
- package/package.json +10 -6
- package/scripts/config.mjs +1 -1
- package/scripts/doctor.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.16 - 2026-09-21
|
|
6
|
+
|
|
7
|
+
### Qualification
|
|
8
|
+
|
|
9
|
+
- The owner waived Windows qualification for this rollout on 2026-09-21; Windows failures remain visible and nonblocking, not passed. Linux/macOS, Node floors, official/fork consumer checks and all three final reviewer approvals remain required before merge, release or activation.
|
|
10
|
+
- Track unresolved Windows policy-lock and restored Electron cleanup failures in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191). The unchanged Electron repeat passed once without a product patch; its original cause remains unknown. Earlier passing focused Windows controls are bounded evidence, not full-suite qualification. Failed private Windows candidates are not integrated.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Validate explicit Windows socket directories using native directory semantics instead of requiring POSIX ownership metadata.
|
|
15
|
+
- Start the config CLI correctly on Windows and encoded or symlinked paths using Node's native entrypoint identity, while keeping module imports silent.
|
|
16
|
+
- Redact bearer credentials followed by Windows path separators in diagnostic text.
|
|
17
|
+
- Await Windows process-tree termination before returning from browser timeouts or cancellation.
|
|
18
|
+
- Preserve literal newlines through agent-browser's recognized global npm Windows CMD launcher by invoking that installation's native executable directly; custom and unrecognized launchers retain their existing behavior.
|
|
19
|
+
- Allow ten seconds for native Windows process-identity queries, including PowerShell startup, while preserving the identity format and five-second POSIX budget.
|
|
20
|
+
- Await tracked Electron process exit before removing its profile and reporting cleanup complete.
|
|
21
|
+
- Inspect native Windows process command lines when verifying ownership of restored Electron launches.
|
|
22
|
+
- Recognize native Windows absolute checkout paths when checking for duplicate extension sources.
|
|
23
|
+
- Recheck released managed-session policy claims after PID inspection so a completed owner does not leave a stale busy result.
|
|
24
|
+
- Refresh transient choosing tickets so a later managed-session contender cannot block its predecessor after publishing its ticket.
|
|
25
|
+
- Retry native Windows open-file rename conflicts during owned policy-claim cleanup, with token revalidation and a fresh one-second retry window even after long browser shutdowns.
|
|
26
|
+
|
|
5
27
|
## 0.6.15 - 2026-09-18
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ A Pi extension that lets coding agents drive real browser sessions with a native
|
|
|
4
4
|
|
|
5
5
|
It is for Pi users who want agents to browse sites, inspect pages, click through flows, capture screenshots, use persistent profiles, and handle authenticated web apps without spending context on `agent-browser` CLI ceremony.
|
|
6
6
|
|
|
7
|
+
## Pi release qualification
|
|
8
|
+
|
|
9
|
+
Development host dependencies are pinned to official Pi **0.86.1**, with wildcard runtime peers. `npm run check:compat` checks the installed candidate SDK/CLI identity, typechecks, runs the existing offline tests (including the native Pi pipeline with a controlled provider and fake browser), and tests a packed runtime-only installation through both the SDK and the actual bundled RPC CLI. The fork lane additionally requires the native idle checkpoint hook. It does not substitute a fork SDK while leaving official types or child CLI installed.
|
|
10
|
+
|
|
11
|
+
Run in an empty HOME with a **short** TMPDIR outside your real home. Long temporary paths can exceed macOS's 103-byte Unix socket limit; keep the fixture's socket safety checks intact. Real-browser/checkpoint controls, live upstream help sampling, lifecycle dogfood, and platform qualification remain the separate gates documented below. A Pi-only compatibility run uses no live browser profiles, credentials, or agent-browser fork, and does not certify those external integrations.
|
|
12
|
+
|
|
13
|
+
For the 0.6.16 compatibility rollout, the owner explicitly waived Windows qualification on 2026-09-21. Windows diagnostics and failures remain visible but nonblocking; this is **not** a Windows full-suite pass. Linux/macOS, declared Node floors, official/fork host and consumer-artifact checks remain required. Known Windows failures and follow-up are tracked in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191); see the [current support evidence](docs/SUPPORT_MATRIX.md#current-0616-rollout).
|
|
14
|
+
|
|
7
15
|
## Source-of-truth map
|
|
8
16
|
|
|
9
17
|
Start here for install and common usage. For deeper work, use the active docs by purpose:
|
|
@@ -639,6 +647,10 @@ Policy-required URL reads keep the two-call read → confirm/deny flow. Returned
|
|
|
639
647
|
|
|
640
648
|
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.
|
|
641
649
|
|
|
650
|
+
### Native working-session checkpoints
|
|
651
|
+
|
|
652
|
+
On Pi hosts with `session_checkpoint`, an unused or cleanly closed browser integration can qualify for sleep. Live browsers, attachments, scripts, unfinished recordings/cleanup leases, and dirty recording persistence keep compute alive. Pi core owns native journal flush/repair and strict restore; an ongoing journal I/O failure rejects checkpoint acquisition. Checkpointing never closes browsers or changes cleanup ownership. Native save-on-close and per-root restore remain the persistence path; page JavaScript, unsaved forms, and live recordings are not serialized. See [checkpoint boundaries](docs/ARCHITECTURE.md#native-checkpoint-boundary).
|
|
653
|
+
|
|
642
654
|
### Profile selection
|
|
643
655
|
|
|
644
656
|
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.
|
|
@@ -9,11 +9,12 @@ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemer
|
|
|
9
9
|
import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
10
10
|
import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
|
|
11
11
|
import { needsManagedSession } from "./lib/command-policy.js";
|
|
12
|
-
import { ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
|
|
12
|
+
import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
|
|
13
13
|
import { isRecord } from "./lib/parsing.js";
|
|
14
14
|
import { runAgentBrowserProcess } from "./lib/process.js";
|
|
15
15
|
import { getAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
|
|
16
16
|
import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
|
|
17
|
+
import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
|
|
17
18
|
import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
|
|
18
19
|
import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
|
|
19
20
|
import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
|
|
@@ -21,9 +22,8 @@ import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
|
|
|
21
22
|
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
22
23
|
import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
|
|
23
24
|
import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
|
|
24
|
-
import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
|
|
25
25
|
import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination, getRecordContactSheetDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
|
|
26
|
-
import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
|
|
26
|
+
import { findElectronLaunchRecordForSession, getActiveElectronRecords, getSessionContextKey } from "./lib/orchestration/browser-run/session-state.js";
|
|
27
27
|
import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
|
|
28
28
|
import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
|
|
29
29
|
import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
|
|
@@ -571,7 +571,7 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
571
571
|
const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
|
|
572
572
|
setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined, replacedSessionNamespace), eventRank);
|
|
573
573
|
}
|
|
574
|
-
if (succeeded && !isCloseCommand(command) && sessionName && (usedImplicitSession || sessionMode === "fresh" || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
|
|
574
|
+
if (succeeded && !isCloseCommand(command) && sessionName && ((!explicitSessionName && (usedImplicitSession || sessionMode === "fresh")) || details.managedSessionHeadedAutosaveDisabled === true || typeof details.managedSessionHeadedAutosaveInterval === "string")) {
|
|
575
575
|
setBranchManagedSessionActive(events, sessionName, namespace, eventRank);
|
|
576
576
|
}
|
|
577
577
|
if (succeeded && isCloseCommand(command)) {
|
|
@@ -1243,6 +1243,78 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1243
1243
|
notifyRecordingPersistence(ctx);
|
|
1244
1244
|
}));
|
|
1245
1245
|
});
|
|
1246
|
+
// Additive native checkpoint event; the package's 0.84 validation types predate it.
|
|
1247
|
+
// Pi owns awaited tools/events (including their execution queues). Only script
|
|
1248
|
+
// children and retained browser/recording resources need extension-side checks.
|
|
1249
|
+
const checkpointPi = pi;
|
|
1250
|
+
checkpointPi.on("session_checkpoint", async (event, ctx) => {
|
|
1251
|
+
const blocked = (reason) => ({ sleepReady: false, reason });
|
|
1252
|
+
if (activeScriptControllers.size || activeScriptExecutions.size)
|
|
1253
|
+
return blocked("Browser script execution or cleanup is pending");
|
|
1254
|
+
flushRecordingReservations();
|
|
1255
|
+
if (recordingReservationsDirty)
|
|
1256
|
+
return blocked("Browser recording journal persistence is dirty");
|
|
1257
|
+
if (activeRecordingReservations.size)
|
|
1258
|
+
return blocked("Browser recording is pending; finish it explicitly before sleep");
|
|
1259
|
+
const entries = ctx.sessionManager.getEntries();
|
|
1260
|
+
if ([...getScriptSessionLeasesFromBranch(entries).values()].some(lease => lease.cleanup !== "closed")
|
|
1261
|
+
|| [...ownedManagedSessions.values()].some(owner => isAgentBrowserScriptSessionName(owner.sessionName))) {
|
|
1262
|
+
return blocked("Browser script cleanup lease is unresolved");
|
|
1263
|
+
}
|
|
1264
|
+
if (getActiveElectronRecords(ownedElectronLaunchRecords).length || getActiveElectronRecords(electronLaunchRecords).length
|
|
1265
|
+
|| [...electronChildProcesses.values()].some(child => child.exitCode === null && child.signalCode === null)) {
|
|
1266
|
+
return blocked("Electron launch is still active");
|
|
1267
|
+
}
|
|
1268
|
+
if (attachedSessionKeys.size || restoreAttachedSessionKeysFromBranch(entries).size)
|
|
1269
|
+
return blocked("Attached browser state is caller-owned and not checkpointed");
|
|
1270
|
+
if (traceOwners.size || [...networkRoutesBySession.values()].some(routes => routes.length))
|
|
1271
|
+
return blocked("Browser trace or network routes are still active");
|
|
1272
|
+
// Include off-branch and caller-owned/root identities, without acquiring
|
|
1273
|
+
// cleanup ownership. Transcript page/ref details do not serialize a browser.
|
|
1274
|
+
const historicalResources = collectBranchManagedResourceEvents(entries);
|
|
1275
|
+
const sessions = new Map(ownedManagedSessions);
|
|
1276
|
+
if (managedSessionActive)
|
|
1277
|
+
trackOwnedManagedSession(sessions, managedSessionName, managedSessionCwd, { namespace: managedSessionNamespace });
|
|
1278
|
+
for (const entry of entries) {
|
|
1279
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== "agent_browser")
|
|
1280
|
+
continue;
|
|
1281
|
+
const details = isRecord(entry.message.details) ? entry.message.details : undefined;
|
|
1282
|
+
// Helpers can launch before a main-process failure. Conversely, native
|
|
1283
|
+
// launch flags on sessionless reads can open the unnamed default browser.
|
|
1284
|
+
const sessionName = typeof details?.sessionName === "string" ? details.sessionName
|
|
1285
|
+
: details?.agentBrowserStarted === true ? "default" : undefined;
|
|
1286
|
+
if (!sessionName)
|
|
1287
|
+
continue;
|
|
1288
|
+
const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
|
|
1289
|
+
const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
1290
|
+
if (!sessions.has(key))
|
|
1291
|
+
trackOwnedManagedSession(sessions, sessionName, ctx.cwd, { namespace });
|
|
1292
|
+
}
|
|
1293
|
+
for (const [key, owner] of sessions) {
|
|
1294
|
+
event.signal.throwIfAborted();
|
|
1295
|
+
// An abnormal restart can leave a wrapper-created browser only off-branch.
|
|
1296
|
+
// Reuse native ownership events for inspection, never cleanup ownership.
|
|
1297
|
+
// A later close can refer to the same identity in a different socket root.
|
|
1298
|
+
const historicalIdentity = historicalResources.managedSessionActiveIdentities.get(key);
|
|
1299
|
+
const historicallyOwned = historicalIdentity && isRestorableManagedSessionName(historicalIdentity.sessionName, managedSessionBaseName);
|
|
1300
|
+
const context = resolveOwnedManagedSessionContext({
|
|
1301
|
+
...owner,
|
|
1302
|
+
currentManagedSessionName: managedSessionActive ? managedSessionName : undefined,
|
|
1303
|
+
currentManagedSessionNamespace: managedSessionNamespace,
|
|
1304
|
+
recordedOwnedSession: ownedManagedSessions.get(key) ?? (historicallyOwned ? { ...historicalIdentity, cwd: ctx.cwd } : undefined),
|
|
1305
|
+
restoreState: managedSessionRestoreState,
|
|
1306
|
+
});
|
|
1307
|
+
// Owned routing must not mask an explicit caller's same-name ambient daemon.
|
|
1308
|
+
for (const inspectionContext of context ? [{ ...context, reuseOnly: true }, undefined] : [undefined]) {
|
|
1309
|
+
const daemon = await withOwnedManagedSessionContext(inspectionContext, () => inspectManagedSessionDaemon({ ...owner, signal: event.signal, timeoutMs: 2_000 }));
|
|
1310
|
+
if (daemon.status !== "inactive")
|
|
1311
|
+
return blocked("Browser daemon is live or unverified; finish browser work explicitly before sleep");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// No detached writer remains to pause/resume. Native ingress stays held;
|
|
1315
|
+
// checkpoint never closes a browser or changes ordinary shutdown ownership.
|
|
1316
|
+
return { sleepReady: true };
|
|
1317
|
+
});
|
|
1246
1318
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
1247
1319
|
for (const controller of activeScriptControllers)
|
|
1248
1320
|
controller.abort();
|
|
@@ -1623,6 +1695,9 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
|
|
|
1623
1695
|
? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
|
|
1624
1696
|
: undefined;
|
|
1625
1697
|
const runBrowserCommand = async (daemonInactive) => {
|
|
1698
|
+
// Load execution-only preparation/output code inside the existing session queue,
|
|
1699
|
+
// before capturing state. Registration, rendering, and restore stay synchronous/eager.
|
|
1700
|
+
const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
|
|
1626
1701
|
flushRecordingReservations();
|
|
1627
1702
|
const branchRestoreGenerationAtStart = branchRestoreGeneration;
|
|
1628
1703
|
const generationAtStart = branchStateGeneration;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { lstat, rm } from "node:fs/promises";
|
|
3
|
+
import { win32 } from "node:path";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
5
|
import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
|
|
5
6
|
import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch.js";
|
|
@@ -53,21 +54,35 @@ export async function inspectElectronLaunchStatus(record, signal) {
|
|
|
53
54
|
version: cdp.version,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
57
|
+
function hasProcessExited(child, pid) {
|
|
58
|
+
// A Windows PID probe can report ESRCH during termination, before the
|
|
59
|
+
// tracked process has exited and released its executable/resources.
|
|
60
|
+
return child ? child.exitCode !== null || child.signalCode !== null : isPidAlive(pid) === false;
|
|
61
|
+
}
|
|
56
62
|
async function waitForProcessExit(child, pid, deadlineMs) {
|
|
57
63
|
while (Date.now() <= deadlineMs) {
|
|
58
|
-
if (
|
|
59
|
-
return true;
|
|
60
|
-
if (isPidAlive(pid) === false)
|
|
64
|
+
if (hasProcessExited(child, pid))
|
|
61
65
|
return true;
|
|
62
66
|
await sleep(ELECTRON_CLEANUP_POLL_INTERVAL_MS);
|
|
63
67
|
}
|
|
64
|
-
return
|
|
68
|
+
return hasProcessExited(child, pid);
|
|
65
69
|
}
|
|
66
70
|
async function readPidCommandLine(pid) {
|
|
67
71
|
if (!pid || !Number.isSafeInteger(pid) || pid <= 0)
|
|
68
72
|
return undefined;
|
|
69
73
|
try {
|
|
70
|
-
|
|
74
|
+
// Win32_Process.CommandLine is the native ownership evidence; process
|
|
75
|
+
// start time (used by daemon-policy locks) cannot prove profile ownership.
|
|
76
|
+
const systemRoot = process.env.SystemRoot;
|
|
77
|
+
const windowsRoot = systemRoot && win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows";
|
|
78
|
+
const file = process.platform === "win32"
|
|
79
|
+
? win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
80
|
+
: "ps";
|
|
81
|
+
const args = process.platform === "win32"
|
|
82
|
+
? ["-NoProfile", "-NonInteractive", "-Command",
|
|
83
|
+
`[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $p = [wmi]"Win32_Process.Handle='${pid}'"; [Console]::WriteLine($p.CommandLine)`]
|
|
84
|
+
: ["-ww", "-p", String(pid), "-o", "command="];
|
|
85
|
+
const { stdout } = await execFileAsync(file, args, {
|
|
71
86
|
timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
|
|
72
87
|
});
|
|
73
88
|
return stdout.trim() || undefined;
|
|
@@ -117,7 +132,7 @@ function signalRestoredLaunchProcess(record, signal) {
|
|
|
117
132
|
async function cleanupProcess(record, child, deadlineMs) {
|
|
118
133
|
if (!record.pid)
|
|
119
134
|
return { resource: "process", state: "skipped" };
|
|
120
|
-
if (
|
|
135
|
+
if (hasProcessExited(child, record.pid))
|
|
121
136
|
return { resource: "process", state: "already-gone" };
|
|
122
137
|
if (!child) {
|
|
123
138
|
const verificationError = await getRestoredProcessVerificationError(record);
|
|
@@ -164,16 +164,29 @@ async function ownerAlive(owner) {
|
|
|
164
164
|
const current = await readProcessStartIdentity(owner.pid);
|
|
165
165
|
return current === undefined ? undefined : processStartIdentitiesMatch(owner.startIdentity, current);
|
|
166
166
|
}
|
|
167
|
-
async function removeClaimOwnedBy(path, token) {
|
|
168
|
-
const current = await readClaim(path);
|
|
169
|
-
if (current?.owner.token !== token)
|
|
170
|
-
return false;
|
|
167
|
+
async function removeClaimOwnedBy(path, token, deadline = 0) {
|
|
171
168
|
const movedPath = join(dirname(path), `.pi-agent-browser-policy-remove-${token}-${randomUUID()}`);
|
|
172
|
-
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
while (true) {
|
|
170
|
+
const current = await readClaim(path);
|
|
171
|
+
if (current?.owner.token !== token)
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
await rename(path, movedPath);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const code = error.code;
|
|
179
|
+
if (code === "ENOENT")
|
|
180
|
+
return true;
|
|
181
|
+
// Windows may refuse a directory move while a contender reads its
|
|
182
|
+
// metadata. Revalidate before retrying. Owned-claim cleanup gets
|
|
183
|
+
// its own bounded window, independent of the acquisition wait.
|
|
184
|
+
if (process.platform !== "win32" || code !== "EPERM" || Date.now() >= deadline)
|
|
185
|
+
return false;
|
|
186
|
+
await waitForRetry();
|
|
187
|
+
if (Date.now() >= deadline)
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
177
190
|
}
|
|
178
191
|
const moved = await readClaim(movedPath);
|
|
179
192
|
if (moved?.owner.token !== token) {
|
|
@@ -209,6 +222,12 @@ function claimPrecedes(left, right) {
|
|
|
209
222
|
return false;
|
|
210
223
|
return left.ticket < right.ticket || (left.ticket === right.ticket && left.owner.token < right.owner.token);
|
|
211
224
|
}
|
|
225
|
+
async function hasPublishedLaterTicket(claim, ownClaim) {
|
|
226
|
+
if (claim.ticket !== null)
|
|
227
|
+
return false;
|
|
228
|
+
const current = await readClaim(claim.path);
|
|
229
|
+
return current?.owner.token === claim.owner.token && !claimPrecedes(current, ownClaim);
|
|
230
|
+
}
|
|
212
231
|
function waitForRetry(signal) {
|
|
213
232
|
return new Promise((resolve) => {
|
|
214
233
|
if (signal?.aborted)
|
|
@@ -266,18 +285,35 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
266
285
|
for (const claim of claims) {
|
|
267
286
|
if (claim.owner.token === token || !claimPrecedes(claim, ownClaim))
|
|
268
287
|
continue;
|
|
288
|
+
// Choosing is transient: a later published ticket must not make
|
|
289
|
+
// its predecessor wait on it using an earlier null snapshot.
|
|
290
|
+
if (await hasPublishedLaterTicket(claim, ownClaim))
|
|
291
|
+
continue;
|
|
269
292
|
const alive = await ownerAlive(claim.owner);
|
|
270
293
|
if (alive === false) {
|
|
271
294
|
await removeClaimOwnedBy(claim.path, claim.owner.token);
|
|
272
295
|
continue;
|
|
273
296
|
}
|
|
297
|
+
// A native identity query can outlive the owner's entire critical
|
|
298
|
+
// section. Its result (including unknown after exit) does not make
|
|
299
|
+
// an already released immutable claim a current blocker.
|
|
300
|
+
try {
|
|
301
|
+
await lstat(claim.path);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error.code === "ENOENT")
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// Publication can also complete during the native identity query.
|
|
308
|
+
if (await hasPublishedLaterTicket(claim, ownClaim))
|
|
309
|
+
continue;
|
|
274
310
|
blocked = true;
|
|
275
311
|
break;
|
|
276
312
|
}
|
|
277
313
|
if (!blocked) {
|
|
278
314
|
await cleanDeadPolicyArtifacts(directory);
|
|
279
315
|
lockAcquired = true;
|
|
280
|
-
return { release: async () => { await removeClaimOwnedBy(claimPath, token); } };
|
|
316
|
+
return { release: async () => { await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS); } };
|
|
281
317
|
}
|
|
282
318
|
if (Date.now() >= deadline)
|
|
283
319
|
return undefined;
|
|
@@ -291,6 +327,6 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
291
327
|
finally {
|
|
292
328
|
await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
|
|
293
329
|
if (claimPublished && !lockAcquired)
|
|
294
|
-
await removeClaimOwnedBy(claimPath, token);
|
|
330
|
+
await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS);
|
|
295
331
|
}
|
|
296
332
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { dirname, join, win32 } from "node:path";
|
|
3
3
|
const WINDOWS_PROCESS_START_IDENTITY_PREFIX = "win32-powershell-ticks-v1:";
|
|
4
|
-
|
|
4
|
+
// Native Windows PowerShell startup can exhaust the former five-second probe budget.
|
|
5
|
+
const PROCESS_START_IDENTITY_TIMEOUT_MS = process.platform === "win32" ? 10_000 : 5_000;
|
|
5
6
|
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
6
7
|
export function buildProcessStartIdentityCommand(pid, platform = process.platform) {
|
|
7
8
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
@@ -11,6 +11,7 @@ import { getPageTargetValidationError, } from "./page-target-validation.js";
|
|
|
11
11
|
import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
|
|
12
12
|
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
13
13
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
14
|
+
import { resolveWindowsStockLauncher } from "./windows-stock-launcher.js";
|
|
14
15
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
16
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
16
17
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
@@ -52,11 +53,17 @@ export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent
|
|
|
52
53
|
normalized[occurrence.index + 1] = customArgs;
|
|
53
54
|
return normalized;
|
|
54
55
|
}
|
|
55
|
-
function terminateSpawnedChild(child, signal) {
|
|
56
|
+
async function terminateSpawnedChild(child, signal) {
|
|
56
57
|
if (processPlatform === "win32" && child.pid) {
|
|
57
|
-
|
|
58
|
-
killer
|
|
59
|
-
|
|
58
|
+
// Keep the shell alive until taskkill has traversed its descendants, and
|
|
59
|
+
// observe the killer before allowing the browser call to finish.
|
|
60
|
+
const killed = await new Promise((resolve) => {
|
|
61
|
+
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
62
|
+
killer.once("error", () => resolve(false));
|
|
63
|
+
killer.once("close", (code) => resolve(code === 0));
|
|
64
|
+
});
|
|
65
|
+
if (killed)
|
|
66
|
+
return;
|
|
60
67
|
}
|
|
61
68
|
child.kill(signal);
|
|
62
69
|
}
|
|
@@ -224,9 +231,29 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
|
|
|
224
231
|
}
|
|
225
232
|
return true;
|
|
226
233
|
}
|
|
227
|
-
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
234
|
+
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined, platform = processPlatform) {
|
|
228
235
|
if (!isAbsolute(socketDir))
|
|
229
236
|
return "the path is not absolute";
|
|
237
|
+
// Windows uses native ACLs and named pipes, not POSIX uid/mode metadata.
|
|
238
|
+
// Still require a real directory; never accept a file or a redirected root.
|
|
239
|
+
if (platform === "win32") {
|
|
240
|
+
try {
|
|
241
|
+
try {
|
|
242
|
+
await mkdir(socketDir);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error.code !== "EEXIST")
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
const metadata = await lstat(socketDir);
|
|
249
|
+
if (metadata.isSymbolicLink())
|
|
250
|
+
return "the directory is a symlink";
|
|
251
|
+
return metadata.isDirectory() ? undefined : "the path is not a directory";
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
230
257
|
if (typeof uid !== "number")
|
|
231
258
|
return "POSIX ownership metadata is unavailable";
|
|
232
259
|
try {
|
|
@@ -378,6 +405,8 @@ export async function runAgentBrowserProcess(options) {
|
|
|
378
405
|
}
|
|
379
406
|
effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
|
|
380
407
|
}
|
|
408
|
+
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
409
|
+
const stockLauncher = resolveWindowsStockLauncher(cwd, childEnv);
|
|
381
410
|
if (signal?.aborted) {
|
|
382
411
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
383
412
|
}
|
|
@@ -395,6 +424,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
395
424
|
let stdoutSpillPending = false;
|
|
396
425
|
let pendingStdoutWrite = Promise.resolve();
|
|
397
426
|
let stdoutSpillError;
|
|
427
|
+
let pendingTermination;
|
|
398
428
|
let killTimer;
|
|
399
429
|
let timeoutTimer;
|
|
400
430
|
let abortListener;
|
|
@@ -444,15 +474,16 @@ export async function runAgentBrowserProcess(options) {
|
|
|
444
474
|
if (settled)
|
|
445
475
|
return;
|
|
446
476
|
settled = true;
|
|
477
|
+
removeAbortListener();
|
|
478
|
+
if (killTimer) {
|
|
479
|
+
clearTimeout(killTimer);
|
|
480
|
+
}
|
|
481
|
+
if (timeoutTimer) {
|
|
482
|
+
clearTimeout(timeoutTimer);
|
|
483
|
+
}
|
|
484
|
+
completionWatcher?.clear();
|
|
447
485
|
void pendingStdoutWrite.finally(async () => {
|
|
448
|
-
|
|
449
|
-
if (killTimer) {
|
|
450
|
-
clearTimeout(killTimer);
|
|
451
|
-
}
|
|
452
|
-
if (timeoutTimer) {
|
|
453
|
-
clearTimeout(timeoutTimer);
|
|
454
|
-
}
|
|
455
|
-
completionWatcher?.clear();
|
|
486
|
+
await pendingTermination;
|
|
456
487
|
if (stdoutSpillHandle) {
|
|
457
488
|
await stdoutSpillHandle.close().catch(() => undefined);
|
|
458
489
|
}
|
|
@@ -478,14 +509,13 @@ export async function runAgentBrowserProcess(options) {
|
|
|
478
509
|
});
|
|
479
510
|
});
|
|
480
511
|
};
|
|
481
|
-
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
482
512
|
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
483
513
|
if (spawnPolicyError) {
|
|
484
514
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
485
515
|
return;
|
|
486
516
|
}
|
|
487
|
-
const spawnBrowser = processPlatform === "win32" ? crossSpawn : spawn;
|
|
488
|
-
const child = spawnBrowser("agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
|
|
517
|
+
const spawnBrowser = processPlatform === "win32" && !stockLauncher ? crossSpawn : spawn;
|
|
518
|
+
const child = spawnBrowser(stockLauncher ?? "agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
|
|
489
519
|
cwd,
|
|
490
520
|
env: childEnv,
|
|
491
521
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -505,10 +535,16 @@ export async function runAgentBrowserProcess(options) {
|
|
|
505
535
|
else {
|
|
506
536
|
timedOut = true;
|
|
507
537
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
538
|
+
if (pendingTermination)
|
|
539
|
+
return;
|
|
540
|
+
pendingTermination = terminateSpawnedChild(child, "SIGTERM");
|
|
541
|
+
// Windows taskkill already forces the entire tree. A concurrent direct
|
|
542
|
+
// kill would remove its root before traversal finishes.
|
|
543
|
+
if (processPlatform !== "win32") {
|
|
544
|
+
killTimer = setTimeout(() => {
|
|
545
|
+
pendingTermination = terminateSpawnedChild(child, "SIGKILL");
|
|
546
|
+
}, 2_000);
|
|
547
|
+
}
|
|
512
548
|
};
|
|
513
549
|
const recordStdinError = (error) => {
|
|
514
550
|
const stdinError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -251,7 +251,7 @@ function redactBearerCredentials(text) {
|
|
|
251
251
|
return match;
|
|
252
252
|
return formatRedactedCredential(label, credential, trailing);
|
|
253
253
|
})
|
|
254
|
-
.replace(/\b(Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
|
|
254
|
+
.replace(/\b(Bearer)\s+([^\s\\"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
|
|
255
255
|
// Without a credential field/header, require a bearer-token shape, not prose, HTML or a URL.
|
|
256
256
|
const token = credential.slice(0, credential.length - credentialTrailingPunctuation(credential).length);
|
|
257
257
|
if (!/^[A-Za-z0-9._~+/-]+=*$/.test(token) || !/[0-9._~+/=-]/.test(token))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { isMainThread } from "node:worker_threads";
|
|
4
|
+
import pathKey from "path-key";
|
|
5
|
+
import which from "which";
|
|
6
|
+
/** Bypass only upstream's optimized npm CMD shim: cmd.exe loses literal CR/LF.
|
|
7
|
+
* Keep which/path-key aligned with cross-spawn's public dependency versions.
|
|
8
|
+
* Unknown/custom launchers (including other npm layouts) stay on cross-spawn.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveWindowsStockLauncher(cwd, env) {
|
|
11
|
+
if (process.platform !== "win32" || !isMainThread)
|
|
12
|
+
return undefined;
|
|
13
|
+
// which reads parent PATHEXT; cmd.exe reads the child's. Node also handles
|
|
14
|
+
// duplicate case-insensitive environment keys differently from path-key.
|
|
15
|
+
// In those ambiguous cases, keep the original transport and its selection.
|
|
16
|
+
const values = (name) => Object.entries(env)
|
|
17
|
+
.filter(([key, value]) => key.toUpperCase() === name && value !== undefined).map(([, value]) => value);
|
|
18
|
+
const paths = values("PATH"), extensions = values("PATHEXT");
|
|
19
|
+
if (paths.length !== 1 || !paths[0] || extensions.length !== 1 || extensions[0] !== process.env.PATHEXT
|
|
20
|
+
|| values("NODEFAULTCURRENTDIRECTORYINEXEPATH").length)
|
|
21
|
+
return undefined;
|
|
22
|
+
// A custom COMSPEC can implement different command selection/semantics.
|
|
23
|
+
// Compare filesystem identity, not short/long Windows path spellings.
|
|
24
|
+
try {
|
|
25
|
+
if (!process.env.comspec || !isAbsolute(process.env.comspec) || !process.env.SystemRoot)
|
|
26
|
+
return undefined;
|
|
27
|
+
const shell = statSync(process.env.comspec, { bigint: true });
|
|
28
|
+
const native = statSync(join(process.env.SystemRoot, "System32", "cmd.exe"), { bigint: true });
|
|
29
|
+
if (shell.dev !== native.dev || shell.ino !== native.ino)
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const previousCwd = process.cwd();
|
|
36
|
+
let selected;
|
|
37
|
+
try {
|
|
38
|
+
// which 2 has no cwd option. Like cross-spawn, change cwd only during
|
|
39
|
+
// synchronous resolution, restoring it before any caller can await.
|
|
40
|
+
process.chdir(cwd);
|
|
41
|
+
selected = which.sync("agent-browser", { path: env[pathKey({ env })], nothrow: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
process.chdir(previousCwd);
|
|
48
|
+
}
|
|
49
|
+
// cross-spawn's second (extensionless) lookup cannot select this CMD shim.
|
|
50
|
+
if (!selected || !selected.toLowerCase().endsWith(".cmd"))
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
// which can return a relative PATH result; interpret it in the child cwd.
|
|
54
|
+
const absoluteShim = resolve(cwd, selected);
|
|
55
|
+
if (statSync(absoluteShim).size > 512)
|
|
56
|
+
return undefined;
|
|
57
|
+
const text = readFileSync(absoluteShim, "utf8");
|
|
58
|
+
if (!/^@ECHO off\r?\n"%~dp0node_modules\\agent-browser\\bin\\agent-browser-win32-x64\.exe" %\*(?:\r?\n)?(?![\s\S])/.test(text))
|
|
59
|
+
return undefined;
|
|
60
|
+
const packageRoot = join(dirname(absoluteShim), "node_modules", "agent-browser");
|
|
61
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
62
|
+
if (manifest.name !== "agent-browser" || !["bin/agent-browser.js", "./bin/agent-browser.js"].includes(manifest.bin?.["agent-browser"]))
|
|
63
|
+
return undefined;
|
|
64
|
+
const binary = join(packageRoot, "bin", "agent-browser-win32-x64.exe");
|
|
65
|
+
return statSync(binary).isFile() ? binary : undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -36,7 +36,8 @@ Why:
|
|
|
36
36
|
|
|
37
37
|
The extension should:
|
|
38
38
|
- resolve `agent-browser` from `PATH`
|
|
39
|
-
- invoke it with native Node `spawn` on POSIX and `cross-spawn` on Windows.
|
|
39
|
+
- invoke it with native Node `spawn` on POSIX and `cross-spawn` on Windows. For the recognized global npm CMD template, public `which`/`path-key` resolution selects the same launcher and Node invokes its exact adjacent native executable directly, preserving literal CR/LF. Recognition requires the stock template, package identity and executable; custom launchers, other layouts, ambiguous search environments and worker-thread calls retain `cross-spawn`. No later installation is substituted. Custom or unrecognized batch files can still truncate literal newlines. Caller commands and helper probes retain empty operands, literal doublequotes, command/subcommand adjacency and explicit default `--namespace ""`. Keep piped stdin/stdout/stderr and await full Windows process-tree termination with `taskkill /T /F` on timeout or abort before falling back to the direct child signal. Missing commands use the spawner's `ENOENT`
|
|
40
|
+
- bound native process-identity queries at ten seconds on Windows, including PowerShell startup, and five seconds on POSIX. These probes preserve PID/start-time comparison and fail closed when identity is unavailable; their budget is separate from lock contention and browser command watchdogs
|
|
40
41
|
- inject `--json`; caller-requested JSON also uses the existing `buildJsonVisibleContent` envelope for early snapshot/network filters, scroll results, and preparation failures, retaining metadata in `details` and keeping output-file notices out of visible JSON. Help/version stays native text
|
|
41
42
|
- complete each upstream invocation when the direct `agent-browser` child exits even if Node delays `"close"`: piped stdio can stay referenced by longer-lived descendant processes, so `runAgentBrowserProcess` watches `exit` and `close` together, leaves stdio intact during a short post-`exit` grace so normal `close` can still win, destroys streams only when the post-`exit` fallback fires, and prefers `close` codes then wrapper timeout (`124`) over signal-shaped `exit` codes (`watchSpawnedChildCompletion` / `resolveSpawnedChildExitCode` in `extensions/agent-browser/lib/process.ts`) so the tool cannot hang after the CLI process has already terminated
|
|
42
43
|
- support optional stdin only for `eval --stdin`, `batch`, `auth save --password-stdin`, and wrapper-generated `batch` stdin from top-level `job`, `qa`, `sourceLookup`, or `networkSourceLookup`, rejecting other command/stdin combinations before launch; top-level `electron` never accepts caller `stdin` (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron))
|
|
@@ -214,6 +215,16 @@ Practical policy:
|
|
|
214
215
|
|
|
215
216
|
This is primarily about ownership clarity and avoiding surprise, not adding a heavy safety wrapper. If the extension invented the session, the extension should own its lifecycle without breaking reload, resume, or branch-tree semantics. If the caller explicitly chose the upstream session model, the extension should stay out of the way.
|
|
216
217
|
|
|
218
|
+
### Native checkpoint boundary
|
|
219
|
+
|
|
220
|
+
The additive native `session_checkpoint` hook qualifies only the no-active-resource path. Pi already awaits tools, commands and lifecycle callbacks, including their returned execution queues. The hook checks the existing script controllers/completion promises, recording reservations and dirty retry state, cleanup leases, Electron records/children, attachments, trace owners and routes. It adds no work queue or state journal, and never invokes shutdown, stops recording, or closes a browser during capture.
|
|
221
|
+
|
|
222
|
+
Known native identities come from existing ownership and all native tool-result entries, including off-branch caller-owned/root sessions and the unnamed native default (launch flags can open it even for an explicit URL read). Read-only `session info` must prove each daemon inactive; active, missing-binary and unknown results remain blockers. These probes reuse the existing owned-session context for runtime-owned identities and transcript-proven wrapper-created identities, including off-branch browsers after an abnormal restart, so an ambient `AGENT_BROWSER_SOCKET_DIR` cannot redirect their inspection. Historical routing uses the existing managed-resource events and namespace/session identity, not a name pattern alone, and does not acquire cleanup ownership. For owned or historically owned targets, inspection also checks native ambient routing: the same namespace/name can identify different daemons in different socket roots. A later transcript close does not suppress the historical route's read-only probe, since it may have closed only the ambient caller. Explicit caller choices (even the exact historical name with `sessionMode: "fresh"`), unrelated namespaces and root identities retain native ambient routing. An unused extension needs no browser executable or profile scan. Independent hosts/group members must still coordinate their own writers and resources; a Pi receipt is not a cross-process browser lock.
|
|
223
|
+
|
|
224
|
+
Recording retries still republish existing reservation transitions and retain a blocker while that extension state is dirty. Native journal persistence is Pi-owned: after extension barriers, core flushes accepted entries before returning a checkpoint; ongoing I/O failures reject acquisition. Repair filesystem permissions and retry capture after ordinary browser close/cleanup. The extension does not scan, compare or repair Pi's journal. Native strict restore rejects a journal/artifact mismatch, and native materialization supports a not-yet-created journal.
|
|
225
|
+
|
|
226
|
+
With no detached work left, native ingress provides the hold and release; there is no extension observer to suspend. Normal ownership-aware shutdown remains unchanged. Native close/save followed by explicit URL reopen can restore synthetic cookies/localStorage/sessionStorage with the same `pi-root-*` identity on fresh filesystem inodes. This does not qualify live DOM/JS, forms, history, IndexedDB, or the separate inode-scoped `piab-r2-*` managed restore key for cold filesystem relocation. See [native qualification](RELEASE.md#native-checkpoint-qualification).
|
|
227
|
+
|
|
217
228
|
### Launch flags
|
|
218
229
|
|
|
219
230
|
`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.
|
package/docs/ELECTRON.md
CHANGED
|
@@ -184,7 +184,9 @@ Output appears under `details.electron.probe`: `title`, `url`, `focusedElement`,
|
|
|
184
184
|
|
|
185
185
|
### `electron.cleanup` — wrapper-owned only
|
|
186
186
|
|
|
187
|
-
Closes the tracked managed session, stops only the wrapper-tracked process, verifies that the debug port no longer serves `/json/version`, and removes the wrapper-created `userDataDir`. Cleanup
|
|
187
|
+
Closes the tracked managed session, stops only the wrapper-tracked process, verifies that the debug port no longer serves `/json/version`, and removes the wrapper-created `userDataDir`. Cleanup waits for actual exit when it has a tracked child handle. For a live restored PID without that handle, it first checks the native process command line for the recorded profile: `Win32_Process.CommandLine` through Windows PowerShell on Windows, or `ps` on POSIX. If inspection fails or the profile does not match, cleanup leaves the process and profile intact.
|
|
188
|
+
|
|
189
|
+
Cleanup partial failures fail the tool result with `failureCategory: "cleanup-failed"` and the `retry-electron-cleanup` next action references the same `launchId` so retries are bounded.
|
|
188
190
|
|
|
189
191
|
```json
|
|
190
192
|
{ "electron": { "action": "cleanup", "launchId": "electron-…" } }
|
|
@@ -313,7 +315,7 @@ Policy mismatches fail with `failureCategory: "policy-blocked"` and `details.ele
|
|
|
313
315
|
|
|
314
316
|
Failed startup diagnostics include `outputCaptured`, `stdoutTail` / `stderrTail`, and `stdoutTruncated` / `stderrTruncated`. Each tail reads at most the last **4096 source bytes** before UTF-8 decoding and normal credential redaction, and appears in both visible failure text and structured details. Empty output is reported explicitly; `stdoutError` / `stderrError` report capture-read or close errors without replacing the original startup reason, exit status, or cleanup warning.
|
|
315
317
|
|
|
316
|
-
The app writes to mode-0600 `stdout.log` and `stderr.log` inside its isolated profile.
|
|
318
|
+
The app writes to mode-0600 `stdout.log` and `stderr.log` inside its isolated profile. Regular-file capture lets a retained app keep writing independently of Pi's output streams. POSIX launches are detached and can outlive the host; Windows launches remain host-owned and stop when the host exits. Preserved logs survive either outcome. Logs follow profile preservation and removal; **the read limit is not a lifetime disk limit**. If failed-startup process cleanup cannot finish, the profile and logs are protected from general temp cleanup. Any failure to persist that protection appears alongside the original `failure.cleanupError`; in-memory protection remains. Use the reported PID and profile path to resolve that failed cleanup before removing files.
|
|
317
319
|
|
|
318
320
|
Single-instance Electron behavior is a common cause of `timeout` and `upstream-error`. Many Electron apps enforce a single running instance and silently drop a second invocation's `--remote-debugging-port` flag. If the app is already running without a debug port, quit it first or use the manual host-launch path against the existing instance instead.
|
|
319
321
|
|
package/docs/RELEASE.md
CHANGED
|
@@ -17,6 +17,14 @@ Related docs:
|
|
|
17
17
|
|
|
18
18
|
Provide one concrete maintainer workflow for validating repo state, package contents, and install guidance before publishing `pi-agent-browser-native`.
|
|
19
19
|
|
|
20
|
+
## Current 0.6.16 rollout waiver
|
|
21
|
+
|
|
22
|
+
The owner's 2026-09-21 **“waive windows”** instruction makes Windows qualification nonblocking for this rollout, even with available runners, and supersedes older mandatory-Windows/no-waiver wording below and in platform guidance. Preserve failed diagnostics; do not claim a Windows full-suite pass or continue Windows repair experiments. Known policy-lock/restored Electron failures and bounded repeat results are tracked in [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191) and the [current support matrix](SUPPORT_MATRIX.md#current-0616-rollout).
|
|
23
|
+
|
|
24
|
+
Linux/macOS, declared Node floors, official/fork host and consumer-artifact checks, and Native checkpoint Linux remain required. All three final reviewers (reviewer-gpt, reviewer-ponytail, reviewer-claude) must approve before merge, release or activation. This waiver changes neither runtime nor the composed `release`/`prepublishOnly` scripts: record individually satisfied gates and the explicit Windows waiver, never label the full composition passed.
|
|
25
|
+
|
|
26
|
+
Docs/workflow-only changes may reuse prior live/platform evidence when the complete compiled runtime, runtime dependencies and relevant tests are unchanged. Changed published files require a new retained tarball checked by `verify-package.mjs --smoke-pi`, including runtime-only SDK and actual CLI loading. Publish those exact checked bytes without repacking.
|
|
27
|
+
|
|
20
28
|
## Pre-release checks
|
|
21
29
|
|
|
22
30
|
From the repository root:
|
|
@@ -47,7 +55,7 @@ npm run verify -- pre-pr
|
|
|
47
55
|
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
56
|
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
57
|
|
|
50
|
-
|
|
58
|
+
When using the documented, maintainer-approved [local macOS and hosted native-Windows alternatives](platform-smoke.md#alternate-native-transports), 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
59
|
|
|
52
60
|
`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.
|
|
53
61
|
|
|
@@ -282,6 +290,32 @@ This suite requires the installed stable `agent-browser --version` to meet the m
|
|
|
282
290
|
|
|
283
291
|
The default unit suite also runs `agentBrowserExtension passes through core command coverage fallback matrix` in [`test/agent-browser.extension-passthrough-validation.test.ts`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/test/agent-browser.extension-passthrough-validation.test.ts): a fake upstream records argv so explicit `--session connector connect 9222`, plus `download` with a selector and path, `get url`, `snapshot --compact`, and `tab new` / `tab t1` / `tab close` on implicit managed sessions, still prove `--json` and session ordering without a browser. A second fake-upstream matrix in that file (`agentBrowserExtension passes through non-core network debug diff stream dashboard and chat families`) pins representative `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat` JSON shapes plus redacted `details.data` and argv echoes without a browser. A third matrix (`agentBrowserExtension passes through provider and specialized skill workflows`) asserts provider `open` argv shapes still receive `--json` plus implicit `--session` while read-only `skills get …` stays stateless (no managed session fields) and provider credential env vars are forwarded into the fake upstream log. Extend those matrices when adding passthrough coverage that should stay out of the slow real-upstream loop.
|
|
284
292
|
|
|
293
|
+
### Native checkpoint qualification
|
|
294
|
+
|
|
295
|
+
`test/agent-browser.checkpoint-native.test.mjs` is an opt-in, model-free test for a Pi SDK exposing `acquireCheckpoint` with native failed-journal repair/flush (qualified against core `c401cada69172dfd683e6d569b2cd9e5a45b4fcd`). It loads the compiled extension through native resource discovery, dispatches real tools through an awaited native command, and records real results with SessionManager. It checks idle/release/reload, live root and managed browsers (including a stable ambient socket override without a Pi override), off-branch state, detached and native-owned scripts, recording, failed journal appends/cleanup leases, routes, trace, caller-owned CDP attachment, and native checkpoint selection/entries plus root restore after replacing checkout/state directories with fresh inodes. No provider or fake readiness classifier is used.
|
|
296
|
+
|
|
297
|
+
Run as a non-root user in an isolated environment with stock `agent-browser`, Chrome for Testing and ffmpeg. Clear inherited credentials/configuration; do not use a personal browser. The test creates its own HOME/socket directory, copies only the supplied browser binary directory, serves a loopback synthetic page, explicitly closes its sessions, and removes its fixtures. `PI_CHECKPOINT_TEST_BROWSER_DIR` must name a binary installation directory such as `.../.agent-browser/browsers/chrome-<version>`, never a browser profile.
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm run build
|
|
301
|
+
PI_CHECKPOINT_TEST_REQUIRED=1 \
|
|
302
|
+
PI_CHECKPOINT_TEST_SDK=/path/to/native/pi-coding-agent/dist/index.js \
|
|
303
|
+
PI_CHECKPOINT_TEST_BROWSER_DIR=/path/to/browsers/chrome-<version> \
|
|
304
|
+
node --test test/agent-browser.checkpoint-native.test.mjs
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`PI_CHECKPOINT_TEST_EXTENSION` can select a separate baseline checkout. With `PI_CHECKPOINT_TEST_BASELINE_RESTORE=1`, only its existing root-restore path runs and every receipt must retain the old shutdown blocker; this is not positive sleep qualification. The SDK selector is test-only; it does not change package peers or activate an extension. This gate is separate from the default 0.84 unit fixtures and release/platform/model-based gates. Qualification is for ordinary native save-on-close and explicit URL reopen, not a live browser snapshot. Journal fault injection checks that ongoing native I/O errors reject acquisition and preserve prior bytes/accepted entries. Permission repair must recover through core capture without repeating the failed append; recording retry and cleanup leases remain extension-owned. The test also checks exact repaired entries/selection in a separate-process strict cold restore and rejection of a mismatched artifact. After explicit close it observes native daemon exit before asserting idle readiness; an acknowledgement alone is not proof of inactivity. The abnormal-restart regression kills only its exact SDK fixture PID after ordinary branch navigation, then reopens the real journal in a fresh SDK process. It verifies that the off-branch wrapper-created daemon blocks capture without acquiring cleanup ownership, and that explicit managed-looking names and a nonowned namespace retain ambient routing. Same-namespace/same-name controls check distinct live daemons in owned and ambient socket roots, continued blocking after closing only the caller, ambient-only blocking after closing the historical daemon, and positive readiness after both are explicitly closed. All browser ownership evidence comes from executed native tools, never injected ownership records.
|
|
308
|
+
|
|
309
|
+
#### Required Linux PR gate
|
|
310
|
+
|
|
311
|
+
[`.github/workflows/native-checkpoint.yml`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/.github/workflows/native-checkpoint.yml) runs **Native checkpoint Linux** on every PR to `main`, pushes to `main`, and manual dispatch. Require this check in branch protection before merging checkpoint changes (repository settings are separate from the workflow). It does not replace release/manual-publication gates or full Linux personal-workspace sleep/resume qualification.
|
|
312
|
+
|
|
313
|
+
The CI-only recipe in [`scripts/ci-native-checkpoint.sh`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/ci-native-checkpoint.sh) builds the public fork target selected by `host-targets.json` in the workflow's pinned `fitchmultz/.github` automation revision, using the fork's lockfile and documented `npm ci --ignore-scripts`, `hydrate:model-data`, `build:offline` sequence. It uses explicit Node **26.9.0**, the extension's unchanged lockfile, stock **agent-browser 0.38.1**, a fresh stock Chrome-for-Testing download, and Ubuntu ffmpeg. No browser fork, personal profile, credentials, or model calls are involved.
|
|
314
|
+
|
|
315
|
+
Sources live under a private `/tmp` root, outside HOME ancestry. Setup and tests use `env -i`; verification runs non-root in a loopback-only network namespace after downloads finish. Shared compatibility checks select independent official/fork graphs for types, tests and consumer installation. This workflow additionally runs the locked official graph on Node 26 before network isolation, then generated-document checks and live command-reference sampling inside the loopback namespace. The separate native gate requires **13 passed, zero failed/cancelled/skipped/todo** in its TAP output. `PI_CHECKPOINT_TEST_REQUIRED=1` turns a missing SDK into a failure and rejects root/baseline-only runs; missing browser parameters, binaries, or checkpoint APIs fail normally. Without the flag or SDK, direct local invocation remains opt-in and skips.
|
|
316
|
+
|
|
317
|
+
CI uploads raw setup/build/provenance logs, Node 26 and document verification output, and native TAP (including receipts), even on failure. Chrome is downloaded through the pinned stock installer, not separately version-pinned; the actual browser version is logged. These checks prove synthetic native behavior on that runner and sourcehost, not real-site authentication, Electron-app behavior, managed-key cold portability, or complete personal qualification. Passing this gate does not replace code review or full personal-workspace acceptance.
|
|
318
|
+
|
|
285
319
|
### Native Linux socket-root regression
|
|
286
320
|
|
|
287
321
|
`test/agent-browser.socket-namespace.test.ts` is opt-in (`PI_AGENT_BROWSER_SOCKET_NAMESPACE=1`). Run it with the installed `tsx` test runner inside a disposable bubblewrap user/mount namespace: a read-only mode-`0755` `/` whose owner is unmapped, and a current-user-owned mode-`0700` tmpfs at `/tmp`. Use a cleared environment with only the required executable PATH, private HOME and the opt-in variable. The test verifies those identities, exchanges data over a real Unix socket, dispatches native `agent-browser --version` through the shared subprocess wrapper, and inspects a disposable managed session with automatic restore enabled. It does not launch a browser or call a model.
|
package/docs/SUPPORT_MATRIX.md
CHANGED
|
@@ -77,7 +77,7 @@ Browser evidence (RQ-0152) is covered by `test/agent-browser.browser-evidence.te
|
|
|
77
77
|
|
|
78
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.
|
|
79
79
|
|
|
80
|
-
Electron diagnostics (RQ-0096, #128) keep list timeout rejection truthful, label explicit-ID cleaned records as historical without changing active selection or actions, and measure the tracked profile path with native `lstat` (`present` / ENOENT-only `absent` / `unknown`). Existing Electron discovery/lifecycle tests cover current liveness independently of cleanup history, native path errors and dangling symlinks, transcript replay, and unchanged cleanup ownership. Failed-startup diagnostics include redacted last-4096-byte stdout/stderr tails in visible text and structured details, using private profile-local regular files. Regression tests cover exact tails, empty output, capture/spawn errors and closed native file handles; a real spawned fixture with injected kill denial proves
|
|
80
|
+
Electron diagnostics (RQ-0096, #128) keep list timeout rejection truthful, label explicit-ID cleaned records as historical without changing active selection or actions, and measure the tracked profile path with native `lstat` (`present` / ENOENT-only `absent` / `unknown`). Existing Electron discovery/lifecycle tests cover current liveness independently of cleanup history, native path errors and dangling symlinks, transcript replay, and unchanged cleanup ownership. Failed-startup diagnostics include redacted last-4096-byte stdout/stderr tails in visible text and structured details, using private profile-local regular files. Regression tests cover exact tails, empty output, capture/spawn errors and closed native file handles; a real spawned fixture with injected kill denial proves live output through temp cleanup, retained logs through native host-exit behavior, and persistence failure reporting. Tracked-child cleanup regressions require actual exit before profile removal and immediate executable deletion. Restored-PID cleanup uses native process command-line evidence on Windows and POSIX; real-process regressions verify matching profiles with spaces/Unicode, refusal of unrelated or mismatched profiles, and shutdown after transcript restoration. The shared daemon policy verifies a restored Electron attachment's live browser endpoint and named upstream `get cdp-url` before ordinary calls, status, or probe reuse; `test/agent-browser.extension-ref-guards.test.ts` covers successful reuse and repeated rejection of replaced app, connection, and namespace identities without weakening generic restore-disabled rules. These checks do not replace native-app, Pi lifecycle, package, or live-site gates.
|
|
81
81
|
|
|
82
82
|
Artifact diagnostics (#124/#127) use a shared pre-dispatch mkdir-error boundary, preserve raw batch argv/precedence, recommend absolute raw artifact paths, recognize image headers rather than filename MIME guesses, retain known requested/reported paths, and warn once about conservative ref invalidation after dispatched recording starts and URL-bearing restarts, on success or failure, without claiming an observed page change. `test/agent-browser.artifact-diagnostics.test.ts` covers registered filesystem failures, real image bytes and misleading suffixes, the inline bound, native macOS path aliases, recording/ref continuity and unreached-row negatives; `test/agent-browser.presentation-artifacts-batch.test.ts` retains artifact/persistence coverage. These source regressions do not qualify daemon-cwd differences or affected-filesystem timestamp behavior (#118), or replace native/Pi/package/live-site gates.
|
|
83
83
|
|
|
@@ -97,6 +97,16 @@ Shared native defaults are covered by `test/agent-browser.shared-defaults.test.t
|
|
|
97
97
|
|
|
98
98
|
Browser correctness regressions cover credential-bearing filtered network details/exports, native generated-Blob downloads with actual saved bytes, accessible-name click target identity and abstention, smooth CSS-container movement, and parseable early-result JSON. Native fixture coverage complements deterministic dispatch/cleanup and output-shape checks; release qualification remains separate.
|
|
99
99
|
|
|
100
|
+
## Current 0.6.16 rollout
|
|
101
|
+
|
|
102
|
+
On 2026-09-21 the owner explicitly instructed **“waive windows”** for this rollout, superseding older required-Windows/no-waiver guidance even where runners are available. Windows diagnostics remain visible but nonblocking; do not interpret a successful compatibility aggregate as a Windows pass. Follow-up: [#191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191).
|
|
103
|
+
|
|
104
|
+
At runtime source `3a9adf490883149e7f79ee070d1900877e17b891`, PR compatibility passed seven of eight lanes, including official Windows with its full consumer contract; fork Windows failed one restored Electron cleanup case. The private combined Windows run had two policy-lock failures (1,162 passed, 37 skipped, zero cancelled out of 1,201). One unchanged Electron repeat passed without a product patch; the original cause is unknown. Historical focused Windows passes below retain only their stated scope, not full Windows qualification.
|
|
105
|
+
|
|
106
|
+
Fresh precommit evidence for that runtime passed real Pi checkout/live-site and supported disposable Electron smoke, configured-source lifecycle, packaged runtime-only SDK/CLI loading, real-upstream checks, native macOS and the canonical named Ubuntu target (Debian 12). Local macOS is the documented alternate, not an SSH pass. Docs/workflow-only delivery may reuse this runtime evidence only after all 104 rebuilt runtime files match the checked artifact; changed published docs require a newly checked retained tarball. No additional paid/live gate is implied by an otherwise byte-identical runtime.
|
|
107
|
+
|
|
108
|
+
Linux/macOS, declared Node floors, official/fork host identities, required consumer artifacts and Native checkpoint Linux remain required. Required current-head CI and all three final reviewer approvals precede merge/release/activation. The unchanged composed `release`/`prepublishOnly` command still includes Windows and is not reported as passed under this waiver; see [release guidance](RELEASE.md#current-0616-rollout-waiver).
|
|
109
|
+
|
|
100
110
|
## Verification evidence
|
|
101
111
|
|
|
102
112
|
**0.38.1 PR evidence (2026-09-17, local macOS):** the full unit/fake suite passed 1,146 tests with 27 opt-in skips; the final review fixes then passed typecheck and 64 affected tests. Generated docs, live command-reference sampling, and the 132-file packaged Pi smoke passed. The real-upstream contract and 75 native batch/fidelity checks passed, as did all eight deterministic dogfood flows. Interactive compiled-checkout Pi smoke covered example.com and react.dev, delta/ref follow-ups, conditional screenshots, and a recording batch whose persisted result had two verified artifacts and no pending artifacts. Separate native probes confirmed helper-consumed WebMCP catalog updates remain visible. These checks do not qualify release-only platform/lifecycle gates; the hosted native-Windows result is recorded on the PR.
|
|
@@ -107,7 +117,7 @@ Re-run the gates below before each release. Dated outcomes in this table are his
|
|
|
107
117
|
| --- | --- | --- |
|
|
108
118
|
| Default local gate | `npm run verify` checks generated playbook drift, clean-builds generated `dist/`, runs `tsc --noEmit`, unit/fake tests, generated command-reference blocks, and live command-reference sampling. | **Earlier 0.36.0:** pass on 2026-09-01; 779 tests passed, two opt-in real-upstream tests skipped, and build/typecheck/docs/live command-reference gates passed. |
|
|
109
119
|
| Pre-PR local gate | `npm run verify -- pre-pr` composes the default gate with package-content verification. Use before larger local handoffs or PR-ready claims when lifecycle/platform/live dogfood cost is not warranted. | **Earlier 0.36.0 / package 0.6.2:** pass on 2026-09-01; 779 tests passed, two opt-in tests skipped, and the 127-file tarball verified. |
|
|
110
|
-
| Windows argument transport (#102/#131/#141) | `
|
|
120
|
+
| Windows argument transport (#102/#131/#141) | `test/agent-browser.windows-argv.test.ts` exercises actual host argv, empty values, quotes, Unicode/spaces, stdin, custom shims and failure outcomes. `test/agent-browser.windows-stock-launcher.test.ts` covers the recognized global npm CMD template's direct-native path, command selection, fallback, literal LF/CRLF and cancellation. Custom or unrecognized batch launchers retain their existing newline limitation. POSIX keeps Node `spawn`. | Native Windows focused qualification on 2026-09-21 passed all seven stock-launcher controls, all 29 Chromium cases and 33 process/argv cases (two existing platform skips). Actual upstream Rust received all 12 expected operands; the original CMD path lost all four newline-bearing operands. These focused results do not replace full-suite, managed-browser, package or release gates. |
|
|
111
121
|
| Real upstream contract | `npm run verify -- real-upstream` runs the localhost fixture matrix against a real stable `agent-browser` at or above the configured floor. | **Earlier recommended 0.36.0:** pass on 2026-09-01 (2/2 tests), including WebMCP and `--no-webmcp`; the 0.35.0 floor last passed on 2026-08-26 (2/2 tests). |
|
|
112
122
|
| Packaged Pi smoke | `npm run verify -- package-pi` validates package contents, installs the extracted tarball's runtime dependencies without lifecycle scripts or host peers, loads the packaged `agent_browser` tool without requiring optional Brave config, and executes fake-upstream `--version`. | **Earlier package 0.6.2 / Pi 0.84.4:** pass on 2026-09-01; the 127-file tarball loaded exactly one packaged `agent_browser` and executed `--version`. |
|
|
113
123
|
| Startup profile | `npm run verify -- startup-profile --samples <n>` clean-builds generated `dist/`, records direct package entrypoint import/factory timing in fresh Node processes, and writes `.artifacts/startup-profile/latest.json`. It must not launch Pi, tmux, mise, npm, browsers, or `agent-browser`; full Pi TUI ready-prompt profiling is intentionally excluded after it proved too invasive for routine verification. Run this opt-in evidence when package layout, the compiled entrypoint, top-level imports, schema registration, or prompt/config startup logic changes. | **Earlier compiled 0.6.2 entrypoint:** pass on 2026-09-01 with three samples; median 104.9 ms and max 114.5 ms, below the 250 ms budget. |
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -820,6 +820,8 @@ For `script`, the top-level category describes the whole orchestration and clean
|
|
|
820
820
|
|
|
821
821
|
These categories are intentionally bounded and stable so agents can branch on them instead of parsing prose. They do not replace raw diagnostics: `details.error`, `details.stderr`, `details.parseError`, `details.validationError`, and visible content still preserve the specific upstream or wrapper message after normal redaction.
|
|
822
822
|
|
|
823
|
+
`details.exitCode` preserves the native child close code when available, so it is not a portable timeout indicator: a Windows process stopped with `taskkill` can report `1`. The wrapper uses `124` for its timeout when no native close code is available. Use `failureCategory`, `timedOut`, and `aborted` to distinguish these outcomes.
|
|
824
|
+
|
|
823
825
|
For argv-supplied `--allowed-domains`, the wrapper treats domain containment as launch-scoped. Upstream owns request, worker, popup, and WebRTC containment plus incompatible-mode rejection; the wrapper passes the setting and upstream result through unchanged.
|
|
824
826
|
|
|
825
827
|
Real Pi custom tools only mark a tool result failed when the tool throws during `execute`; returned `isError` fields are not authoritative. The extension therefore also registers a `tool_result` handler that treats any `agent_browser` result with `details.resultCategory: "failure"` as a real Pi tool error. For normal prose output, it appends `Result category: failure; failureCategory: …; Pi tool isError: true.` to model-visible text. For caller-requested `--json` output, it only patches `isError` and preserves visible parseable JSON content unchanged. The TUI renderer also repeats that category line at the top of failed rendered results so collapsed failed rows keep the outcome visible. The hook treats `--json` as requested when echoed `details.args` or the original tool `input.args` includes that flag; it skips appending the prose notice when any non-empty text content item is parseable JSON, even if other text items are not parseable. Invalid or non-JSON text still gets the visible prose notice. Implementation: `buildAgentBrowserToolResultPatch` in `extensions/agent-browser/lib/pi-tool-rendering.ts`; `extensions/agent-browser/index.ts` registers the handler. This keeps Pi transcript semantics aligned with the machine-readable result contract, including wrapper-side reclassifications such as `qa-failure` after an upstream-successful batch and `artifact-missing` after an upstream-successful artifact command whose requested file is absent.
|
package/docs/platform-smoke.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
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
|
-
|
|
5
|
+
**Current 0.6.16 rollout exception:** the owner explicitly waived Windows qualification on 2026-09-21, including available runners. Windows failures remain visible but nonblocking, not passed; no further Windows repair experiments are part of this rollout. Linux/macOS and all other required checks remain required. See [release waiver and artifact requirements](RELEASE.md#current-0616-rollout-waiver) and [follow-up #191](https://github.com/fitchmultz/pi-agent-browser-native/issues/191). The commands below are unchanged and a waived Windows result does not make the full composition a passing matrix.
|
|
6
|
+
|
|
7
|
+
Outside that rollout exception, 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
8
|
|
|
7
9
|
## Required release gate
|
|
8
10
|
|
|
@@ -34,11 +36,11 @@ npm run verify -- platform-smoke run --target ubuntu --suite platform-build
|
|
|
34
36
|
| --- | --- | --- | --- |
|
|
35
37
|
| `macos` | `ssh` static localhost | POSIX shell on macOS | Required |
|
|
36
38
|
| `ubuntu` | `local-container` | POSIX shell in a Docker-compatible local container | Required |
|
|
37
|
-
| `windows-native` | `parallels` | native Windows PowerShell over OpenSSH |
|
|
39
|
+
| `windows-native` | `parallels` | native Windows PowerShell over OpenSSH | Nonblocking for the owner-waived 0.6.16 rollout; otherwise required |
|
|
38
40
|
|
|
39
41
|
## Alternate native transports
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
With maintainer approval, local macOS execution can replace 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. Record these as native platform checks, separately from Crabbox SSH or Parallels results. The default `release` and `prepublishOnly` commands still select the Crabbox matrix; record alternate suite evidence separately rather than reporting those commands as passed.
|
|
42
44
|
|
|
43
45
|
On macOS, run the unchanged commands returned by `buildPlatformBuildCommand('macos', 'pi-agent-browser-native', 24)` and `buildBrowserDogfoodCommand('macos', '0.38.1', 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
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.16",
|
|
4
4
|
"description": "pi extension that exposes agent-browser as a native tool for browser automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
@@ -65,11 +65,12 @@
|
|
|
65
65
|
"typebox": "*"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@earendil-works/pi-ai": "0.
|
|
69
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
70
|
-
"@earendil-works/pi-tui": "0.
|
|
68
|
+
"@earendil-works/pi-ai": "0.86.1",
|
|
69
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
70
|
+
"@earendil-works/pi-tui": "0.86.1",
|
|
71
71
|
"@types/cross-spawn": "6.0.6",
|
|
72
72
|
"@types/node": "^25.9.3",
|
|
73
|
+
"@types/which": "2.0.2",
|
|
73
74
|
"tsx": "^4.21.0",
|
|
74
75
|
"typebox": "^1.1.38",
|
|
75
76
|
"typescript": "^6.0.3"
|
|
@@ -90,8 +91,9 @@
|
|
|
90
91
|
"smoke:platform:ubuntu": "node scripts/platform-smoke.mjs run --target ubuntu",
|
|
91
92
|
"smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
|
|
92
93
|
"smoke:platform:all": "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native",
|
|
94
|
+
"check:compat": "node scripts/compat-host.mjs && npm run typecheck && npm test && node scripts/verify-package.mjs --smoke-pi",
|
|
93
95
|
"typecheck": "node ./scripts/project.mjs verify typecheck",
|
|
94
|
-
"test": "node ./scripts/build.mjs && tsx --test --test-concurrency=1 test/**/*.test.ts",
|
|
96
|
+
"test": "node ./scripts/build.mjs && tsx --test --test-reporter=tap --test-concurrency=1 test/**/*.test.ts",
|
|
95
97
|
"verify": "node ./scripts/project.mjs verify",
|
|
96
98
|
"prepublishOnly": "npm run verify -- release && npm pack --dry-run",
|
|
97
99
|
"build": "node ./scripts/build.mjs",
|
|
@@ -114,6 +116,8 @@
|
|
|
114
116
|
}
|
|
115
117
|
},
|
|
116
118
|
"dependencies": {
|
|
117
|
-
"cross-spawn": "7.0.6"
|
|
119
|
+
"cross-spawn": "7.0.6",
|
|
120
|
+
"path-key": "3.1.1",
|
|
121
|
+
"which": "2.0.2"
|
|
118
122
|
}
|
|
119
123
|
}
|
package/scripts/config.mjs
CHANGED
|
@@ -363,7 +363,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
363
363
|
throw new UsageError(`Unknown command: ${command}`);
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
if (import.meta.
|
|
366
|
+
if (import.meta.main) {
|
|
367
367
|
main().catch((error) => {
|
|
368
368
|
if (error instanceof UsageError) {
|
|
369
369
|
console.error(error.message);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile as execFileCallback } from "node:child_process";
|
|
|
11
11
|
import { realpathSync } from "node:fs";
|
|
12
12
|
import { access, readFile } from "node:fs/promises";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, resolve, sep } from "node:path";
|
|
14
|
+
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
|
|
@@ -155,7 +155,7 @@ function expandUserPath(path) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function isPathLikeSource(source) {
|
|
158
|
-
return source
|
|
158
|
+
return isAbsolute(source) || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function sourceLooksLikeThisPackage(source, cwd, sourceBaseDir = cwd) {
|