pi-agent-browser-native 0.9.0 → 0.9.1
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 -0
- package/README.md +2 -1
- package/dist/extensions/agent-browser/index.js +3 -107
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
- package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
- package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
- package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
- package/dist/extensions/agent-browser/lib/config.js +3 -48
- package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
- package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
- package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
- package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +1 -1
- package/dist/extensions/agent-browser/lib/process.js +0 -3
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
- package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
- package/dist/extensions/agent-browser/lib/results/network.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
- package/dist/extensions/agent-browser/lib/runtime.js +4 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
- package/dist/extensions/agent-browser/lib/web-search.js +15 -15
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/COMMAND_REFERENCE.md +2 -0
- package/docs/RELEASE.md +1 -1
- package/docs/SUPPORT_MATRIX.md +3 -3
- package/docs/TOOL_CONTRACT.md +3 -3
- package/package.json +3 -3
- package/scripts/doctor.mjs +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.9.1 - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Removed
|
|
8
|
+
|
|
9
|
+
- Deleted dead production code surfaced by the test audit: `getOwnedManagedSessionRestoreKey`, `parseValidBatchStepEntries`, `normalizeCommandName` (alias resolution is owned by the capability-table predicates), `AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS`, `isProjectSafeCredentialValueForProvider`, `isPlaintextCredentialValue`, the unused async `loadAgentBrowserConfig` duplicate of the sync loader, the unused `ensureAgentBrowserSocketDir` wrapper, and 46 export keywords (22 files) with no cross-module consumers. No behavior changed; owning tests now assert through the exported boundaries (`loadAgentBrowserConfigSync`, `getAgentBrowserSocketDirValidationError`, `runAgentBrowserScript`, capability predicates).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Share the unchanged 300000ms code timeout ceiling between its schema and runner, and the unchanged startup measurement/250ms budget between the regression test and safe profiler (retaining the Android test's 1000ms exception). Remove the unreachable platform-smoke passthrough entry without changing facade gates.
|
|
14
|
+
- Move retained regressions to focused queue, artifact, input-validation, Electron host/branch, and Pi-pipeline owners. Relocate queue and concurrent artifact-merge helpers out of the extension entrypoint without changing their behavior. Consolidate required platform paths and private-file/tarball leak checks into the canonical package verifier and its existing real-tarball test. Allocate the macOS artifact alias fixture under `/tmp` so its path-alias contract does not depend on `TMPDIR`.
|
|
15
|
+
- Consolidate duplicate test invocations into their owning tables and delivery boundaries, preserving distinct lifecycle, redaction, recovery, and native activation checks. Split recording dependency, reservation, and batch-lifecycle assertions into named contracts. Test-only; no production behavior changed.
|
|
16
|
+
- Repaired 23 weak test assertions found by the test audit: literal prompt-byte expectations instead of source-derived constants, per-fixture bound expectations replacing pass-for-every-row alternations, real execution and credential-laziness probes for web-search/config, exact root-session `get url` probe matrices in passthrough coverage, complete guarded-set and key-set assertions, and anchored literal launcher/argv-delivery checks. Test-only; no production behavior changed.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Execute maintainer command-reference and startup-profile scripts correctly through symlink and space/percent-encoded paths using Node's native main-module detection.
|
|
21
|
+
- Keep ordinary `cookies set --curl` import paths visible in invocation echoes and failed batch diagnostics without exposing inline cookie values, secret-bearing flags or file contents.
|
|
22
|
+
- Host the permissioned `agent_browser_code` child with a real Node runtime from `PATH` when Pi runs as a Bun-compiled binary, and fail as `missing-binary` with a clear remediation when no Node runtime is available instead of hanging the worker handshake (#238).
|
|
23
|
+
- Resolve npm `.cmd` shims on Windows in `pi-agent-browser-doctor` PATH checks so installed `agent-browser` and `pi` binaries stop producing false negatives (#219).
|
|
24
|
+
|
|
25
|
+
- Host cold-boundaries, cold-resume, and destination-cancel fixture browser sockets in a short private `0700` directory (`/tmp/piab-s-…`, via `createShortPrivateSocketDir` in the test harness) so realpath-canonicalized CI temp roots cannot exceed the macOS 103-byte Unix socket path cap. Test-only; production length and privacy guards are unchanged, and the fallback keeps the previous fixture-root path on Windows or when `/tmp` is unusable.
|
|
26
|
+
|
|
3
27
|
## 0.9.0 - 2026-10-01
|
|
4
28
|
|
|
5
29
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -570,6 +570,7 @@ Use these rules:
|
|
|
570
570
|
- Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
|
|
571
571
|
- Prefer `auth save --password-stdin` over putting passwords in `args`; the wrapper only accepts caller `stdin` for `batch`, `eval --stdin`, and `auth save --password-stdin` (advanced QA and source tools generate their own batch stdin).
|
|
572
572
|
- Use `state save <path>` / `state load <path>` for portable test state. `state save` is reported as a file artifact with verification metadata; if an upstream-successful artifact command reports a non-pending file path that the wrapper cannot find or did not update during this command, the tool fails with `failureCategory: "artifact-missing"` instead of treating missing/stale evidence as durable. `state load` may mention a path but is not treated as a newly saved artifact.
|
|
573
|
+
- `cookies set --curl <file>` treats its operand as a file path, not a cookie value: invocation echoes and failed batch diagnostics keep ordinary import paths visible. Inline cookie values and secret-bearing flags or path text still redact.
|
|
573
574
|
- Treat `cookies get`, `storage local|session`, `state show`, and `auth show` output as sensitive. `state show` is presented as saved-state metadata only, and cookie/localStorage/sessionStorage values are redacted from structured details. The native presentation summarizes and redacts credential-like values while allowing benign primitive storage values to aid local QA, but avoid requesting broad dumps unless the task needs them.
|
|
574
575
|
- Use `dialog status`, `dialog accept [text]`, `dialog dismiss`, and `frame <selector|main>` through native `args`; dialog commands use a shorter wrapper timeout and timed-out interactions add `inspect-dialog-after-timeout` / `dismiss-dialog-after-timeout` / fresh-session recovery actions so a blocking alert/prompt does not burn the full default watchdog. Use exact `confirm <id>` / `deny <id>` next actions for guarded-action confirmations.
|
|
575
576
|
|
|
@@ -729,7 +730,7 @@ Install upstream `agent-browser`, then install dependencies:
|
|
|
729
730
|
npm install
|
|
730
731
|
```
|
|
731
732
|
|
|
732
|
-
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.
|
|
733
|
+
Use the npm version declared in `package.json` `packageManager` when refreshing `package-lock.json` (for example `npx -y npm@12.2.0 install`) so optional-platform lockfile metadata does not drift. Use Node 24.21.0 and Pi 1.0.0 or newer for lifecycle and interactive browser smokes; the pinned Pi 1.0.0 devDependencies are validation fixtures, not an exact-version requirement for the host CLI. See [Environment and automation pitfalls](docs/RELEASE.md#environment-and-automation-pitfalls) in `docs/RELEASE.md`.
|
|
733
734
|
|
|
734
735
|
Checkout-only extension smoke test:
|
|
735
736
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
@@ -21,6 +20,8 @@ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment,
|
|
|
21
20
|
import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
|
|
22
21
|
import { getBrowserCwdError, resolveExecutionCwd } from "./lib/execution-cwd.js";
|
|
23
22
|
import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
|
|
23
|
+
import { AsyncExecutionQueue, KeyedAsyncExecutionQueue } from "./lib/orchestration/execution-queue.js";
|
|
24
|
+
import { mergeBrowserRunArtifactManifest } from "./lib/orchestration/browser-run/artifact-merge.js";
|
|
24
25
|
import { closeManagedSession, inspectManagedSessionDaemon } from "./lib/orchestration/browser-run/managed-session-daemon-policy.js";
|
|
25
26
|
import { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
|
|
26
27
|
import { buildPromptPolicy, getLatestUserMessage, getMessageText } from "./lib/prompt-policy.js";
|
|
@@ -37,7 +38,7 @@ import { applyAgentBrowserOutputPath, canWriteAgentBrowserOutput, normalizeReque
|
|
|
37
38
|
import { appendScriptSessionLease, createBrowserCodeOutput, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
|
|
38
39
|
import { resolveBrowserExecutionIdentity, withBrowserExecutionLock, withBrowserExecutionLocks } from "./lib/managed-session-policy-lock.js";
|
|
39
40
|
import { AGENT_BROWSER_INSTRUCTION_GROUP, AGENT_BROWSER_TOOL_INVENTORY, registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
|
|
40
|
-
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey,
|
|
41
|
+
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
|
|
41
42
|
import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
|
|
42
43
|
import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
|
|
43
44
|
import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
|
|
@@ -647,90 +648,6 @@ function shouldSerializeBrowserCommand(options) {
|
|
|
647
648
|
return true;
|
|
648
649
|
return getActiveElectronRecords(options.ownedElectronLaunchRecords).some((record) => record.sessionName === options.explicitSessionName);
|
|
649
650
|
}
|
|
650
|
-
// Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
|
|
651
|
-
class AsyncExecutionQueue {
|
|
652
|
-
tail = Promise.resolve();
|
|
653
|
-
active = new AsyncLocalStorage();
|
|
654
|
-
isCurrent() { return this.active.getStore()?.active === true; }
|
|
655
|
-
run(work, signal, barrier = Promise.resolve()) {
|
|
656
|
-
if (this.isCurrent()) {
|
|
657
|
-
signal?.throwIfAborted();
|
|
658
|
-
return work();
|
|
659
|
-
}
|
|
660
|
-
const previous = this.tail;
|
|
661
|
-
let release;
|
|
662
|
-
this.tail = new Promise((resolve) => {
|
|
663
|
-
release = resolve;
|
|
664
|
-
});
|
|
665
|
-
let rejectWaiting;
|
|
666
|
-
const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
|
|
667
|
-
const abortWaiting = () => rejectWaiting(signal?.reason);
|
|
668
|
-
signal?.addEventListener("abort", abortWaiting, { once: true });
|
|
669
|
-
if (signal?.aborted)
|
|
670
|
-
abortWaiting();
|
|
671
|
-
const execution = (async () => {
|
|
672
|
-
await Promise.all([previous, barrier]);
|
|
673
|
-
signal?.removeEventListener("abort", abortWaiting);
|
|
674
|
-
try {
|
|
675
|
-
signal?.throwIfAborted();
|
|
676
|
-
const scope = { active: true };
|
|
677
|
-
try {
|
|
678
|
-
return await this.active.run(scope, work);
|
|
679
|
-
}
|
|
680
|
-
finally {
|
|
681
|
-
scope.active = false;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
finally {
|
|
685
|
-
release();
|
|
686
|
-
}
|
|
687
|
-
})();
|
|
688
|
-
return signal ? Promise.race([execution, cancelled]) : execution;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
export class KeyedAsyncExecutionQueue {
|
|
692
|
-
barriers = new Map();
|
|
693
|
-
entries = new Map();
|
|
694
|
-
async run(key, namespace, work, signal) {
|
|
695
|
-
const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
|
|
696
|
-
if (entry.queue.isCurrent())
|
|
697
|
-
return work();
|
|
698
|
-
const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
|
|
699
|
-
entry.users += 1;
|
|
700
|
-
this.entries.set(key, entry);
|
|
701
|
-
try {
|
|
702
|
-
return await entry.queue.run(work, signal, barrier);
|
|
703
|
-
}
|
|
704
|
-
finally {
|
|
705
|
-
entry.users -= 1;
|
|
706
|
-
if (entry.users === 0 && this.entries.get(key) === entry)
|
|
707
|
-
this.entries.delete(key);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
async runExclusive(namespace, work) {
|
|
711
|
-
const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
|
|
712
|
-
const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
|
|
713
|
-
let release;
|
|
714
|
-
const blocked = new Promise((resolve) => {
|
|
715
|
-
release = resolve;
|
|
716
|
-
});
|
|
717
|
-
const barrier = previous.then(() => blocked);
|
|
718
|
-
this.barriers.set(namespaceKey, barrier);
|
|
719
|
-
const drains = [...this.entries]
|
|
720
|
-
.filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
721
|
-
.map(([, { queue }]) => queue.run(async () => undefined));
|
|
722
|
-
await previous;
|
|
723
|
-
await Promise.all(drains);
|
|
724
|
-
try {
|
|
725
|
-
return await work();
|
|
726
|
-
}
|
|
727
|
-
finally {
|
|
728
|
-
release();
|
|
729
|
-
if (this.barriers.get(namespaceKey) === barrier)
|
|
730
|
-
this.barriers.delete(namespaceKey);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
651
|
function mergeBrowserRunMap(current, initial, updated) {
|
|
735
652
|
if (updated === initial)
|
|
736
653
|
return current;
|
|
@@ -745,27 +662,6 @@ function mergeBrowserRunMap(current, initial, updated) {
|
|
|
745
662
|
}
|
|
746
663
|
return merged;
|
|
747
664
|
}
|
|
748
|
-
export function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
749
|
-
if (!updated || updated === initial)
|
|
750
|
-
return current;
|
|
751
|
-
if (current === initial)
|
|
752
|
-
return updated;
|
|
753
|
-
const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
|
|
754
|
-
const changedEntries = updated.entries
|
|
755
|
-
.map((entry, index) => ({ entry, index }))
|
|
756
|
-
.filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
|
|
757
|
-
.sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
|
|
758
|
-
|| Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
|
|
759
|
-
|| left.index - right.index)
|
|
760
|
-
.map(({ entry }) => entry);
|
|
761
|
-
return changedEntries.length === 0
|
|
762
|
-
? current
|
|
763
|
-
: mergeSessionArtifactManifest({
|
|
764
|
-
base: current,
|
|
765
|
-
entries: changedEntries,
|
|
766
|
-
nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
665
|
function findPackageRoot(startDir) {
|
|
770
666
|
let currentDir = startDir;
|
|
771
667
|
while (true) {
|
|
@@ -76,7 +76,7 @@ function getOpenCommandTarget(commandTokens) {
|
|
|
76
76
|
}
|
|
77
77
|
return undefined;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
function parseCommandInfoFromTokens(commandTokens) {
|
|
80
80
|
const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
|
|
81
81
|
const command = upstreamCommandTokens[0];
|
|
82
82
|
return {
|
|
@@ -38,7 +38,7 @@ export const GLOBAL_VALUE_FLAGS = [
|
|
|
38
38
|
"--idle-timeout",
|
|
39
39
|
"--input-mode",
|
|
40
40
|
];
|
|
41
|
-
|
|
41
|
+
const COMMAND_VALUE_FLAGS = [
|
|
42
42
|
"--allowed-origins",
|
|
43
43
|
"--baseline",
|
|
44
44
|
"--body",
|
|
@@ -79,7 +79,7 @@ export const COMMAND_VALUE_FLAGS = [
|
|
|
79
79
|
"--password",
|
|
80
80
|
"--wait-until",
|
|
81
81
|
];
|
|
82
|
-
|
|
82
|
+
const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
|
|
83
83
|
export const VALUE_FLAGS = new Set([...GLOBAL_VALUE_FLAGS, ...COMMAND_VALUE_FLAGS]);
|
|
84
84
|
export const PREVALIDATED_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
|
|
85
85
|
export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args", "--session"]);
|
|
@@ -246,7 +246,7 @@ export function extractRequestedRestoreKey(args, sessionName, envValue) {
|
|
|
246
246
|
}
|
|
247
247
|
return restoreKey;
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
function getFlagName(token) {
|
|
250
250
|
return token.split("=", 1)[0] ?? token;
|
|
251
251
|
}
|
|
252
252
|
export function isNonFlagToken(token) {
|
|
@@ -4,7 +4,7 @@ import { buildPageTransitionRefSnapshotInvalidation, normalizeSessionTabTarget,
|
|
|
4
4
|
import { getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isSessionArtifactManifest } from "./results/artifact-manifest.js";
|
|
5
5
|
import { extractAgentBrowserLifecycle } from "./results/presentation/common.js";
|
|
6
6
|
export const BROWSER_TRANSITION_ENTRY = "agent-browser-transition";
|
|
7
|
-
|
|
7
|
+
const BROWSER_EVENT_VERSION = 1;
|
|
8
8
|
export const BROWSER_RESULT_TOOLS = new Set([
|
|
9
9
|
"agent_browser", "agent_browser_code", "agent_browser_action", "agent_browser_qa", "agent_browser_electron",
|
|
10
10
|
"agent_browser_source", "agent_browser_network_source",
|
|
@@ -295,9 +295,6 @@ function getCommandCapability(command) {
|
|
|
295
295
|
function hasCommandCapability(command, capability) {
|
|
296
296
|
return getCommandCapability(command)?.[capability] === true;
|
|
297
297
|
}
|
|
298
|
-
export function normalizeCommandName(command) {
|
|
299
|
-
return getCommandCapability(command)?.command ?? command;
|
|
300
|
-
}
|
|
301
298
|
export function isCloseCommand(command) {
|
|
302
299
|
return hasCommandCapability(command, "closesSession");
|
|
303
300
|
}
|
|
@@ -229,19 +229,6 @@ function validateBrowserDefaultProfile(value, path, errors) {
|
|
|
229
229
|
}
|
|
230
230
|
return { name, policy: /** @type {BrowserDefaultProfilePolicy} */ (policy) };
|
|
231
231
|
}
|
|
232
|
-
/** @param {string} rawValue */
|
|
233
|
-
export function isPlaintextCredentialValue(rawValue) {
|
|
234
|
-
const trimmed = rawValue.trim();
|
|
235
|
-
return Boolean(trimmed) && !trimmed.startsWith("!") && !trimmed.startsWith("$");
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* @param {string} rawValue
|
|
239
|
-
* @param {WebSearchProvider} provider
|
|
240
|
-
*/
|
|
241
|
-
export function isProjectSafeCredentialValueForProvider(rawValue, provider) {
|
|
242
|
-
void provider;
|
|
243
|
-
return rawValue.trim().length > 0;
|
|
244
|
-
}
|
|
245
232
|
/**
|
|
246
233
|
* @param {unknown} value
|
|
247
234
|
* @param {string} path
|
|
@@ -1,53 +1,8 @@
|
|
|
1
1
|
import { exec as execCallback } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { promisify } from "node:util";
|
|
4
|
-
import { SECRET_COMMAND_TIMEOUT_MS,
|
|
5
|
-
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource,
|
|
3
|
+
import { SECRET_COMMAND_TIMEOUT_MS, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, resolveEnvInterpolations, } from "./config-policy.js";
|
|
4
|
+
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
|
|
6
5
|
const exec = promisify(execCallback);
|
|
7
|
-
async function readConfigLayer(path, scope, errors, warnings) {
|
|
8
|
-
let raw;
|
|
9
|
-
try {
|
|
10
|
-
raw = await readFile(path, "utf8");
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
errors.push(`Could not read ${scope} config ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
return parseAgentBrowserConfigLayer(raw, path, scope, errors, warnings);
|
|
20
|
-
}
|
|
21
|
-
export async function loadAgentBrowserConfig(options = {}) {
|
|
22
|
-
const env = options.env ?? process.env;
|
|
23
|
-
const paths = getAgentBrowserConfigPaths({ cwd: options.cwd, env });
|
|
24
|
-
const includeProjectConfig = options.includeProjectConfig !== false;
|
|
25
|
-
const errors = [];
|
|
26
|
-
const warnings = [];
|
|
27
|
-
const layerCandidates = [
|
|
28
|
-
{ path: paths.global, scope: "global" },
|
|
29
|
-
...(includeProjectConfig ? [{ path: paths.project, scope: "project" }] : []),
|
|
30
|
-
...(paths.override ? [{ path: paths.override, scope: "override" }] : []),
|
|
31
|
-
];
|
|
32
|
-
const layers = [];
|
|
33
|
-
let mergedConfig = {};
|
|
34
|
-
for (const candidate of layerCandidates) {
|
|
35
|
-
const layer = await readConfigLayer(candidate.path, candidate.scope, errors, warnings);
|
|
36
|
-
if (!layer)
|
|
37
|
-
continue;
|
|
38
|
-
layers.push(layer);
|
|
39
|
-
mergedConfig = mergeAgentBrowserConfig(mergedConfig, layer.config);
|
|
40
|
-
}
|
|
41
|
-
return buildAgentBrowserConfigState({
|
|
42
|
-
env,
|
|
43
|
-
errors,
|
|
44
|
-
layers,
|
|
45
|
-
mergedConfig,
|
|
46
|
-
paths,
|
|
47
|
-
projectConfigIncluded: includeProjectConfig,
|
|
48
|
-
warnings,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
6
|
export function loadAgentBrowserConfigSync(options = {}) {
|
|
52
7
|
return loadAgentBrowserConfigStateSync(options);
|
|
53
8
|
}
|
|
@@ -70,7 +25,7 @@ async function resolveCommandCredential(rawValue, signal) {
|
|
|
70
25
|
throw new Error("Credential command failed without exposing command output. Check pi-agent-browser-config web-search status and the configured secret manager command.");
|
|
71
26
|
}
|
|
72
27
|
}
|
|
73
|
-
|
|
28
|
+
async function resolveCredentialSource(source, options = {}) {
|
|
74
29
|
if (!source)
|
|
75
30
|
return undefined;
|
|
76
31
|
let value;
|
|
@@ -6,8 +6,8 @@ import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
|
|
|
6
6
|
import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
|
|
7
7
|
import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
|
|
8
8
|
export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
|
+
const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
|
|
11
11
|
const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
12
12
|
export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
|
|
13
13
|
const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"];
|
|
@@ -46,7 +46,7 @@ function policyEntryMatchesApp(entry, app) {
|
|
|
46
46
|
return false;
|
|
47
47
|
return appIdentifiers(app).some((identifier) => identifier.toLowerCase().includes(normalizedEntry));
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
function evaluateElectronLaunchPolicy(options) {
|
|
50
50
|
const denyEntry = options.deny?.find((entry) => policyEntryMatchesApp(entry, options.target));
|
|
51
51
|
if (denyEntry) {
|
|
52
52
|
return {
|
|
@@ -66,7 +66,7 @@ export function evaluateElectronLaunchPolicy(options) {
|
|
|
66
66
|
}
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
async function resolveElectronLaunchTarget(options) {
|
|
70
70
|
if (options.appPath)
|
|
71
71
|
return inspectElectronAppPath(options.appPath);
|
|
72
72
|
if (options.executablePath)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { JsonSchema } from "../json-schema.js";
|
|
2
2
|
import { StringEnum as localStringEnum } from "../string-enum-schema.js";
|
|
3
3
|
import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS } from "../electron/discovery.js";
|
|
4
|
-
import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
|
-
|
|
6
|
-
export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
|
|
4
|
+
import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
|
+
const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
|
|
7
6
|
function outputProperties(Type) {
|
|
8
7
|
return {
|
|
9
8
|
outputPath: Type.Optional(Type.String({ description: "Result-data path, separate from browser artifact paths.", minLength: 1 })),
|
|
@@ -2,20 +2,34 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import which from "which";
|
|
5
6
|
import { isRecord } from "../parsing.js";
|
|
6
7
|
import { extractExplicitNamespace, extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
7
8
|
import { getUpstreamEffectiveBatchSteps } from "../orchestration/batch-stdin.js";
|
|
8
9
|
import { extractUpstreamCommandTokens } from "../argv-descriptor.js";
|
|
9
10
|
import { isCloseAllCommand } from "../command-taxonomy.js";
|
|
10
11
|
import { redactSensitiveText, validateToolArgs } from "../runtime.js";
|
|
12
|
+
import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS } from "./types.js";
|
|
11
13
|
export const AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES = 64 * 1_024;
|
|
12
14
|
export const AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
13
15
|
export const AGENT_BROWSER_SCRIPT_NAMESPACE = "";
|
|
14
|
-
export const AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS = 300_000;
|
|
15
16
|
export const AGENT_BROWSER_SCRIPT_MAX_CALLS = 25;
|
|
16
17
|
export const AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES = 64 * 1_024;
|
|
17
18
|
export const AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES = 1 * 1_024 * 1_024;
|
|
18
|
-
|
|
19
|
+
const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the Node runtime that hosts the permissioned code child. A Bun-compiled Pi binary
|
|
22
|
+
* points `process.execPath` at the pi executable, which rejects Node-only flags like
|
|
23
|
+
* `--permission`, so those hosts must spawn a real `node` from PATH instead.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveScriptChildNodePath(options) {
|
|
26
|
+
if (options.runtime.bun === undefined)
|
|
27
|
+
return options.execPath;
|
|
28
|
+
const nodePath = options.whichNode();
|
|
29
|
+
if (nodePath === null)
|
|
30
|
+
throw new Error("agent_browser_code requires a `node` runtime on PATH when pi runs on a Bun binary.");
|
|
31
|
+
return nodePath;
|
|
32
|
+
}
|
|
19
33
|
function findPackageRoot(startDir) {
|
|
20
34
|
let currentDir = startDir;
|
|
21
35
|
for (;;) {
|
|
@@ -34,7 +48,7 @@ function resolveScriptWorkerPath() {
|
|
|
34
48
|
return workerPath;
|
|
35
49
|
}
|
|
36
50
|
const SCRIPT_SESSION_NAME_PATTERN = /^piab-script-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
37
|
-
|
|
51
|
+
function validateAgentBrowserScriptSource(input) {
|
|
38
52
|
if (typeof input !== "string")
|
|
39
53
|
return { error: "script must be a string." };
|
|
40
54
|
const bytes = Buffer.byteLength(input, "utf8");
|
|
@@ -175,8 +189,8 @@ export async function runAgentBrowserScript(options) {
|
|
|
175
189
|
return buildFailedRun({ callCount: 0, emitCount: 0, error: compiled.error, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
176
190
|
}
|
|
177
191
|
const timeoutMs = options.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
|
|
178
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs >
|
|
179
|
-
return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${
|
|
192
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > AGENT_BROWSER_CODE_MAX_TIMEOUT_MS) {
|
|
193
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${AGENT_BROWSER_CODE_MAX_TIMEOUT_MS}.`, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
180
194
|
}
|
|
181
195
|
if (options.signal?.aborted) {
|
|
182
196
|
return buildFailedRun({ aborted: true, callCount: 0, emitCount: 0, error: "Script execution was aborted.", failureCategory: "aborted", rejectedCallCount: 0, steps: [] });
|
|
@@ -189,7 +203,15 @@ export async function runAgentBrowserScript(options) {
|
|
|
189
203
|
const message = error instanceof Error ? error.message : "Compiled script worker is missing.";
|
|
190
204
|
return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
|
|
191
205
|
}
|
|
192
|
-
|
|
206
|
+
let childNodePath;
|
|
207
|
+
try {
|
|
208
|
+
childNodePath = resolveScriptChildNodePath({ runtime: { bun: process.versions.bun }, execPath: process.execPath, whichNode: () => which.sync("node", { nothrow: true }) });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : "No Node runtime is available for the code child.";
|
|
212
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
|
|
213
|
+
}
|
|
214
|
+
const child = spawn(childNodePath, [
|
|
193
215
|
"--permission",
|
|
194
216
|
"--max-old-space-size=64",
|
|
195
217
|
workerPath,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const DEFAULT_SESSION_MODE = "auto";
|
|
2
|
+
export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
|
|
2
3
|
export const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select"];
|
|
3
4
|
export const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"];
|
|
4
|
-
|
|
5
|
-
export const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
|
|
5
|
+
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
|
|
6
6
|
export const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"];
|
|
7
7
|
export const AGENT_BROWSER_ELECTRON_ACTIONS = ["list", "launch", "status", "cleanup", "probe"];
|
|
8
8
|
export const AGENT_BROWSER_ELECTRON_HANDOFFS = ["connect", "tabs", "snapshot"];
|
|
@@ -82,9 +82,6 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
|
|
|
82
82
|
export async function withOwnedManagedSessionContext(context, run) {
|
|
83
83
|
return await ownedManagedSessionStorage.run(context, run);
|
|
84
84
|
}
|
|
85
|
-
export function getOwnedManagedSessionRestoreKey() {
|
|
86
|
-
return ownedManagedSessionStorage.getStore()?.restoreKey;
|
|
87
|
-
}
|
|
88
85
|
export function resolveOwnedManagedSessionContext(options) {
|
|
89
86
|
const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
|
|
90
87
|
const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
|
|
@@ -115,7 +112,7 @@ function hasExplicitConfigArg(args) {
|
|
|
115
112
|
function closesBrowserSession(args) {
|
|
116
113
|
return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
|
|
117
114
|
}
|
|
118
|
-
|
|
115
|
+
function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
|
|
119
116
|
return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
|
|
120
117
|
}
|
|
121
118
|
/** Caller-selected upstream config disables the wrapper's automatic restore injection without blocking that config. */
|
|
@@ -139,12 +139,3 @@ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
|
139
139
|
return argumentSteps;
|
|
140
140
|
return parseUserBatchStdin(stdin).steps ?? [];
|
|
141
141
|
}
|
|
142
|
-
export function parseValidBatchStepEntries(stdin) {
|
|
143
|
-
const parsed = parseBatchStdinJsonArray(stdin);
|
|
144
|
-
if (parsed.error || parsed.steps === undefined)
|
|
145
|
-
return [];
|
|
146
|
-
return parsed.steps.flatMap((step, index) => {
|
|
147
|
-
const validated = validateUserBatchStep(step, index);
|
|
148
|
-
return validated.ok ? [{ index, step: validated.step }] : [];
|
|
149
|
-
});
|
|
150
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getSessionArtifactManifestEntryKey, isPendingRecordingCommand, mergeSessionArtifactManifest } from "../../results/artifact-manifest.js";
|
|
2
|
+
export function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
3
|
+
if (!updated || updated === initial)
|
|
4
|
+
return current;
|
|
5
|
+
if (current === initial)
|
|
6
|
+
return updated;
|
|
7
|
+
const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
|
|
8
|
+
const changedEntries = updated.entries
|
|
9
|
+
.map((entry, index) => ({ entry, index }))
|
|
10
|
+
.filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
|
|
11
|
+
.sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
|
|
12
|
+
|| Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
|
|
13
|
+
|| left.index - right.index)
|
|
14
|
+
.map(({ entry }) => entry);
|
|
15
|
+
return changedEntries.length === 0
|
|
16
|
+
? current
|
|
17
|
+
: mergeSessionArtifactManifest({
|
|
18
|
+
base: current,
|
|
19
|
+
entries: changedEntries,
|
|
20
|
+
nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -566,7 +566,7 @@ export function formatQaAttachedTargetText(target) {
|
|
|
566
566
|
return undefined;
|
|
567
567
|
return ["QA attached target:", target.sessionName, target.title, target.url].filter((part) => typeof part === "string" && part.length > 0).join(" — ");
|
|
568
568
|
}
|
|
569
|
-
|
|
569
|
+
function buildQaAttachedRecoveryNextActions(sessionName) {
|
|
570
570
|
const sessionArgs = (args) => withOptionalSessionArgs(sessionName, args);
|
|
571
571
|
return [
|
|
572
572
|
buildNextToolAction({
|
|
@@ -26,13 +26,13 @@ export function buildMissingBinaryMessage() {
|
|
|
26
26
|
].join("\n");
|
|
27
27
|
}
|
|
28
28
|
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set(["try-button-name-candidate", "try-link-name-candidate"]);
|
|
29
|
-
|
|
29
|
+
function formatSemanticActionCandidateText(actions) {
|
|
30
30
|
const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
|
|
31
31
|
if (candidateActions.length === 0)
|
|
32
32
|
return undefined;
|
|
33
33
|
return ["Agent-browser candidate fallbacks:", ...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`)].join("\n");
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function buildSemanticActionCandidateActions(compiled) {
|
|
36
36
|
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
37
37
|
if (commandIndex < 0 || compiled.args[commandIndex] !== "find")
|
|
38
38
|
return [];
|
|
@@ -333,7 +333,7 @@ function resolveSemanticActionVisibleRefArgsFromSnapshot(compiled, snapshotData)
|
|
|
333
333
|
return undefined;
|
|
334
334
|
return { args: [...getCompiledSemanticActionSessionPrefix(compiled), ...resolution.args], snapshot: resolution.snapshot };
|
|
335
335
|
}
|
|
336
|
-
|
|
336
|
+
async function resolveSemanticActionVisibleRefArgs(options) {
|
|
337
337
|
if (!options.compiled || !options.sessionName)
|
|
338
338
|
return undefined;
|
|
339
339
|
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../argv-grammar.js";
|
|
3
|
+
// Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
|
|
4
|
+
export class AsyncExecutionQueue {
|
|
5
|
+
tail = Promise.resolve();
|
|
6
|
+
active = new AsyncLocalStorage();
|
|
7
|
+
isCurrent() { return this.active.getStore()?.active === true; }
|
|
8
|
+
run(work, signal, barrier = Promise.resolve()) {
|
|
9
|
+
if (this.isCurrent()) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
return work();
|
|
12
|
+
}
|
|
13
|
+
const previous = this.tail;
|
|
14
|
+
let release;
|
|
15
|
+
this.tail = new Promise((resolve) => {
|
|
16
|
+
release = resolve;
|
|
17
|
+
});
|
|
18
|
+
let rejectWaiting;
|
|
19
|
+
const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
|
|
20
|
+
const abortWaiting = () => rejectWaiting(signal?.reason);
|
|
21
|
+
signal?.addEventListener("abort", abortWaiting, { once: true });
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
abortWaiting();
|
|
24
|
+
const execution = (async () => {
|
|
25
|
+
await Promise.all([previous, barrier]);
|
|
26
|
+
signal?.removeEventListener("abort", abortWaiting);
|
|
27
|
+
try {
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
const scope = { active: true };
|
|
30
|
+
try {
|
|
31
|
+
return await this.active.run(scope, work);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
scope.active = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
release();
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return signal ? Promise.race([execution, cancelled]) : execution;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class KeyedAsyncExecutionQueue {
|
|
45
|
+
barriers = new Map();
|
|
46
|
+
entries = new Map();
|
|
47
|
+
async run(key, namespace, work, signal) {
|
|
48
|
+
const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
|
|
49
|
+
if (entry.queue.isCurrent())
|
|
50
|
+
return work();
|
|
51
|
+
const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
|
|
52
|
+
entry.users += 1;
|
|
53
|
+
this.entries.set(key, entry);
|
|
54
|
+
try {
|
|
55
|
+
return await entry.queue.run(work, signal, barrier);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
entry.users -= 1;
|
|
59
|
+
if (entry.users === 0 && this.entries.get(key) === entry)
|
|
60
|
+
this.entries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async runExclusive(namespace, work) {
|
|
64
|
+
const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
|
|
65
|
+
const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
|
|
66
|
+
let release;
|
|
67
|
+
const blocked = new Promise((resolve) => {
|
|
68
|
+
release = resolve;
|
|
69
|
+
});
|
|
70
|
+
const barrier = previous.then(() => blocked);
|
|
71
|
+
this.barriers.set(namespaceKey, barrier);
|
|
72
|
+
const drains = [...this.entries]
|
|
73
|
+
.filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
74
|
+
.map(([, { queue }]) => queue.run(async () => undefined));
|
|
75
|
+
await previous;
|
|
76
|
+
await Promise.all(drains);
|
|
77
|
+
try {
|
|
78
|
+
return await work();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
release();
|
|
82
|
+
if (this.barriers.get(namespaceKey) === barrier)
|
|
83
|
+
this.barriers.delete(namespaceKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -157,7 +157,7 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
|
|
|
157
157
|
}
|
|
158
158
|
return text;
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
function formatAgentBrowserRenderResult(result, options, theme, isError) {
|
|
161
161
|
if (options.isPartial) {
|
|
162
162
|
return theme.fg("warning", "Running agent-browser...");
|
|
163
163
|
}
|
|
@@ -290,9 +290,6 @@ export async function getAgentBrowserSocketDirValidationError(socketDir, uid = t
|
|
|
290
290
|
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
|
-
export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
294
|
-
return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
|
|
295
|
-
}
|
|
296
293
|
export function getAgentBrowserSocketPathValidationError(options) {
|
|
297
294
|
if ((options.platform ?? processPlatform) === "win32")
|
|
298
295
|
return undefined;
|
|
@@ -7,8 +7,8 @@ export function isPendingRecordingArtifact(artifact) {
|
|
|
7
7
|
return artifact.recordingState === "openRecording" || artifact.status === "pending"
|
|
8
8
|
|| (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
|
|
11
|
+
const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
|
|
12
12
|
export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
|
|
13
13
|
function parsePositiveSafeInteger(value) {
|
|
14
14
|
if (value === undefined)
|
|
@@ -31,7 +31,7 @@ function getRouteDiagnosticReason(item, route) {
|
|
|
31
31
|
return "unfulfilled-routed-request";
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
function getNetworkRouteMode(args) {
|
|
35
35
|
if (args.includes("--abort"))
|
|
36
36
|
return "abort";
|
|
37
37
|
if (args.includes("--body"))
|
|
@@ -3,7 +3,7 @@ export function getStringRecordField(value, key) {
|
|
|
3
3
|
const field = value[key];
|
|
4
4
|
return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
function getNetworkRequestUrlPath(url) {
|
|
7
7
|
if (!url)
|
|
8
8
|
return undefined;
|
|
9
9
|
try {
|
|
@@ -68,7 +68,7 @@ function getStatefulCommandSensitiveValues(command) {
|
|
|
68
68
|
return [];
|
|
69
69
|
const tokens = extractUpstreamCommandTokens(command);
|
|
70
70
|
const values = [];
|
|
71
|
-
if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
|
|
71
|
+
if (tokens[0] === "cookies" && tokens[1] === "set" && !tokens.slice(2).includes("--curl") && tokens[3])
|
|
72
72
|
values.push(tokens[3]);
|
|
73
73
|
if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4])
|
|
74
74
|
values.push(tokens[4]);
|
|
@@ -507,7 +507,7 @@ export function formatNetworkRouteDiagnosticsText(diagnostics) {
|
|
|
507
507
|
lines.push("If this route is intended as a mock, inspect the request/headers and treat failed, pending, or CORS-looking rows as unfulfilled until a mocked response is observed.");
|
|
508
508
|
return lines.join("\n");
|
|
509
509
|
}
|
|
510
|
-
|
|
510
|
+
function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
|
|
511
511
|
const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
|
|
512
512
|
if (!diagnostic)
|
|
513
513
|
return undefined;
|
|
@@ -19,7 +19,7 @@ function formatSemanticActionTarget(compiled) {
|
|
|
19
19
|
const target = `${locator} ${quotedValue}`;
|
|
20
20
|
return name ? `${target} (name ${JSON.stringify(name)})` : target;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
function formatSemanticActionCompactLine(compiled) {
|
|
23
23
|
const target = formatSemanticActionTarget(compiled);
|
|
24
24
|
switch (compiled.action) {
|
|
25
25
|
case "click":
|
|
@@ -34,7 +34,7 @@ export function formatSemanticActionCompactLine(compiled) {
|
|
|
34
34
|
return `${compiled.action}: ${target}`;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
function resolveSemanticPresentationCommand(compiled) {
|
|
38
38
|
if (!compiled || !SEMANTIC_PRESENTATION_ACTIONS.has(compiled.action))
|
|
39
39
|
return undefined;
|
|
40
40
|
if (compiled.action === "select")
|
|
@@ -256,6 +256,6 @@ export function formatRichInputRecoveryText(diagnostic) {
|
|
|
256
256
|
`- ${diagnostic.inputMethodHint}`,
|
|
257
257
|
].join("\n");
|
|
258
258
|
}
|
|
259
|
-
|
|
259
|
+
function normalizeSemanticActionAccessibleName(name) {
|
|
260
260
|
return name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
261
261
|
}
|
|
@@ -254,7 +254,7 @@ function redactBearerCredentials(text) {
|
|
|
254
254
|
return formatRedactedCredential(label, credential, trailing);
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
|
-
|
|
257
|
+
function isSensitiveFieldName(key) {
|
|
258
258
|
SENSITIVE_FIELD_NAME_PATTERN.lastIndex = 0;
|
|
259
259
|
return SENSITIVE_FIELD_NAME_PATTERN.test(key);
|
|
260
260
|
}
|
|
@@ -347,7 +347,9 @@ export function redactInvocationArgs(args) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
|
-
if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set"
|
|
350
|
+
if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set"
|
|
351
|
+
&& !extractUpstreamCommandTokens(args).slice(2).includes("--curl")
|
|
352
|
+
&& redacted[commandStartIndex + 3] !== undefined) {
|
|
351
353
|
redacted[commandStartIndex + 3] = "[REDACTED]";
|
|
352
354
|
}
|
|
353
355
|
if (commandStartIndex !== undefined
|
|
@@ -62,7 +62,7 @@ function extractStringResultField(data, fieldName) {
|
|
|
62
62
|
const text = data[fieldName].trim();
|
|
63
63
|
return text.length > 0 ? text : undefined;
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
function extractSessionTabTargetFromData(data) {
|
|
66
66
|
const directTarget = normalizeSessionTabTarget({
|
|
67
67
|
targetId: isRecord(data) && typeof data.targetId === "string" ? data.targetId : undefined,
|
|
68
68
|
title: extractStringResultField(data, "title"),
|
|
@@ -5,20 +5,20 @@ import { redactSensitiveText } from "./runtime.js";
|
|
|
5
5
|
import { StringEnum as localStringEnum } from "./string-enum-schema.js";
|
|
6
6
|
import { DEFAULT_WEB_SEARCH_PROVIDER, EXA_SEARCH_TYPES, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
|
|
7
7
|
export const AGENT_BROWSER_WEB_SEARCH_TOOL_NAME = "agent_browser_web_search";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
|
9
|
+
const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
|
|
10
|
+
const DEFAULT_SEARCH_RESULT_COUNT = 5;
|
|
11
|
+
const MAX_SEARCH_RESULT_COUNT = 10;
|
|
12
|
+
const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
|
|
13
|
+
const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
|
|
14
|
+
const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
|
|
15
|
+
const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
|
|
16
|
+
const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
|
|
17
17
|
export const WEB_SEARCH_MIN_REQUEST_INTERVAL_MS = 1_100;
|
|
18
18
|
export const EXA_SEARCH_SYSTEM_PROMPT = "Prefer primary, official sources. Respect any requested version or date. Avoid duplicate or equivalent results.";
|
|
19
19
|
export { EXA_SEARCH_TYPES };
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
|
|
21
|
+
const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
|
|
22
22
|
const MAX_EXA_DOMAIN_FILTERS = 20;
|
|
23
23
|
const MAX_EXA_ADDITIONAL_QUERIES = 10;
|
|
24
24
|
export function createAgentBrowserWebSearchParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
|
|
@@ -193,7 +193,7 @@ export function cleanSearchText(value, maxLength = 500) {
|
|
|
193
193
|
return cleaned;
|
|
194
194
|
return `${cleaned.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
function normalizeSearchUrl(value) {
|
|
197
197
|
if (typeof value !== "string")
|
|
198
198
|
return undefined;
|
|
199
199
|
try {
|
|
@@ -258,7 +258,7 @@ export function normalizeExaSearchResult(result) {
|
|
|
258
258
|
function getProviderLabel(provider) {
|
|
259
259
|
return provider === "exa" ? "Exa" : "Brave";
|
|
260
260
|
}
|
|
261
|
-
|
|
261
|
+
function formatSearchResults(provider, query, results) {
|
|
262
262
|
const providerLabel = getProviderLabel(provider);
|
|
263
263
|
if (results.length === 0) {
|
|
264
264
|
return `No ${providerLabel} web results found for: ${query}`;
|
|
@@ -538,14 +538,14 @@ const EXA_WEB_SEARCH_ADAPTER = {
|
|
|
538
538
|
};
|
|
539
539
|
},
|
|
540
540
|
};
|
|
541
|
-
|
|
541
|
+
const WEB_SEARCH_PROVIDER_ADAPTERS = {
|
|
542
542
|
exa: EXA_WEB_SEARCH_ADAPTER,
|
|
543
543
|
brave: BRAVE_WEB_SEARCH_ADAPTER,
|
|
544
544
|
};
|
|
545
545
|
export function getWebSearchProviderAdapter(provider) {
|
|
546
546
|
return WEB_SEARCH_PROVIDER_ADAPTERS[provider];
|
|
547
547
|
}
|
|
548
|
-
|
|
548
|
+
function dedupeSearchResults(results) {
|
|
549
549
|
const seen = new Set();
|
|
550
550
|
return results.filter((result) => {
|
|
551
551
|
if (seen.has(result.url))
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -46,15 +46,15 @@ The extension should:
|
|
|
46
46
|
|
|
47
47
|
The child has an empty environment, Node permission mode without host capability allowlists, a 64 MiB heap ceiling, and a VM with string/WebAssembly code generation disabled. Null-prototype `browser`, `emit`, and `emitImage` functions use JSON-only IPC; parent promises/objects/functions are not exposed. There are no imports or host filesystem/network/process/timer APIs. Node's VM alone is not a security boundary. The retained child isolation is distinct from the selected browser's persistent authentication and native capabilities; no QuickJS, broker, second driver, or provider payload injection is involved.
|
|
48
48
|
|
|
49
|
-
Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
|
|
49
|
+
Limits are 25 attempted calls, 64 KiB source/final JSON, 1 MiB per IPC message, 8 MiB cumulative IPC, and a 120-second default/300-second maximum deadline. `lib/input-modes/types.ts` owns the maximum shared by the public code schema and runner. `browser()` returns a canonical observation. `emit` selects JSON; `emitImage` selects a cell-local verified image handle, with file identity rechecks and normal inline limits. `lib/orchestration/script-mode.ts` aggregates selected output, failures, and artifact receipts; retired isolated-session leases are normalized by the offline converter into UUID-owned cleanup facts.
|
|
50
50
|
|
|
51
51
|
### Ordered transitions and shared execution
|
|
52
52
|
|
|
53
|
-
`lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier.
|
|
53
|
+
`lib/managed-session-policy-lock.ts` supplies cooperating-process execution coordination keyed by the **actual socket context**, canonical namespace, and session, separate from existing managed-daemon policy checks. The lock spans direct helper/action/state work and an entire code cell. Fresh replacement and multi-identity cleanup acquire sorted identity sets; namespace-wide direct close uses an exclusive namespace barrier. `lib/orchestration/execution-queue.ts` owns the existing local managed/artifact queues and keyed namespace barriers; branch-generation checks still protect in-process ownership/replay. Independent identities remain concurrent. Direct calls bound coordination waits using the existing configured/command-aware timeout, then retain their native subprocess and Electron action watchdogs. Code owns a whole-cell deadline, including local queue waits. Independently bounded cleanup can borrow an already-held covered claim after cancellation, but cannot acquire a new identity or release the outer claim.
|
|
54
54
|
|
|
55
55
|
All participating Pi processes must restart with the updated package. The lock is not a browser transaction, rollback, or a guarantee against humans, third-party native clients, older extensions, or separately named sessions attached to one external Chrome. Separate tool calls still need workflow coordination.
|
|
56
56
|
|
|
57
|
-
`lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
|
|
57
|
+
`lib/browser-transcript.ts` defines one canonical `data.event` format and optional once-stored `data.snapshot`. Direct, advanced, code and Electron host operations record begin/finish receipts before dispatch and after observed effects, using outer tool-call ID plus inner index and a unique operation ID. The shared page reducer retains an inaccessible candidate during unresolved work and requires explicit reuse/replace/invalidate/unknown disposition. Ordinary results omit unchanged maps and unrelated artifact receipts; journal events carry redacted artifact-row changes in commit order. Final rendering queues actual spill writes and manifest publication, allowing unrelated small observations to finish while another namespace holds its artifact/lifecycle operation. `lib/orchestration/browser-run/artifact-merge.ts` preserves concurrent three-way manifest updates and recording lifecycle order. Native lifecycle evidence is retained before presentation/export. Failed begin blocks dispatch; failed finish stops dependent work and requires inspection without mutation replay. A named persistent journal requires actual publication even before Pi's first conversation. Disabled/no-file sessions use the native current in-memory ancestry for direct browser work without claiming durability; code still requires a published intent journal. Observation-only artifact receipts are optional without a published journal, so sessionless local results retain complete redacted private spills and selected files. Code lifecycle joins include outer finalization, and the shared generation guard keeps prior-branch observation spills process-private and prevents their artifact publication onto the new branch. JavaScript variables are never persisted.
|
|
58
58
|
|
|
59
59
|
`lib/browser-journal.ts` token-projects published envelopes and ancestry through public Pi file/header/leaf APIs, loading only winning snapshot definitions. It captures the selected leaf before asynchronous reads and validates that same branch through public `getEntry()` ancestry on both hosts. Capture-local admission reuses a validated append-only prefix while checking endpoint identity/parent and new suffixes; publication/durability checks remain independent. Startup/tree restoration uses one queued captured replay for branch state and cleanup leases, with one per-session launch-configuration fold. Stock official 1.0.0 still eagerly loads native history before extension startup. Legacy adapters live only in `lib/browser-session-conversion.ts`, which publishes a validated stopped separate copy while retaining the source. Native generation checks use PID/start and socket identity, never saved URL or delta revision alone. Filtered/helper captures share this check. Small owner-qualified launch receipts retain observed restore-key provenance; restart verifies the current native generation and key before treating that provenance as live. Full storage, migration and consumer-memory boundaries are documented in [`SESSION_STORAGE.md`](SESSION_STORAGE.md).
|
|
60
60
|
|
|
@@ -266,7 +266,7 @@ Upstream restore-state persistence remains upstream-owned. The wrapper passes an
|
|
|
266
266
|
- subprocess execution and JSON parsing through `buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`: copies the parent process environment so user-approved provider credentials and other runtime variables reach upstream, then applies wrapper overrides such as the managed socket directory and clamped default operation timeout
|
|
267
267
|
- clear missing-binary errors
|
|
268
268
|
- filtered `network requests --current-page` / `--current-origin` / `--current-url` preparation results use the same `redactPresentationData` credential redactor as ordinary diagnostics before populating `details.data` or writing `outputPath`
|
|
269
|
-
- compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set
|
|
269
|
+
- compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set` while distinguishing native `--curl` file-import operands from inline cookie values (including failed batch diagnostics), and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors. Shared URL redaction covers SAMLRequest/SAMLResponse/RelayState and auth-context-only state/nonce at model/persisted-artifact boundaries while retaining exact internal page-target URLs; snapshot spills are redacted before disk writes
|
|
270
270
|
- bounded machine-readable outcome metadata on tool `details` (`resultCategory`, `successCategory`, `failureCategory`, optional `nextActions`, optional `pageChangeSummary` with observed-vs-dispatched evidence and per-step summaries on `batch`, optional `artifactVerification` with the same shape on each successful `batchSteps[]` row) so agents can branch without parsing prose; enums, classifier precedence, and generic follow-up payloads are implemented under `extensions/agent-browser/lib/results/` in focused modules (`contracts.ts` for shared types, `categories.ts` for `classifyAgentBrowserSuccessCategory` / `classifyAgentBrowserFailureCategory` / `buildAgentBrowserResultCategoryDetails`, `action-recommendations.ts` for `buildAgentBrowserNextActions`, `next-actions.ts` for the `AgentBrowserNextAction` shape and merge helpers, `recovery-actions.ts` for recovery id registries and `buildRecoveryNextActions`, `network.ts` for `classifyNetworkRequestFailure` / `summarizeNetworkFailures`, and related helpers). Per-session tab target, `refSnapshot` alignment, invalidation, and tab pinning observations flow through `extensions/agent-browser/lib/session-page-state.ts` from `extensions/agent-browser/index.ts`. Compact page-change summaries and artifact verification rollups are built in `extensions/agent-browser/lib/results/presentation.ts` (`buildPageChangeSummary`, `buildArtifactVerificationSummary`), and the human contract lives in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). Final public results return native `isError` after all wrapper reclassifications, without a `tool_result` repair hook. Prose retains the visible category notice; explicit parseable JSON stays unchanged. `lib/native-output.ts` and `finalizeAgentBrowserFailure` in `lib/pi-tool-rendering.ts` keep structured observations, transcript status and TUI affordances aligned (transcript semantics in the same contract doc)
|
|
271
271
|
- inline screenshots/images for the plain `screenshot` command; other image-like saves (for example `diff screenshot`) still appear in `details.artifacts` and summaries but are not auto-inlined as Pi image attachments (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details))
|
|
272
272
|
- lightweight session convenience
|
|
@@ -759,6 +759,8 @@ These calls return plain text and stay stateless: the extension does not inject
|
|
|
759
759
|
| `cookies [get|set|clear]` | Manage cookies. Full set form: `cookies set <name> <value> --url <url> --domain <domain> --path <path> --httpOnly --secure --sameSite <Strict|Lax|None> --expires <timestamp>`; also supports `cookies set --curl <file>` for JSON, cURL, or bare Cookie-header bulk imports. |
|
|
760
760
|
| `storage <local|session>` | Manage web storage. |
|
|
761
761
|
|
|
762
|
+
`cookies set --curl <file>` selects native file-import mode even when `--curl` follows other set operands. Ordinary import paths remain visible in invocation echoes and failed batch diagnostics; inline cookie values, sensitive flags, and secret-bearing path text still redact. File contents are not echoed by this exception.
|
|
763
|
+
|
|
762
764
|
Privacy note: `cookies get` can expose real profile cookies. Do not run it against `--profile Default` or other authenticated profiles unless the user explicitly needs cookie inspection; prefer task-specific page actions and storage checks.
|
|
763
765
|
|
|
764
766
|
### WebMCP page tools
|
package/docs/RELEASE.md
CHANGED
|
@@ -274,7 +274,7 @@ These show up often in cloud dev boxes and scripted smokes; they are maintainer
|
|
|
274
274
|
| Topic | What to watch for | Mitigation |
|
|
275
275
|
| --- | --- | --- |
|
|
276
276
|
| **Pi CLI vs repo devDependencies** | Global `pi` older than the minimum Pi runtime floor for the release can change TUI behavior, `/reload`, package installs, and tool routing during lifecycle or checkout smokes. | Run `npm run doctor` and align `pi` with the current audited baseline before release gates (`pi update` or install the matching version). The published peer range stays wildcard per Pi package docs, and the doctor enforces the minimum Pi runtime floor before package validation. |
|
|
277
|
-
| **npm lockfile (`packageManager`)** | `package.json` pins **npm@12.
|
|
277
|
+
| **npm lockfile (`packageManager`)** | `package.json` pins **npm@12.2.0** for the Node 24.21.0 runtime baseline. Older npm versions can rewrite optional-platform metadata in `package-lock.json`; private registry proxies can also leak organization URLs into `resolved` entries. | Prefer `npx -y npm@12.2.0 install` when refreshing the lockfile; do not commit unrelated lockfile churn or WorkOS/private-registry URLs. |
|
|
278
278
|
| **`pi -p` / print mode** | Non-interactive `pi -p` may hang or emit no stdout for long real-browser smokes without a TTY. | Use **tmux**-driven interactive `pi` for release evidence and checkout smokes; reserve `-p` for short, non-browser checks. |
|
|
279
279
|
| **Real-browser cleanup** | `real-upstream`, Sauce Demo, and live-site runs can leave defunct Chrome/`agent-browser` children if a session aborts mid-flow. | Close via `agent_browser` / `agent-browser` `close`, kill stray tmux sessions, and remove temp screenshots/HARs under `/tmp` or your chosen artifact dirs. |
|
|
280
280
|
| **Automated prompt driving** | Grepping tmux pane text for words that also appear in the **user** prompt (`PASS`, `FAIL`, `checkout overview`, `Smoke result:`) can false-complete before the agent finishes. | Wait for pane idle (no `Working…`), `agent_browser close` / `Artifact lifecycle`, or JSONL tool results—not instruction phrases copied from the prompt. |
|
package/docs/SUPPORT_MATRIX.md
CHANGED
|
@@ -99,7 +99,7 @@ Browser evidence (RQ-0152) is covered by `test/agent-browser.browser-evidence.te
|
|
|
99
99
|
|
|
100
100
|
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.
|
|
101
101
|
|
|
102
|
-
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`).
|
|
102
|
+
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`). Focused Electron discovery, host-lifecycle, and branch-ownership 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-electron-branch-ownership.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.
|
|
103
103
|
|
|
104
104
|
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.
|
|
105
105
|
|
|
@@ -143,7 +143,7 @@ Re-run the gates below before each release. Dated outcomes in this table are his
|
|
|
143
143
|
| 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. |
|
|
144
144
|
| 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). |
|
|
145
145
|
| 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`. |
|
|
146
|
-
| 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. |
|
|
146
|
+
| Startup profile | `npm run verify -- startup-profile --samples <n>` clean-builds generated `dist/`, records direct package entrypoint import/factory timing in fresh Node processes using the same `scripts/startup-measurement.mjs` measurement and 250ms budget as the startup owner test (the test retains its Android-only 1000ms exception), 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. |
|
|
147
147
|
| Deterministic dogfood smoke | `npm run verify -- dogfood` clean-builds, then `scripts/verify-agent-browser-dogfood.ts` drives the native wrapper against a loopback HTTP fixture through persistent code aggregation, advanced QA/actions, native batch, screenshot artifact verification, and session close with the real `agent-browser` on `PATH`. | **Earlier 0.36.0 / package 0.6.2 / Pi 0.84.4:** pass on 2026-09-01; QA, script branching/cleanup, fresh/current opens, semantic clicks, job screenshot verification, and close all passed. |
|
|
148
148
|
| WebGPU and restore autosave | Live 0.31.2 feature probes validate the new upstream paths independently from ordinary browser dogfood. | **macOS:** `doctor --webgpu` passed Apple Metal render/readback and headless red-pixel screenshot checks; the official Hello Triangle sample exposed the Metal adapter and produced a verified non-black screenshot. **Ubuntu image:** `doctor --webgpu --headed --json` passed SwiftShader render/readback and red-pixel screenshot checks with the baked Vulkan/Mesa/Xvfb dependencies. **Restore autosave:** an idle headless page timer changed localStorage and the namespaced restore file contained the new value before close; test state was removed. Wrapper-owned headed launches default periodic autosave off to avoid upstream 0.33.2 visible temporary collector tabs; native close still saves, direct window close can lose newer state because headed browsers are exempt from idle shutdown, and explicit `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` opts in at daemon launch; its effective value persists across resume, and changing a running wrapper-owned headed daemon in either direction requires close plus a fresh launch. On 2026-08-06, a headed checkout dogfood run crossed `react.dev` → `example.com` → `react.dev`, idled past the autosave interval while recording, showed no temporary page switch in frame/contact-sheet review, and closed both sessions successfully; disposable evidence was removed. **Windows interactive desktop:** post-release validation on 2026-07-15 used a disposable clone of snapshot `57cc3c0d-7d7c-4a4c-9c57-8270d4174679`, a logged-in console session, `agent-browser 0.31.2`, and Edge 150. The headed doctor WebGPU render/readback subcheck passed in 0.99s and its decoded screenshot subcheck passed with `rgb(255,0,0)`; a separate `--webgpu --headed` launch opened the proof page and the Parallels console capture visibly recorded the red triangle (553,500 bright-red pixels; SHA-256 `f5a28f5336cbdfeb0ff557af9425458bddc6b266d3cc6b946de7b101a0b43288`). The full doctor remained nonzero only for the unrelated absence of a separately installed Chrome binary; the explicit Edge executable completed the launch and both WebGPU probes. Local evidence is under `.artifacts/windows-webgpu-interactive/20260715T150903Z/summary.json`; the disposable clone and temporary account were deleted afterward. |
|
|
149
149
|
| Android/Termux local bring-up | Non-release-blocking source validation uses Termux system Chromium through upstream's Linux `which chromium` discovery, the packaged Linux-musl arm64 CLI, real Pi managed-session smoke, deterministic dogfood, and the default unit/package gate. Electron desktop lifecycle is not applicable. | **Historical 0.6 local evidence (not 0.7 code qualification):** Android 17 / arm64 Termux passed implicit, explicit, and namespaced fresh sessions, managed restore, script, QA, semantic action, job, click, eval/batch stdin, screenshot attachment, recording dependencies, and cleanup; Android is not yet a Crabbox release target. |
|
|
@@ -160,7 +160,7 @@ Runtime floor note: package metadata requires Node 24.21.0 or newer, keeps optio
|
|
|
160
160
|
| --- | --- | --- | --- | --- | --- |
|
|
161
161
|
| Built-in skills | 19 canonical tokens from baseline section `skills`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#built-in-skills), generated baseline block, README proof section, release docs. | `needsManagedSession` keeps read-only skills inspection sessionless while preserving thin upstream passthrough; upstream `@agent-browser/sandbox` remains external package guidance, not a bundled wrapper dependency. | Runtime and extension-validation skills/provider matrix; real-upstream inspection/skills group. | Supported. |
|
|
162
162
|
| Core page, element, navigation, and extraction commands | Canonical tokens from baseline section `core-commands`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#core-page-and-element-commands), [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md), README quick start. | Thin passthrough with wrapper-owned JSON/session planning, ref guidance, artifact verification, page-change summaries, click-dispatch diagnostics only after native temporary-marker target identity readback (otherwise abstaining), inspection-only recovery for upstream overlay-blocked clicks, deterministic document scroll before wheel/no-op diagnostics and instant CSS-container movement, shorthand compilers, and redaction. | Real-upstream core matrix (including snapshot-ref select) plus fake core matrix for passthrough, ordering, diagnostics, and compiler validation. | Supported. Upstream semantics remain upstream-owned. |
|
|
163
|
-
| Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers
|
|
163
|
+
| Sessions, state, tabs, frames, dialogs, and windows | 28 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries redact credentials while preserving restore identifiers, every session/state list row, and ordinary `cookies set --curl` file operands in invocation echoes and failed batch diagnostics; explicit targets and paths pass through. Runtime retains state artifact verification, sessionless local command planning, automatic managed restore, native-CDP-ID tab recovery across duplicate URLs and transcript replay, reached failed-batch target invalidation/live verification, active-target refresh after tab close, and close alias cleanup. | Extension tab/ref tests, real-upstream stable-id/label tab lifecycle, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
|
|
164
164
|
| Network, storage, artifacts, diagnostics, and performance | Canonical tokens from baseline section `network-storage-artifacts-diagnostics`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage), diagnostic sections, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Thin passthrough plus native-owned click/download execution for all downloads (including loopback, Blob exports, and redirects), compact diagnostics with identical credential redaction for page/origin/URL-filtered details and exports, route-mock warnings, useful-but-redacted storage output, stream idempotency normalization, FPS/cursor/contact-sheet-aware recording destinations/pinning and contact-sheet image verification, truthful ref-refresh and missing-ffmpeg warnings, positive WebMCP navigation hints and catalog updates, sensitive-data redaction, timeout bounds, and cleanup-pair guidance. | Fake non-core matrix and safe real-upstream coverage for network/HAR, diff, trace/profiler, console/errors/highlight, stream, vitals, React missing-renderer, native FPS recording path/page checks and WebMCP navigation hints. | Supported. Environment-sensitive operations need suitable local/browser state. |
|
|
165
165
|
| Batch, auth, confirmations, setup, dashboard, devices, and AI commands | Canonical tokens from baseline section `batch-auth-setup-ai`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup), README security notes, release docs. | Native-tool batch stdin, generated QA/lookup batch plans, auth/confirmation redaction, sessionless local auth/setup/dashboard/doctor/plugin planning, upgrade-only native text normalization with error precedence and explicit-JSON results, plugin list/show JSON envelope normalization, bare-`mcp` validation with `mcp --help` preserved, timeout/cleanup guidance. | Parser/runtime plugin and MCP unit coverage; fake-upstream plugin list/show and MCP help/blocking coverage; registered upgrade text/success/nonzero/timeout/abort/JSON controls in `test/agent-browser.upgrade-output.test.ts`; real-upstream plugin list shape probe; structured input-mode tests. | Supported. Interactive side-effecting setup/auth/chat remains upstream-owned. `plugin` is local/sessionless; `mcp` is external-client-only except help; `auth login --credential-provider` resolves credentials via a plugin; `install --with-deps` failures remain upstream-owned. |
|
|
166
166
|
| Global flags, config, providers, policy, and environment | Canonical tokens from baseline section `options-and-env`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment), README provider/setup notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sessionmode), architecture/runtime docs. | Runtime preserves caller-requested JSON for early filters, scroll results, and preparation failures without output-file prose (help/version remains native text), and handles command discovery, value-flag prevalidation, launch-scoped flags, redacted echoes, fresh-session recovery hints, explicit sessions, provider/device launch-scoping, parent env forwarding with consistent managed idle-timeout/socket/operation-timeout overrides on top-level and helper subprocesses, subprocess completion, and package-owned Pi-scoped config for optional companion features. | Runtime tests for flags/planning/redaction/session behavior; process/ref tests for env consistency and stdio-linger completion; config/web-search/CLI tests; fake provider/specialized-skill matrix; package doctor. | Supported. Provider clouds, iOS/Appium, proxies, profiles, and credentials require external setup. |
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -321,7 +321,7 @@ The browser persists between code and direct calls. JavaScript variables do not.
|
|
|
321
321
|
|
|
322
322
|
Inner calls are serialized, including calls issued with `Promise.all`. The maximum is 25 attempted calls, 64 KiB emitted JSON, 1 MiB per IPC message, and 8 MiB cumulative IPC. Inner execution keeps full redacted data without full model rendering/spilling/image encoding; only selected outer output is rendered. An observation that cannot fit the bridge limit fails rather than silently discarding recovery or fabricating data.
|
|
323
323
|
|
|
324
|
-
Source runs in a fresh permissioned Node child with an empty child environment, 64 MiB V8 heap ceiling, and a VM context whose string/WebAssembly code generation is disabled. Task globals have null prototypes and JSON-only IPC; no parent promise/object/function is exposed. There are no host filesystem, network, process, import, or timer APIs. This language boundary is separate from native browser capabilities: parent-side browser subprocesses still receive ordinary auth/config/environment. `node:vm` alone is not a security boundary, and this contract is not a universal sandbox certification.
|
|
324
|
+
Source runs in a fresh permissioned Node child with an empty child environment, 64 MiB V8 heap ceiling, and a VM context whose string/WebAssembly code generation is disabled. Task globals have null prototypes and JSON-only IPC; no parent promise/object/function is exposed. There are no host filesystem, network, process, import, or timer APIs. When Pi itself runs as a Bun-compiled binary, the child is hosted by a real `node` runtime from `PATH`; if none is available the call fails as `missing-binary` with that exact remediation instead of hanging the handshake. This language boundary is separate from native browser capabilities: parent-side browser subprocesses still receive ordinary auth/config/environment. `node:vm` alone is not a security boundary, and this contract is not a universal sandbox certification.
|
|
325
325
|
|
|
326
326
|
#### Failure and persistence
|
|
327
327
|
|
|
@@ -618,7 +618,7 @@ Ref preflight details (command taxonomy in `extensions/agent-browser/lib/command
|
|
|
618
618
|
- **URL alignment:** `refSnapshot.target.url` and the session’s current tab URL are compared via `targetsMatch` / `normalizeComparableUrl` in `extensions/agent-browser/index.ts`: values are trimmed, parsed as URLs when possible, compared **after dropping the `#fragment`**, and the query string remains significant. If either side lacks a `url`, `targetsMatch` treats the pair as matching so early-session calls are not blocked.
|
|
619
619
|
- **Batch stdin ordering:** user `batch` JSON is scanned in order. Any step whose first token satisfies `isRefInvalidatingBatchCommand` sets a latch that blocks later steps whose first token satisfies `isRefGuardedCommand` and that mention `@e…` refs, except for same-snapshot native form-control steps whose current snapshot role metadata identifies all refs as safe controls (`check`/`uncheck` or direct `click`/`tap` on checkbox or radio refs, and `select` on combobox refs). A step whose first token is `snapshot` clears that latch for subsequent steps (pre-spawn intent only; it does not wait for upstream success). These predicates read explicit command capability flags from `command-taxonomy.ts`: navigation/mutation verbs such as `open` / `goto`, `reload`, non-form `click`, and related upstream commands have `invalidatesBatchRefs`, and `record start` steps (any outcome), `record restart` steps with a URL operand, plus WebMCP `invoke` / `result` / `cancel` steps also set the latch because upstream swaps or navigates the active page; same-snapshot `fill` rows and the role-checked native form-control rows stay guarded against missing/stale refs but do not set the latch, allowing ordinary form batches before a final click/submit step. Direct `click`/`tap @e…` is only treated as a safe form-control row when every ref in that step is a latest-snapshot checkbox or radio; other click/tap refs remain invalidating. Ref-guarded commands accept page-scoped refs for interaction (`click`, `fill`, `download`, `scrollintoview` / `scrollinto`, and others centralized in the command taxonomy). Changing either capability requires updating this contract, [`docs/SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md) `RQ-0072`/`RQ-0087` notes, README and command-reference pitfalls, and `test/agent-browser.extension-validation.test.ts`.
|
|
620
620
|
|
|
621
|
-
**Presentation redaction (implementation map):** Successful non-`batch` tool calls and each successful `batchSteps[]` row run upstream `data` through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation/diagnostics.ts`: `cookies` still walk objects/arrays and replace case-insensitive `value` keys with `"[REDACTED]"`; `storage` redacts values when the key or value looks credential-like (token, cookie, auth, secret, JWT, bearer/basic credential, high-entropy token-like string, or nested sensitive JSON) but keeps low-risk primitive QA values such as booleans, numbers, and short strings visible. Redacted storage entries add `valueRedacted` plus `valueRedactionReason` in `details.data`; diagnostic formatters mirror the same decision. Every other command’s payload is recursively scrubbed with the shared `redactSensitiveValue`, which redacts known sensitive key names and applies string-level sensitivity heuristics so network, diff, trace/profiler, stream, dashboard, chat, and other structured results do not echo bearer tokens, proxy credentials, or similar fields verbatim into `details.data`. Echoed `command` arrays in `details` and in batch roll-ups use `redactInvocationArgs` from `extensions/agent-browser/lib/runtime.ts` to mask trailing values for sensitive global flags (including `--body`, `--headers`, `--password`, and `--proxy`), preserve the special positional rules for `cookies set`, `storage local|session set`, and `set credentials
|
|
621
|
+
**Presentation redaction (implementation map):** Successful non-`batch` tool calls and each successful `batchSteps[]` row run upstream `data` through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation/diagnostics.ts`: `cookies` still walk objects/arrays and replace case-insensitive `value` keys with `"[REDACTED]"`; `storage` redacts values when the key or value looks credential-like (token, cookie, auth, secret, JWT, bearer/basic credential, high-entropy token-like string, or nested sensitive JSON) but keeps low-risk primitive QA values such as booleans, numbers, and short strings visible. Redacted storage entries add `valueRedacted` plus `valueRedactionReason` in `details.data`; diagnostic formatters mirror the same decision. Every other command’s payload is recursively scrubbed with the shared `redactSensitiveValue`, which redacts known sensitive key names and applies string-level sensitivity heuristics so network, diff, trace/profiler, stream, dashboard, chat, and other structured results do not echo bearer tokens, proxy credentials, or similar fields verbatim into `details.data`. Echoed `command` arrays in `details` and in batch roll-ups use `redactInvocationArgs` from `extensions/agent-browser/lib/runtime.ts` to mask trailing values for sensitive global flags (including `--body`, `--headers`, `--password`, and `--proxy`), preserve the special positional rules for `cookies set`, `storage local|session set`, and `set credentials` (native `cookies set --curl <file>` imports are file operands, so ordinary import paths remain visible in echoes and failed batch diagnostics), and scrub other argv tokens for URLs and inline secrets. Raw batch row strings are parsed and command-redacted for presentation and ordered intent/completion journals; execution argv remains unchanged. Failed batch steps additionally run `redactExactValues` on structured step errors so literals taken from that step’s argv (cookie value, storage set value, `--password` / `--password=` tokens) cannot reappear inside formatted error blobs. When the full batch is large enough to need its own aggregate spill, that spill reapplies these per-command data and argv redactors before persistence rather than using generic batch redaction.
|
|
622
622
|
|
|
623
623
|
`nextActions` is an optional list of exact follow-ups. Each entry has a `tool`, `id`, `reason`, optional `safety`, and executable `params` or an `artifactPath`. Native actions use `agent_browser` with argv/stdin; Electron and network-source actions use their actual advanced tool name and flat params. Enable the corresponding capability with `agent_browser_tools` if needed. Both prose and JSON expose recovery; oversized observations retain complete payloads in the named observation spill rather than inventing shortened calls.
|
|
624
624
|
|
|
@@ -699,7 +699,7 @@ Additional structured fields can appear when relevant:
|
|
|
699
699
|
- `attachedBrowserSession: true` on successful calls that establish or reuse a wrapper-tracked CDP/auto-connect/Electron attachment, and on a failed fresh attachment only when `managedSessionOutcome.activeAfter` proves its daemon remained active for cleanup. The marker restores attachment continuity from the active transcript branch, including that active-after-failure case; live state and transcript replay remove it after a terminal successful close/cleanup even when aggregate verification failed; a close followed by a later step whose lifecycle reports a browser launch preserves it, while a successful non-launching diagnostic leaves the close terminal. Caller config, environment, paths, and file-access settings remain upstream-owned; the marker only adds live-URL verification and lifecycle continuity.
|
|
700
700
|
- `lifecycle: { effectiveLaunch: { browserLaunched } }` when upstream returned that boolean. It separates starting the requested `agent-browser` CLI process from the effective Chrome session context. `readSource` exposes upstream's string `data.source` for direct `read` calls and identifies the raw HTTP fetch path; its lifecycle boolean can be `false` before a browser launch or `true` when the same managed session already has an active browser. Direct reads also append one visible `Read execution` line with the source, CLI-start result, managed browser lifecycle, and managed-session outcome so Pi models do not have to infer model-invisible details.
|
|
701
701
|
- `browserWindow: { mode: "headed", ownership: "wrapper-managed", sessionName, visibility: "unverified" }` only after a successful first/fresh local wrapper-managed headed call (including `batch`) that is not an attachment and whose lifecycle proves a browser launched and whose managed-session outcome is `created` or `replaced`. One visible handoff sentence tells the user to complete the login in that window if they can see it, then continue with `sessionMode: "auto"`; the field never claims OS desktop visibility.
|
|
702
|
-
- `sessionTabTarget` retains the observed `url`, optional `title`, and native CDP `targetId` when available. When an observed page lacks a native ID, a read-only `tab list` observes current identity; earlier commands and equal URLs cannot supply it. If another tab matches the observed URL while the active tab does not, existing URL-based drift recovery selects the matching tab instead of attaching the unrelated active ID. Resume and branch recovery select that exact target, even when several tabs share an address/title; a missing known ID never falls back to another same-address tab. Older transcripts without an ID retain URL-based recovery. An existing cold-managed-browser reopen records its new native ID. `sessionTabCorrection.selectionKind` can be `targetId`, `tabId`, `label`, or `index`.
|
|
702
|
+
- `sessionTabTarget` retains the observed `url`, optional `title`, and native CDP `targetId` when available. When an observed page lacks a native ID, a read-only `tab list` observes current identity; earlier commands and equal URLs cannot supply it. If another tab matches the observed URL while the active tab does not, existing URL-based drift recovery selects the matching tab instead of attaching the unrelated active ID. Resume and branch recovery select that exact target, even when several tabs share an address/title; a missing known ID never falls back to another same-address tab. Older transcripts without an ID retain URL-based recovery. An existing cold-managed-browser reopen records its new native ID. `sessionTabCorrection.selectionKind` can be `targetId`, `tabId`, `label`, or `index`. `sessionTabCorrection` records an applied tab selection (before the command or during post-command recovery), not proof of the final page or successful later recovery. Use `sessionTabTarget` for the final observed position and `aboutBlankSessionMismatch.recoveryApplied` for the recovery outcome; the historical correction may remain when that recovery fails.
|
|
703
703
|
- `sessionTabTargetUnknown: true` after a spawned `connect`, `state load`, history navigation, tab-selection/close, `window new`, or `diff url` call changes the active page without a trustworthy observed target. Direct `window new` / `diff url` and reached native batch rows retire the old target and refs; an intentional new blank window or an observed blank URL-diff destination never triggers old-tab recovery. URL diff inputs do not prove the final URL after redirects. Successful tab creation/selection/close, including dispatched batch rows, live-probes URL and a fresh non-blank title before state is committed, even when the new tab shares the prior URL; new blank tabs, explicitly selected `about:blank` tabs, and post-close blank targets are retained. It is persisted and restored across branch/reload replay, clears stale refs and tab pinning, and blocks page inspection until `get url` or explicit navigation observes a target; `tab list`, tab selection, close, and blocking-dialog `status` / `accept` / `dismiss` remain available. A timeout against an unknown target removes standalone snapshot actions and returns `verify-page-target-after-timeout`, a session-scoped `batch --bail` whose stdin runs `get url` before `snapshot -i`.
|
|
704
704
|
- `compiledSemanticAction` when the call used `agent_browser_action` and the result includes the unified `details` merge: `{ action, locator, args }` for `find` actions or `{ action: "select", selector?, locator?, values, args }` for `select`, with the same redaction rules as `args` / `effectiveArgs`; omitted for plain direct native calls and omitted on some early error returns that omit this field (see the `agent_browser_action` section above)
|
|
705
705
|
- `compiledJob` is internal QA batch-plan metadata: `{ args: ["batch", "--bail"], failFast: true, stdin, steps }`, with redacted step argv. It does not expose a public job input or compiler.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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)",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
73
73
|
"@earendil-works/pi-tui": "1.0.0",
|
|
74
74
|
"@types/cross-spawn": "6.0.6",
|
|
75
|
-
"@types/node": "24.19.
|
|
75
|
+
"@types/node": "24.19.1",
|
|
76
76
|
"@types/which": "2.0.2",
|
|
77
77
|
"tsx": "4.23.15",
|
|
78
78
|
"typebox": "1.3.27",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"startup-profile": "node ./scripts/profile-startup.mjs",
|
|
99
99
|
"prepare": "node ./scripts/prepare.mjs"
|
|
100
100
|
},
|
|
101
|
-
"packageManager": "npm@12.
|
|
101
|
+
"packageManager": "npm@12.2.0",
|
|
102
102
|
"peerDependenciesMeta": {
|
|
103
103
|
"@earendil-works/pi-ai": {
|
|
104
104
|
"optional": true
|
package/scripts/doctor.mjs
CHANGED
|
@@ -124,15 +124,23 @@ export function parseCliArgs(argv = process.argv.slice(2)) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
async function defaultRunAgentBrowser(args) {
|
|
127
|
-
const { stdout, stderr } = await execFile("agent-browser", args, { maxBuffer: 1024 * 1024 });
|
|
127
|
+
const { stdout, stderr } = await execFile("agent-browser", args, { maxBuffer: 1024 * 1024, ...buildNpmShimExecOptions(process.platform) });
|
|
128
128
|
return `${stdout}${stderr}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
async function defaultRunPi(args) {
|
|
132
|
-
const { stdout, stderr } = await execFile("pi", args, { maxBuffer: 1024 * 1024 });
|
|
132
|
+
const { stdout, stderr } = await execFile("pi", args, { maxBuffer: 1024 * 1024, ...buildNpmShimExecOptions(process.platform) });
|
|
133
133
|
return `${stdout}${stderr}`;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* npm's global bin shims on Windows are `.cmd` files that `execFile` cannot launch without a
|
|
138
|
+
* shell. Doctor only passes fixed flag arrays here, so shell composition stays safe.
|
|
139
|
+
*/
|
|
140
|
+
export function buildNpmShimExecOptions(platform = process.platform) {
|
|
141
|
+
return platform === "win32" ? { shell: true } : {};
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
async function defaultPathExists(path) {
|
|
137
145
|
try {
|
|
138
146
|
await access(path);
|