pi-agent-browser-native 0.6.8 → 0.6.10
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 +28 -0
- package/README.md +16 -14
- package/dist/extensions/agent-browser/index.js +8 -21
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
- package/dist/extensions/agent-browser/lib/argv-grammar.js +6 -0
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +5 -5
- package/dist/extensions/agent-browser/lib/electron/launch.js +77 -23
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +2 -2
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +3 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +6 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +11 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +4 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +26 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +16 -18
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +5 -11
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +4 -3
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +4 -1
- package/dist/extensions/agent-browser/lib/playbook.js +4 -4
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +17 -29
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -5
- package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
- package/dist/extensions/agent-browser/lib/temp.js +14 -0
- package/dist/scripts/agent-browser-target.mjs +1 -1
- package/docs/ARCHITECTURE.md +5 -4
- package/docs/COMMAND_REFERENCE.md +38 -20
- package/docs/ELECTRON.md +8 -4
- package/docs/RELEASE.md +8 -4
- package/docs/SUPPORT_MATRIX.md +19 -15
- package/docs/TOOL_CONTRACT.md +15 -13
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +10 -3
- package/scripts/agent-browser-target.mjs +1 -1
|
@@ -2,8 +2,7 @@ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
4
|
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
5
|
-
|
|
6
|
-
const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
|
|
5
|
+
import { getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
7
6
|
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
8
7
|
function isSingleScreenshotPathToken(token) {
|
|
9
8
|
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
@@ -17,11 +16,7 @@ function getScreenshotPositionalIndices(commandTokens) {
|
|
|
17
16
|
const positionalIndices = [];
|
|
18
17
|
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
19
18
|
const token = commandTokens[index];
|
|
20
|
-
if (
|
|
21
|
-
index += 1;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (SCREENSHOT_BOOLEAN_FLAGS.has(token))
|
|
19
|
+
if (token === "--full" || token === "-f")
|
|
25
20
|
continue;
|
|
26
21
|
positionalIndices.push(index);
|
|
27
22
|
}
|
|
@@ -54,9 +49,6 @@ function getDiffScreenshotOutputPath(commandTokens) {
|
|
|
54
49
|
}
|
|
55
50
|
return outputPath;
|
|
56
51
|
}
|
|
57
|
-
function foldArtifactPath(path, platform) {
|
|
58
|
-
return foldAgentBrowserFilesystemIdentity(path, platform);
|
|
59
|
-
}
|
|
60
52
|
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
61
53
|
let cursor = absolutePath;
|
|
62
54
|
const suffix = [];
|
|
@@ -71,7 +63,7 @@ function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
|
71
63
|
catch {
|
|
72
64
|
// The destination does not exist yet; canonical ancestry still catches aliases.
|
|
73
65
|
}
|
|
74
|
-
return
|
|
66
|
+
return foldAgentBrowserFilesystemIdentity(canonicalPath, platform);
|
|
75
67
|
}
|
|
76
68
|
catch {
|
|
77
69
|
let symlinkTarget;
|
|
@@ -90,7 +82,7 @@ function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
|
90
82
|
}
|
|
91
83
|
const parent = dirname(cursor);
|
|
92
84
|
if (parent === cursor)
|
|
93
|
-
return
|
|
85
|
+
return foldAgentBrowserFilesystemIdentity(absolutePath, platform);
|
|
94
86
|
suffix.unshift(basename(cursor));
|
|
95
87
|
cursor = parent;
|
|
96
88
|
}
|
|
@@ -120,7 +112,7 @@ export function getExplicitArtifactDestination(commandTokens) {
|
|
|
120
112
|
return commandTokens[3];
|
|
121
113
|
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
122
114
|
return commandTokens[2];
|
|
123
|
-
if (command === "record"
|
|
124
|
-
return commandTokens
|
|
115
|
+
if (command === "record")
|
|
116
|
+
return getRecordCommandOperands(commandTokens).path;
|
|
125
117
|
return undefined;
|
|
126
118
|
}
|
|
@@ -7,11 +7,11 @@ import { formatSessionArtifactRetentionSummary } from "../../results/artifact-ma
|
|
|
7
7
|
import { buildInspectOverlayStateAction, buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
8
8
|
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
|
|
9
9
|
import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
|
|
10
|
-
import {
|
|
10
|
+
import { redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
11
11
|
import { isRecord } from "../../parsing.js";
|
|
12
12
|
import { extractBatchResultCommand, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, runSessionCommandData, } from "./session-state.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
14
|
+
import { getExplicitArtifactDestination } from "./artifact-paths.js";
|
|
15
15
|
const ELECTRON_FILL_VERIFICATION_TIMEOUT_MS = 2_000;
|
|
16
16
|
export function sleepMs(ms) {
|
|
17
17
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -227,7 +227,7 @@ export async function collectRecordingDependencyWarning(options) {
|
|
|
227
227
|
return undefined;
|
|
228
228
|
if (await executableExistsOnPath("ffmpeg"))
|
|
229
229
|
return undefined;
|
|
230
|
-
return { command: recordCommand, dependency: "ffmpeg", message: `${recordCommand}
|
|
230
|
+
return { command: recordCommand, dependency: "ffmpeg", message: `${recordCommand} reported a pending recording, but ffmpeg is not on PATH. Its output is unverified; install ffmpeg before starting a new recording.`, reason: "ffmpeg-missing-for-recording", recommendations: ["Install ffmpeg before recording; on macOS with Homebrew, brew install ffmpeg or brew install ffmpeg-full.", "Stop this recording, check the result, and start a new recording after ensuring Pi can find ffmpeg on PATH."] };
|
|
231
231
|
}
|
|
232
232
|
export function formatRecordingDependencyWarningText(warning) {
|
|
233
233
|
if (!warning)
|
|
@@ -711,12 +711,10 @@ export async function collectElectronHandoff(options) {
|
|
|
711
711
|
}
|
|
712
712
|
return { handoff: "snapshot", refSnapshot, snapshot, ...(snapshotRetryCount > 0 ? { snapshotRetryCount } : {}), tabs };
|
|
713
713
|
}
|
|
714
|
-
function getTimeoutProgressSteps(compiledJob,
|
|
714
|
+
function getTimeoutProgressSteps(compiledJob, commandTokens, stdin) {
|
|
715
715
|
if (compiledJob)
|
|
716
716
|
return compiledJob.steps.map((step, index) => ({ args: step.args, generatedFrom: step.generatedFrom, index: index + 1 }));
|
|
717
|
-
|
|
718
|
-
return [];
|
|
719
|
-
return parseValidBatchStepEntries(stdin).map(({ index, step }) => ({ args: step, index: index + 1 }));
|
|
717
|
+
return getUpstreamEffectiveBatchSteps(commandTokens, stdin).map((args, index) => ({ args, index: index + 1 }));
|
|
720
718
|
}
|
|
721
719
|
function getLastPositionalToken(args, startIndex = 1) {
|
|
722
720
|
for (let index = args.length - 1; index >= startIndex; index -= 1) {
|
|
@@ -726,20 +724,8 @@ function getLastPositionalToken(args, startIndex = 1) {
|
|
|
726
724
|
}
|
|
727
725
|
return undefined;
|
|
728
726
|
}
|
|
729
|
-
function getTimeoutStepArtifactPath(
|
|
730
|
-
|
|
731
|
-
const [command] = commandArgs;
|
|
732
|
-
if (command === "screenshot") {
|
|
733
|
-
const index = getScreenshotPathTokenIndex(commandArgs);
|
|
734
|
-
return index === undefined ? undefined : commandArgs[index];
|
|
735
|
-
}
|
|
736
|
-
if (command === "pdf")
|
|
737
|
-
return getLastPositionalToken(commandArgs);
|
|
738
|
-
if (command === "download")
|
|
739
|
-
return getLastPositionalToken(commandArgs, 2);
|
|
740
|
-
if (command === "wait")
|
|
741
|
-
return parseWaitCommandTokens(commandArgs).downloadPath;
|
|
742
|
-
return undefined;
|
|
727
|
+
function getTimeoutStepArtifactPath(commandTokens) {
|
|
728
|
+
return ["screenshot", "pdf", "download", "wait"].includes(commandTokens[0]) ? getExplicitArtifactDestination(commandTokens) : undefined;
|
|
743
729
|
}
|
|
744
730
|
async function statTimeoutArtifactPath(absolutePath) {
|
|
745
731
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -795,7 +781,7 @@ const TIMEOUT_RETRYABLE_COMMANDS = new Set([
|
|
|
795
781
|
]);
|
|
796
782
|
function getTimeoutStepRetry(step) {
|
|
797
783
|
const command = step.args[0];
|
|
798
|
-
return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: step.args } : undefined;
|
|
784
|
+
return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: ["batch"], stdin: JSON.stringify([step.args]) } : undefined;
|
|
799
785
|
}
|
|
800
786
|
function normalizeUrlForTimeoutComparison(url) {
|
|
801
787
|
if (!url)
|
|
@@ -873,7 +859,7 @@ function buildTimeoutProgressSteps(options) {
|
|
|
873
859
|
};
|
|
874
860
|
}
|
|
875
861
|
export async function collectTimeoutPartialProgress(options) {
|
|
876
|
-
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.
|
|
862
|
+
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.commandTokens, options.stdin);
|
|
877
863
|
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
878
864
|
const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
|
|
879
865
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
@@ -930,7 +916,7 @@ export function formatTimeoutPartialProgressText(progress, pageTargetUnknown = f
|
|
|
930
916
|
lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
931
917
|
}
|
|
932
918
|
if (progress.retryStep?.retry?.args) {
|
|
933
|
-
const payload = JSON.stringify({
|
|
919
|
+
const payload = JSON.stringify({ ...progress.retryStep.retry, stdin: JSON.stringify([redactInvocationArgs(progress.retryStep.args)]) });
|
|
934
920
|
lines.push(pageTargetUnknown
|
|
935
921
|
? `Retry candidate for step ${progress.retryStep.index}: ${payload}. Verify the current URL before running it.`
|
|
936
922
|
: `Retry failed step: ${payload}`);
|
|
@@ -205,7 +205,7 @@ export async function prepareFinalResultRecoveryState(options) {
|
|
|
205
205
|
return { categoryDetails, currentRefSnapshot, currentRefSnapshotInvalidation, noActivePageSnapshotFailure, richInputRecoveryDiagnostic, visibleRefFallbackDiagnostic, visibleRefFallbackSessionName };
|
|
206
206
|
}
|
|
207
207
|
function buildTimeoutPartialProgressNextActions(options) {
|
|
208
|
-
const
|
|
208
|
+
const retry = options.timeoutPartialProgress?.retryStep?.retry;
|
|
209
209
|
const stepIndex = options.timeoutPartialProgress?.retryStep?.index;
|
|
210
210
|
const freshSessionAbandoned = options.sessionMode === "fresh" && options.timeoutPartialProgress?.liveUrlRecovered !== true;
|
|
211
211
|
if (options.currentSessionTabTargetUnknown && !freshSessionAbandoned && options.executionPlan.sessionName) {
|
|
@@ -220,12 +220,12 @@ function buildTimeoutPartialProgressNextActions(options) {
|
|
|
220
220
|
tool: "agent_browser",
|
|
221
221
|
}];
|
|
222
222
|
}
|
|
223
|
-
if (
|
|
223
|
+
if (retry) {
|
|
224
224
|
return [{
|
|
225
225
|
id: "retry-timeout-step",
|
|
226
226
|
params: freshSessionAbandoned
|
|
227
|
-
? {
|
|
228
|
-
: { args: withOptionalSessionArgs(options.executionPlan.sessionName,
|
|
227
|
+
? { ...retry, sessionMode: "fresh" }
|
|
228
|
+
: { ...retry, args: withOptionalSessionArgs(options.executionPlan.sessionName, retry.args) },
|
|
229
229
|
reason: freshSessionAbandoned
|
|
230
230
|
? `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} in a fresh browser session because the timed-out fresh session was not proven live.`
|
|
231
231
|
: `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} against the current browser session.`,
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
3
|
+
import { inspectElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
2
4
|
import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lock.js";
|
|
3
|
-
import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, } from "../../managed-session-restore.js";
|
|
5
|
+
import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
4
6
|
import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
5
7
|
import { isRecord } from "../../parsing.js";
|
|
6
8
|
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
7
|
-
import { runAgentBrowserProcess } from "../../process.js";
|
|
9
|
+
import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
|
|
8
10
|
import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
9
11
|
import { redactInvocationArgs } from "../../runtime.js";
|
|
12
|
+
import { runSessionCommandData } from "./session-state.js";
|
|
10
13
|
const MANAGED_SESSION_DAEMON_INSPECTION_TIMEOUT_MS = 35_000;
|
|
11
14
|
const RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR = "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS cannot change a running wrapper-owned headed session's launch-time periodic autosave interval. Close that session first, then retry with sessionMode: \"fresh\" so the new daemon starts with the requested interval.";
|
|
12
15
|
export function getRunningHeadedAutosavePolicyChangeError(recordedInterval, closeCommand = false) {
|
|
@@ -49,6 +52,23 @@ export async function inspectManagedSessionDaemon(options) {
|
|
|
49
52
|
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
async function verifyRestoredElectronAttachment(context, record, signal, timeoutMs) {
|
|
56
|
+
const cwd = context.cwd;
|
|
57
|
+
if (!cwd || !record?.webSocketDebuggerUrl || !record.sessionName || record.cleanupState === "cleaned"
|
|
58
|
+
|| getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) !== getAgentBrowserSessionIdentityKey(context.sessionName, context.namespace))
|
|
59
|
+
return false;
|
|
60
|
+
const status = await inspectElectronLaunchStatus(record, signal);
|
|
61
|
+
if (signal?.aborted || status.pidAlive !== true || status.userDataDirState !== "present"
|
|
62
|
+
|| status.version?.webSocketDebuggerUrl !== record.webSocketDebuggerUrl)
|
|
63
|
+
return false;
|
|
64
|
+
// This metadata probe must not grant daemon provenance just by spawning.
|
|
65
|
+
const connection = await withAttachedBrowserSessionContext(true, () => withOwnedManagedSessionContext(undefined, () => runSessionCommandData({
|
|
66
|
+
args: ["get", "cdp-url"], cwd, env: getHeadedManagedAutosaveEnv(context.headedManagedAutosaveInterval), namespace: context.namespace, pinNamespace: true,
|
|
67
|
+
sessionName: context.sessionName, signal, timeoutMs,
|
|
68
|
+
})));
|
|
69
|
+
return !signal?.aborted && isRecord(connection) && typeof connection.cdpUrl === "string"
|
|
70
|
+
&& (connection.cdpUrl === record.webSocketDebuggerUrl || status.targets.some((target) => target.webSocketDebuggerUrl === connection.cdpUrl));
|
|
71
|
+
}
|
|
52
72
|
export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
53
73
|
const { context, signal } = options;
|
|
54
74
|
if (!context.cwd)
|
|
@@ -81,7 +101,7 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
81
101
|
return { daemonStatus: daemon.status, lock };
|
|
82
102
|
}
|
|
83
103
|
const stickyDisabled = context.restoreState.isDisabled(context.sessionName, context.namespace);
|
|
84
|
-
|
|
104
|
+
let hasKnownDaemonRestoreKey = context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace);
|
|
85
105
|
const knownDaemonRestoreKey = context.restoreState.getDaemonRestoreKey(context.sessionName, context.namespace);
|
|
86
106
|
const requestedDaemonRestoreKey = context.restoreDecision === "enabled" && stickyDisabled
|
|
87
107
|
? knownDaemonRestoreKey ?? null
|
|
@@ -93,6 +113,9 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
115
|
const restoreDisabledPolicyNeedsProvenance = stickyDisabled || context.restoreDecision !== "enabled";
|
|
116
|
+
if (daemon.status === "active" && restoreDisabledPolicyNeedsProvenance && !hasKnownDaemonRestoreKey && daemon.restoreKey === requestedDaemonRestoreKey) {
|
|
117
|
+
hasKnownDaemonRestoreKey = await verifyRestoredElectronAttachment(context, options.electronLaunchRecord, signal, options.electronVerificationTimeoutMs);
|
|
118
|
+
}
|
|
96
119
|
const activePolicyMatches = daemon.status === "active"
|
|
97
120
|
&& (!restoreDisabledPolicyNeedsProvenance || hasKnownDaemonRestoreKey)
|
|
98
121
|
&& daemon.restoreKey === requestedDaemonRestoreKey;
|
|
@@ -22,7 +22,7 @@ import { buildOwnedManagedSessionRestoreContext, resolveExplicitAutosaveInterval
|
|
|
22
22
|
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
23
23
|
import { getExplicitSessionPageVerificationRequirement, getPageTargetValidationError, } from "../../page-target-validation.js";
|
|
24
24
|
import { acquireOwnedManagedSessionDaemonPolicy, getRunningHeadedAutosavePolicyChangeError } from "./managed-session-daemon-policy.js";
|
|
25
|
-
import { buildManagedSessionOutcome, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, extractStringResultField, ensureSessionTabTarget, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
|
|
25
|
+
import { buildManagedSessionOutcome, buildSessionDetailFields, buildStaleRefPreflight, getSessionContextKey, findElectronLaunchRecordForSession, extractStringResultField, ensureSessionTabTarget, getGuardedRefUsage, getTraceOwnerGuardMessage, runSessionCommandData, shouldPinSessionTabForCommand, } from "./session-state.js";
|
|
26
26
|
import { getUpstreamEffectiveBatchSteps, parseBatchStdinJsonArray } from "../batch-stdin.js";
|
|
27
27
|
import { buildElectronHostFailureResult, formatAgentBrowserNextActionsText, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
|
|
28
28
|
import { prepareClickDispatchProbe } from "./click-dispatch.js";
|
|
@@ -73,21 +73,19 @@ async function ensureArtifactParentDirectory(commandTokens, cwd) {
|
|
|
73
73
|
return;
|
|
74
74
|
await mkdir(dirname(resolve(cwd, requestedPath)), { recursive: true });
|
|
75
75
|
}
|
|
76
|
-
async function normalizeScreenshotPathInTokens(commandTokens, cwd) {
|
|
77
|
-
|
|
78
|
-
const projection = projectUpstreamGlobalFlags(
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
76
|
+
async function normalizeScreenshotPathInTokens(commandTokens, cwd, batchStep = false) {
|
|
77
|
+
// Native batch rows skip outer CLI global-flag cleanup.
|
|
78
|
+
const projection = batchStep ? undefined : projectUpstreamGlobalFlags(commandTokens);
|
|
79
|
+
const pathIndex = getScreenshotPathTokenIndex(projection?.tokens ?? commandTokens);
|
|
80
|
+
const screenshotPathTokenIndex = pathIndex === undefined ? undefined : projection ? projection.indices[pathIndex] : pathIndex;
|
|
81
|
+
if (screenshotPathTokenIndex === undefined)
|
|
82
82
|
return { tokens: commandTokens };
|
|
83
|
-
}
|
|
84
|
-
const screenshotPathTokenIndex = commandTokens.length - scopedCommandTokens.length + scopedPathTokenIndex;
|
|
85
83
|
const requestedPath = commandTokens[screenshotPathTokenIndex];
|
|
86
84
|
const absolutePath = resolve(cwd, requestedPath);
|
|
87
85
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
88
86
|
const tokens = [...commandTokens];
|
|
89
87
|
tokens[screenshotPathTokenIndex] = absolutePath;
|
|
90
|
-
const terminatorIndex = tokens.indexOf("--");
|
|
88
|
+
const terminatorIndex = batchStep ? -1 : tokens.indexOf("--");
|
|
91
89
|
if (terminatorIndex >= 0) {
|
|
92
90
|
tokens.splice(terminatorIndex, 1);
|
|
93
91
|
}
|
|
@@ -110,13 +108,12 @@ async function prepareBatchScreenshotPaths(args, stdin, cwd) {
|
|
|
110
108
|
// prepare parent directories for the rows that will run and skip stdin
|
|
111
109
|
// preparation (no directories for never-executed rows).
|
|
112
110
|
for (const step of argumentSteps) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (stepTokens[0] === "screenshot") {
|
|
111
|
+
await ensureArtifactParentDirectory(step, cwd);
|
|
112
|
+
if (step[0] === "screenshot") {
|
|
116
113
|
// Reuse the screenshot path resolution for its parent-directory side
|
|
117
114
|
// effect only: raw strings are never rewritten, so the normalized
|
|
118
115
|
// tokens and path request are deliberately discarded.
|
|
119
|
-
await normalizeScreenshotPathInTokens(step, cwd);
|
|
116
|
+
await normalizeScreenshotPathInTokens(step, cwd, true);
|
|
120
117
|
}
|
|
121
118
|
}
|
|
122
119
|
return undefined;
|
|
@@ -134,12 +131,11 @@ async function prepareBatchScreenshotPaths(args, stdin, cwd) {
|
|
|
134
131
|
if (!Array.isArray(step) || !step.every((item) => typeof item === "string")) {
|
|
135
132
|
return step;
|
|
136
133
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (upstreamStep[0] !== "screenshot") {
|
|
134
|
+
await ensureArtifactParentDirectory(step, cwd);
|
|
135
|
+
if (step[0] !== "screenshot") {
|
|
140
136
|
return step;
|
|
141
137
|
}
|
|
142
|
-
const normalized = await normalizeScreenshotPathInTokens(step, cwd);
|
|
138
|
+
const normalized = await normalizeScreenshotPathInTokens(step, cwd, true);
|
|
143
139
|
batchScreenshotPathRequests[index] = normalized.request;
|
|
144
140
|
if (normalized.request) {
|
|
145
141
|
changed = true;
|
|
@@ -508,6 +504,8 @@ export async function prepareBrowserRun(options) {
|
|
|
508
504
|
const closeCommand = isCloseCommand(executionPlan.commandInfo.command);
|
|
509
505
|
const policy = await acquireOwnedManagedSessionDaemonPolicy({
|
|
510
506
|
context: ownedManagedSession,
|
|
507
|
+
electronLaunchRecord: findElectronLaunchRecordForSession(executionPlan.sessionName, state.electronLaunchRecords, executionPlan.namespace),
|
|
508
|
+
electronVerificationTimeoutMs: params.timeoutMs,
|
|
511
509
|
mode: closeCommand ? "close" : "reuse",
|
|
512
510
|
signal,
|
|
513
511
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
3
3
|
import { needsManagedSession } from "../../command-policy.js";
|
|
4
|
-
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
4
|
+
import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
5
5
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
|
|
6
6
|
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
7
7
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
@@ -108,12 +108,6 @@ function batchStartedManagedBrowser(data) {
|
|
|
108
108
|
function withoutNamespaceEntries(entries, namespace) {
|
|
109
109
|
return new Map([...entries].filter(([key]) => !isAgentBrowserSessionIdentityKeyInNamespace(key, namespace)));
|
|
110
110
|
}
|
|
111
|
-
function deleteNamespaceEntries(entries, namespace) {
|
|
112
|
-
for (const key of entries.keys()) {
|
|
113
|
-
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
114
|
-
entries.delete(key);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
111
|
function setNetworkRouteState(options) {
|
|
118
112
|
if (!options.sessionName)
|
|
119
113
|
return options.routesBySession;
|
|
@@ -223,8 +217,8 @@ export async function processBrowserOutput(input) {
|
|
|
223
217
|
}
|
|
224
218
|
if (closeAllApplied) {
|
|
225
219
|
networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
deleteIdentityKeysInNamespace(state.attachedSessionKeys, prepared.executionPlan.namespace);
|
|
221
|
+
deleteIdentityKeysInNamespace(traceOwners, prepared.executionPlan.namespace);
|
|
228
222
|
sessionPageState.clearNamespace(prepared.executionPlan.namespace);
|
|
229
223
|
const retainedSessionKey = nestedBatchRemainsActive ? sessionStateKey : undefined;
|
|
230
224
|
for (const [key, owner] of state.ownedManagedSessions) {
|
|
@@ -370,7 +364,7 @@ export async function processBrowserOutput(input) {
|
|
|
370
364
|
let fillVerificationDiagnostic;
|
|
371
365
|
let selectorTextVisibilityDiagnostics = [];
|
|
372
366
|
let electronBroadGetTextScopeDiagnostics = [];
|
|
373
|
-
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
367
|
+
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
374
368
|
if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
|
|
375
369
|
currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
|
|
376
370
|
}
|
|
@@ -763,7 +757,7 @@ export async function processBrowserOutput(input) {
|
|
|
763
757
|
? presentation.batchSteps?.some((step) => isRecordPageTransitionCommand(extractUpstreamCommandTokens(step.command ?? [])))
|
|
764
758
|
: isRecordPageTransitionCommand(prepared.commandTokens);
|
|
765
759
|
const recordingPageWarning = processResult.agentBrowserStarted && !prepared.executionPlan.plainTextInspection && recordingTransitionReached
|
|
766
|
-
? "Page state: this
|
|
760
|
+
? "Page state: this wrapper conservatively invalidates earlier refs after recording starts and URL-bearing restarts. Take a fresh snapshot before continuing; this does not prove the page changed."
|
|
767
761
|
: undefined;
|
|
768
762
|
const sessionWarning = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
769
763
|
const warningText = [sessionWarning, recordingPageWarning].filter(Boolean).join("\n\n") || undefined;
|
|
@@ -7,7 +7,7 @@ import { buildAgentBrowserNextActions } from "../../results/action-recommendatio
|
|
|
7
7
|
import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
8
8
|
import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
9
9
|
import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, targetsMatch, } from "../../session-page-state.js";
|
|
10
|
-
import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand,
|
|
10
|
+
import { getRecordCommandOperands, isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, isWindowOrDiffPageTransitionCommand, } from "../../command-taxonomy.js";
|
|
11
11
|
import { chooseOpenResultTabCorrection } from "../../runtime.js";
|
|
12
12
|
import { isRecord, parseRefId } from "../../parsing.js";
|
|
13
13
|
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
@@ -424,7 +424,7 @@ export function commandChoosesSessionTabTarget(args) {
|
|
|
424
424
|
|| isWindowOrDiffPageTransitionCommand(command, subcommand)
|
|
425
425
|
|| (command === "a11y" && findFirstPositionalArgument(tokens) !== undefined)
|
|
426
426
|
|| (["vitals", "web-vitals"].includes(command) && tokens.slice(1).some((token) => !token.startsWith("--")))
|
|
427
|
-
|| (
|
|
427
|
+
|| getRecordCommandOperands(tokens).url !== undefined;
|
|
428
428
|
}
|
|
429
429
|
export function shouldPinSessionTabForCommand(options) {
|
|
430
430
|
if (!options.pinningRequired || !options.sessionName || !options.command)
|
|
@@ -483,12 +483,13 @@ function selectAnySessionTargetTab(options) {
|
|
|
483
483
|
return selection ? { ...selection, ...(targetTitle ? { targetTitle } : {}), targetUrl } : undefined;
|
|
484
484
|
}
|
|
485
485
|
export async function runSessionCommandData(options) {
|
|
486
|
-
const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
|
|
486
|
+
const { args, cwd, env, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
|
|
487
487
|
if (!sessionName)
|
|
488
488
|
return undefined;
|
|
489
489
|
const processResult = await runAgentBrowserProcess({
|
|
490
490
|
args: ["--json", ...(namespace !== undefined || pinNamespace ? ["--namespace", namespace ?? ""] : []), "--session", sessionName, ...args],
|
|
491
491
|
cwd,
|
|
492
|
+
env,
|
|
492
493
|
signal,
|
|
493
494
|
stdin,
|
|
494
495
|
timeoutMs,
|
|
@@ -414,7 +414,7 @@ async function withOwnedElectronManagedSessionPolicy(options, run) {
|
|
|
414
414
|
throw new ElectronManagedSessionPolicyError("Electron helper could not establish wrapper ownership for its managed session.");
|
|
415
415
|
let policy;
|
|
416
416
|
try {
|
|
417
|
-
policy = await acquireOwnedManagedSessionDaemonPolicy({ context, signal: options.signal });
|
|
417
|
+
policy = await acquireOwnedManagedSessionDaemonPolicy({ context, electronLaunchRecord: options.electronLaunchRecord, electronVerificationTimeoutMs: options.timeoutMs, signal: options.signal });
|
|
418
418
|
}
|
|
419
419
|
catch (error) {
|
|
420
420
|
throw new ElectronManagedSessionPolicyError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
@@ -675,6 +675,7 @@ async function handleElectronHostInputInContext(options) {
|
|
|
675
675
|
const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
|
|
676
676
|
return collectOwnedElectronManagedSessionTarget({
|
|
677
677
|
cwd,
|
|
678
|
+
electronLaunchRecord: record,
|
|
678
679
|
headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
|
|
679
680
|
headedManagedAutosaveInterval: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveInterval,
|
|
680
681
|
namespace: record.namespace,
|
|
@@ -750,12 +751,14 @@ async function handleElectronHostInputInContext(options) {
|
|
|
750
751
|
const probe = await withOwnedElectronManagedSessionPolicy({
|
|
751
752
|
args: ["snapshot", "-i"],
|
|
752
753
|
cwd,
|
|
754
|
+
electronLaunchRecord: launchRecord,
|
|
753
755
|
headedManagedAutosaveDisabled,
|
|
754
756
|
headedManagedAutosaveInterval,
|
|
755
757
|
namespace: probeNamespace,
|
|
756
758
|
restoreState: managedSessionRestoreState,
|
|
757
759
|
sessionName: probeSessionName,
|
|
758
760
|
signal,
|
|
761
|
+
timeoutMs: compiledElectron.timeoutMs,
|
|
759
762
|
}, async () => await collectElectronProbe({ cwd, namespace: probeNamespace, sessionName: probeSessionName, signal, timeoutMs: compiledElectron.timeoutMs }));
|
|
760
763
|
const managedSession = {
|
|
761
764
|
sessionName: probe.sessionName,
|
|
@@ -15,7 +15,7 @@ export const QUICK_START_GUIDELINES = [
|
|
|
15
15
|
`Common advanced calls: { args: ["batch", "--bail"], stdin: "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }, { job: { steps: [{ action: "open", url: "https://example.com" }, { action: "assertText", text: "Example Domain" }, { action: "screenshot", path: ".dogfood/example.png" }] } }, { qa: { url: "https://example.com", expectedText: "Example Domain", screenshotPath: ".dogfood/qa-example.png" } } (example.com smoke only; elsewhere match exact visible text from snapshot -i), { electron: { action: "list", query: "code" } }, { electron: { action: "launch", appName: "Visual Studio Code", handoff: "snapshot" } }, { electron: { action: "probe" } }, { qa: { attached: true, expectedText: "Explorer" } }, { args: ["eval", "--stdin"], stdin: "document.title", outputPath: "logs/page-title.json" }, { args: ["auth", "save", "name", "--password-stdin"], stdin: "<password from user-approved secret source>" }, { args: ["--profile", "Default", "open", "https://example.com/account"], sessionMode: "fresh" }, and { args: ["open", "--enable", "react-devtools", "https://example.com"], sessionMode: "fresh" }. For app pages with a native dropdown, job steps can include { action: "select", selector: "#flavor", value: "chocolate" } before the dependent assertion; for locator-friendly pages, job click/fill steps can use semantic locator fields such as { action: "fill", locator: "role", role: "searchbox", name: "Search", text: "agent browser" }; for human-paced input, job type steps can use { action: "type", selector: "#prompt", text: "hello", delayMs: 20, press: "Enter" }; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in visible batch prose while full rows remain in details.batchSteps.`,
|
|
16
16
|
"Constrained job navigation is explicit only: click (and select/submit flows that may navigate) does not prove the next page loaded; add an assertUrl that does not already match the starting page and/or assertText for new page state after navigation-prone steps before screenshot or later interactions. assertText takes only text, not selector or locator fields. Clicks can stale subsequent @refs: split the job and re-snapshot before using those refs. Keep jobs short around navigation, click, and rerender boundaries on dynamic React/product apps; avoid a whole checkout in one job. If a long job times out and details.timeoutPartialProgress shows a mutating incomplete step, inspect current page state and continue with a shorter job or single action instead of blindly retrying the mutating step. Example: { job: { steps: [{ action: \"open\", url: \"https://shop.example/checkout\" }, { action: \"fill\", selector: \"#email\", text: \"user@example.com\" }, { action: \"click\", selector: \"#continue\" }, { action: \"assertUrl\", url: \"**/shipping\" }, { action: \"assertText\", text: \"Shipping address\" }, { action: \"screenshot\", path: \".dogfood/shipping.png\" }] } }. Top-level click may add pageChangeSummary hints, but job never auto-inserts post-click asserts.",
|
|
17
17
|
"High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; wrapper-handled scroll <dir> [px|percent] and scroll to end/top target document scrolling before upstream fallback, while scroll <selector> <dir> [px|percent] targets nested scrollers; download <selector> <path> saves a file triggered by a click; read [url] returns agent-readable text (explicit URLs prefer markdown without requiring a Chrome page; omit the URL for rendered active-tab DOM); get title/url need no selector; get text/html/value/count <selector> and get attr <selector> <name> read elements/page state (use body for whole-page text/html); screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree, react inspect <fiberId>, react renders start/stop, and react suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
|
|
18
|
-
"For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target.
|
|
18
|
+
"For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target. Current upstream records the active page unless a URL is supplied. To support older natives, the wrapper conservatively blocks prior @e… refs after every dispatched start attempt and URL-bearing restart, even on failure; this does not prove the page changed. Take a fresh snapshot before continuing. A restart with only --fps keeps the page and refs; --fps <n> selects 1–60 fps (default 30). The wrapper creates parent directories for direct artifact paths and can save simple loopback HTTP(S) anchor downloads directly to the requested path before upstream download fallback. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. A bare inbound image/video path is not a requested output artifact and does not block close. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
|
|
19
19
|
"When failure output shows Next actions, prefer those exact native agent_browser follow-up payloads over guessed commands. The same actions are available in details.nextActions to callers that expose structured details; short stdin is shown inline, while long stdin stays details-only.",
|
|
20
20
|
];
|
|
21
21
|
export const WEB_SEARCH_PROMPT_GUIDELINE = "Prefer agent_browser_web_search for current or external web facts and URL discovery over public search-engine forms that can hit anti-bot/CAPTCHA-gated pages. For research before implementation, pass searchType: deep-lite unless webSearch.defaultSearchType already does; omit it for everyday lookups so config/auto wins. Provider rank is not proof of authority: when correctness or version matters, prefer the vendor or project's primary current docs, inspect page-date and version clues, and constrain one follow-up after discovering the official domain (Exa includeDomains; Brave site: in query). Do not count URL aliases as independent sources. Use agent_browser after you have a target URL that needs interaction, screenshots, or DOM inspection.";
|
|
@@ -35,7 +35,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
35
35
|
"After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume and live-checks the URL before later page reads/interactions because an attached browser can drift externally; caller config, file access, launch arguments, and environment pass through unchanged. A successful close clears the marker. When several named sessions share one Chrome, pass --pin-tab once (AGENT_BROWSER_PIN_TAB) so a closed bound tab fails as tab_gone instead of acting on a neighbor; recover with tab new or tab list. --no-pin-tab turns the sticky pin off. tab list includes each tab's CDP targetId, accepted as a tab ref.",
|
|
36
36
|
`If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
|
|
37
37
|
"For WebGPU pages, use args [\"--webgpu\", \"open\", \"<url>\"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.",
|
|
38
|
-
"For experimental WebMCP page tools, use webmcp list, then webmcp invoke <tool> with --params and optional --frame/--detach/--timeout; use webmcp result or cancel for detached calls. Locally managed Chrome enables WebMCP by default. --no-webmcp is launch-scoped and requires a fresh session; invoke/result/cancel can mutate or navigate, so refresh snapshot refs afterward.",
|
|
38
|
+
"For experimental WebMCP page tools, use webmcp list, then webmcp invoke <tool> with --params and optional --frame/--detach/--timeout; use webmcp result or cancel for detached calls. Locally managed Chrome enables WebMCP by default; a positive navigation hint means the page has tools to list. --no-webmcp is launch-scoped and requires a fresh session; invoke/result/cancel can mutate or navigate, so refresh snapshot refs afterward.",
|
|
39
39
|
"For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.",
|
|
40
40
|
"For React introspection, launch the page with --enable react-devtools before first navigation, then use react tree, react inspect <fiberId>, sourceLookup candidates for local UI source hints, react renders start/stop, or react suspense; sourceLookup is experimental and reports confidence/evidence instead of guaranteed DOM-to-file mappings. For failed fetches and APIs, networkSourceLookup (experimental) correlates failed network requests with initiator metadata and bounded workspace URL literals—candidates only, not definitive blame. Use vitals [url] for Core Web Vitals and hydration timing, and pushstate <url> for client-side SPA navigation.",
|
|
41
41
|
"For first-navigation setup, use open without a URL plus network route --resource-type <csv>, cookies set --curl <file>, or --init-script/--enable before navigate/opening the target page.",
|
|
@@ -57,7 +57,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
57
57
|
"When commands save or spill files (screenshots, downloads, PDFs, traces, recordings, HAR, large snapshot spills), use the user's exact requested paths when given and treat paths as provisional until details.artifactVerification shows every row verified: branch on missingCount, pendingCount, unverifiedCount, per-entry state, and optional limitation before downstream file use or PASS/FAIL reporting.",
|
|
58
58
|
"For evidence-only screenshots, QA captures, or other audit artifacts, save to an explicit path and branch on details.artifactVerification plus details.artifacts before reporting PASS/FAIL; do not require vision review of inline image attachments unless the user asked for visual inspection.",
|
|
59
59
|
"Respect explicit user stop boundaries yourself. When the surrounding authenticated employee or automation context is explicitly unattended/auto-approved, ordinary non-destructive form submissions within the requested flow may proceed without separate confirmation. Still require explicit authorization for purchases, production-control actions, destructive or irreversible actions, and account, security, or privacy changes. The wrapper does not infer broad business intent from prompt text; details.promptGuard is reserved for concrete artifact-before-close checks.",
|
|
60
|
-
"
|
|
60
|
+
"Recording needs ffmpeg on PATH before start. Current upstream checks it at startup; older natives may defer failure. A pending recording is not verified output.",
|
|
61
61
|
"Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
|
|
62
62
|
];
|
|
63
63
|
export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
@@ -80,7 +80,7 @@ export const RUNTIME_PROMPT_GUIDELINES = [
|
|
|
80
80
|
"Use agent_browser with one input mode: script, args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin: batch/eval/auth/wrapper batch only; electron rejects it; never pass --json.",
|
|
81
81
|
"For agent_browser, use open → snapshot -i → @refs; re-snapshot after changes. In authenticated unattended/auto-approved employee flows, ordinary requested non-destructive submissions may proceed. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
|
|
82
82
|
"Use agent_browser sessionMode=fresh for launch flags. Use requested/configured profiles only; run profiles/doctor on failure. --allowed-domains cannot restore; macOS profile copies may omit encrypted cookies. Verify auth; use a user-approved headed login if needed. Profile content is model-visible.",
|
|
83
|
-
"agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close;
|
|
83
|
+
"agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; ffmpeg before recording; close keeps files; waited:timeout proves nothing.",
|
|
84
84
|
"When agent_browser details.nextActions exists, use them. Check Omitted high-value controls in dense snapshots. Dashboards: verify scroll via screenshot/snapshot.",
|
|
85
85
|
"agent_browser: read <url> for docs/text or active DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>. Batch 3+ getters; heed visibility warnings.",
|
|
86
86
|
];
|
|
@@ -3,7 +3,6 @@ import { extname, resolve } from "node:path";
|
|
|
3
3
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
4
4
|
import { getExplicitArtifactDestination } from "../../orchestration/browser-run/artifact-paths.js";
|
|
5
5
|
import { isRecord, parsePositiveInteger } from "../../parsing.js";
|
|
6
|
-
import { extractUpstreamCommandTokens } from "../../runtime.js";
|
|
7
6
|
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
|
|
8
7
|
import { classifyAgentBrowserSuccessCategory } from "../categories.js";
|
|
9
8
|
const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
|
|
@@ -192,7 +191,7 @@ async function buildFileArtifactMetadata(options) {
|
|
|
192
191
|
namespace: options.namespace,
|
|
193
192
|
path: displayPath,
|
|
194
193
|
recordingState: pendingRecording ? "openRecording" : undefined,
|
|
195
|
-
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(
|
|
194
|
+
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(options.commandInfo.commandTokens ?? []),
|
|
196
195
|
session: options.sessionName,
|
|
197
196
|
sizeBytes,
|
|
198
197
|
status: pendingRecording ? "pending" : exists === false ? "missing" : stale ? "stale" : options.artifactRequest?.status ?? "saved",
|
|
@@ -213,44 +212,33 @@ async function buildPreviousRestartRecordingArtifact(options) {
|
|
|
213
212
|
if (!previousRecording)
|
|
214
213
|
return undefined;
|
|
215
214
|
const absolutePath = previousRecording.absolutePath ?? resolve(options.cwd, previousRecording.path);
|
|
215
|
+
const base = {
|
|
216
|
+
absolutePath,
|
|
217
|
+
artifactType: "video",
|
|
218
|
+
command: "record",
|
|
219
|
+
cwd: previousRecording.cwd ?? options.cwd,
|
|
220
|
+
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
221
|
+
kind: "video",
|
|
222
|
+
namespace: previousRecording.namespace ?? options.namespace,
|
|
223
|
+
path: previousRecording.path,
|
|
224
|
+
requestedPath: previousRecording.requestedPath,
|
|
225
|
+
session: previousRecording.session ?? options.sessionName,
|
|
226
|
+
subcommand: "restart-previous",
|
|
227
|
+
};
|
|
216
228
|
try {
|
|
217
229
|
const fileStats = await stat(absolutePath);
|
|
218
230
|
const stale = artifactMtimeIsOutsideCommandWindow(fileStats.mtimeMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
219
231
|
return {
|
|
220
|
-
|
|
221
|
-
artifactType: "video",
|
|
222
|
-
command: "record",
|
|
223
|
-
cwd: previousRecording.cwd ?? options.cwd,
|
|
232
|
+
...base,
|
|
224
233
|
exists: true,
|
|
225
|
-
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
226
|
-
kind: "video",
|
|
227
234
|
mediaType: fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined,
|
|
228
|
-
namespace: previousRecording.namespace ?? options.namespace,
|
|
229
|
-
path: previousRecording.path,
|
|
230
|
-
requestedPath: previousRecording.requestedPath,
|
|
231
|
-
session: previousRecording.session ?? options.sessionName,
|
|
232
235
|
sizeBytes: fileStats.size,
|
|
233
236
|
status: stale ? "stale" : "saved",
|
|
234
|
-
subcommand: "restart-previous",
|
|
235
237
|
updatedAtMs: fileStats.mtimeMs,
|
|
236
238
|
};
|
|
237
239
|
}
|
|
238
240
|
catch {
|
|
239
|
-
return {
|
|
240
|
-
absolutePath,
|
|
241
|
-
artifactType: "video",
|
|
242
|
-
command: "record",
|
|
243
|
-
cwd: previousRecording.cwd ?? options.cwd,
|
|
244
|
-
exists: false,
|
|
245
|
-
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
246
|
-
kind: "video",
|
|
247
|
-
namespace: previousRecording.namespace ?? options.namespace,
|
|
248
|
-
path: previousRecording.path,
|
|
249
|
-
requestedPath: previousRecording.requestedPath,
|
|
250
|
-
session: previousRecording.session ?? options.sessionName,
|
|
251
|
-
status: "missing",
|
|
252
|
-
subcommand: "restart-previous",
|
|
253
|
-
};
|
|
241
|
+
return { ...base, exists: false, status: "missing" };
|
|
254
242
|
}
|
|
255
243
|
}
|
|
256
244
|
export async function extractFileArtifacts(options) {
|
|
@@ -436,7 +424,7 @@ function formatArtifactLabel(artifact) {
|
|
|
436
424
|
}
|
|
437
425
|
if (!isPendingRecordingArtifact(artifact))
|
|
438
426
|
return "Saved recording";
|
|
439
|
-
return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started
|
|
427
|
+
return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started; output will be written on stop";
|
|
440
428
|
}
|
|
441
429
|
}
|
|
442
430
|
export function formatArtifactSummary(artifacts) {
|
|
@@ -64,11 +64,11 @@ export function getPageSummary(data) {
|
|
|
64
64
|
const url = typeof data.url === "string" ? data.url : undefined;
|
|
65
65
|
if (title === undefined && url === undefined)
|
|
66
66
|
return undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const summary = title && url ? `${title}\n${url}` : url || title || UNTITLED_PAGE_SUMMARY;
|
|
68
|
+
const webmcp = isRecord(data.webmcp) ? data.webmcp : undefined;
|
|
69
|
+
return webmcp?.available === true && typeof webmcp.toolCount === "number" && Number.isInteger(webmcp.toolCount) && webmcp.toolCount > 0
|
|
70
|
+
? `${summary}\n\nWebMCP tools are available on this page (experimental). Run webmcp list to view them.`
|
|
71
|
+
: summary;
|
|
72
72
|
}
|
|
73
73
|
export function formatCount(count, singular, plural = `${singular}s`) {
|
|
74
74
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
@@ -212,7 +212,7 @@ export function buildNoActivePageRefSnapshotInvalidation() {
|
|
|
212
212
|
export function buildPageTransitionRefSnapshotInvalidation(summary) {
|
|
213
213
|
return {
|
|
214
214
|
reason: "page-transition",
|
|
215
|
-
summary: summary ?? "
|
|
215
|
+
summary: summary ?? "Recording starts and URL-bearing restarts conservatively invalidate earlier page-scoped refs. Run snapshot -i before using refs; this is not evidence of a page change.",
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
export function getCommandRefSnapshotInvalidation(commandTokens) {
|