pi-agent-browser-native 0.6.9 → 0.6.11
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 +48 -0
- package/README.md +62 -19
- package/dist/extensions/agent-browser/index.js +424 -451
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
- package/dist/extensions/agent-browser/lib/argv-grammar.js +7 -1
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +42 -12
- 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 +14 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +12 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +38 -31
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +60 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -5
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
- package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
- package/dist/extensions/agent-browser/lib/playbook.js +13 -12
- package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
- package/dist/extensions/agent-browser/lib/process.js +8 -5
- package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
- package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
- package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +76 -57
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -25
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
- package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
- package/dist/extensions/agent-browser/lib/runtime.js +72 -20
- package/dist/extensions/agent-browser/lib/session-page-state.js +24 -8
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/dist/scripts/agent-browser-target.mjs +1 -1
- package/docs/ARCHITECTURE.md +29 -12
- package/docs/COMMAND_REFERENCE.md +67 -31
- package/docs/RELEASE.md +6 -4
- package/docs/SUPPORT_MATRIX.md +24 -16
- package/docs/TOOL_CONTRACT.md +82 -28
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +10 -3
- package/scripts/agent-browser-target.mjs +1 -1
- package/scripts/prepare.mjs +2 -4
|
@@ -65,7 +65,7 @@ function normalizeExplicitEvalStdinArgs(args, stdin) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
export function resolveAgentBrowserInput(options) {
|
|
68
|
-
const { getBatchPreflightValidationError,
|
|
68
|
+
const { getBatchPreflightValidationError, params } = options;
|
|
69
69
|
const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
|
|
70
70
|
const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
|
|
71
71
|
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
@@ -126,9 +126,7 @@ export function resolveAgentBrowserInput(options) {
|
|
|
126
126
|
const attachedQaSessionError = compiledQaPreset?.checks.attached
|
|
127
127
|
? params.sessionMode === "fresh"
|
|
128
128
|
? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
|
|
129
|
-
:
|
|
130
|
-
? "qa.attached requires an active attached session. Run electron.launch or connect to an Electron debug port first."
|
|
131
|
-
: undefined
|
|
129
|
+
: undefined
|
|
132
130
|
: undefined;
|
|
133
131
|
const validationError = semanticActionResult.error
|
|
134
132
|
?? jobResult.error
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
5
|
+
import { isRecord } from "../parsing.js";
|
|
6
|
+
import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../process-environment.js";
|
|
7
|
+
import { runAgentBrowserProcess } from "../process.js";
|
|
8
|
+
import { parseAgentBrowserEnvelope } from "../results/envelope.js";
|
|
9
|
+
import { isPlainTextInspectionArgs } from "../runtime.js";
|
|
10
|
+
// Native `session` reports the resolved name, but not whether it was configured.
|
|
11
|
+
// Read only identity presence/namespace; native validates the rest of its schema.
|
|
12
|
+
async function readNativeIdentity(path, cwd, signal) {
|
|
13
|
+
let config;
|
|
14
|
+
try {
|
|
15
|
+
config = JSON.parse(await readFile(path, "utf8"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(config) || (typeof config.session !== "string" && typeof config.namespace !== "string"))
|
|
21
|
+
return {};
|
|
22
|
+
const result = await runAgentBrowserProcess({ args: ["--config", path, "--json", "session"], cwd, signal, timeoutMs: 5_000 });
|
|
23
|
+
try {
|
|
24
|
+
if (result.aborted || result.timedOut || result.spawnError)
|
|
25
|
+
throw new Error("Could not resolve native agent-browser session configuration; the browser command was not run.");
|
|
26
|
+
if (result.exitCode !== 0)
|
|
27
|
+
return {}; // Native ignores invalid discovered files; explicit files fail again in the actual command.
|
|
28
|
+
const parsed = await parseAgentBrowserEnvelope({ stdout: result.stdout, stdoutPath: result.stdoutSpillPath });
|
|
29
|
+
const data = parsed.envelope?.data;
|
|
30
|
+
if (!isRecord(data) || typeof data.session !== "string")
|
|
31
|
+
throw new Error("Native agent-browser session inspection returned no session name; the browser command was not run.");
|
|
32
|
+
return {
|
|
33
|
+
...(typeof config.session === "string" ? { session: data.session } : {}),
|
|
34
|
+
...(typeof config.namespace === "string" ? { namespace: config.namespace } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (result.stdoutSpillPath)
|
|
39
|
+
await rm(result.stdoutSpillPath, { force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function withNativeSessionDefaults(input, cwd, signal, run) {
|
|
43
|
+
if (input.kind === "electron" && input.compiledElectron.action === "launch")
|
|
44
|
+
return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
|
|
45
|
+
if (input.kind === "script" || input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
|
|
46
|
+
return run(input);
|
|
47
|
+
const env = getAgentBrowserProcessEnvironment();
|
|
48
|
+
const configArg = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--config")[0];
|
|
49
|
+
const configPath = configArg?.value ?? env.AGENT_BROWSER_CONFIG;
|
|
50
|
+
const paths = configPath !== undefined
|
|
51
|
+
? [resolve(cwd, configPath)]
|
|
52
|
+
: [join(homedir(), ".agent-browser", "config.json"), join(cwd, "agent-browser.json")];
|
|
53
|
+
let identity = {};
|
|
54
|
+
for (const path of paths)
|
|
55
|
+
identity = { ...identity, ...await readNativeIdentity(path, cwd, signal) };
|
|
56
|
+
const session = env.AGENT_BROWSER_SESSION ?? identity.session;
|
|
57
|
+
const namespace = env.AGENT_BROWSER_NAMESPACE ?? identity.namespace;
|
|
58
|
+
let args = input.toolArgs;
|
|
59
|
+
if (session !== undefined && extractExplicitSessionName(args) === undefined)
|
|
60
|
+
args = ["--session", session, ...args];
|
|
61
|
+
const idleTimeout = scanUpstreamGlobalFlagOccurrences(args, "--idle-timeout").at(-1)?.value;
|
|
62
|
+
return withAgentBrowserProcessEnvironment({
|
|
63
|
+
...(idleTimeout !== undefined ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: idleTimeout } : {}),
|
|
64
|
+
...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
|
|
65
|
+
...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
|
|
66
|
+
...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
|
|
67
|
+
}, () => run({ ...input, toolArgs: args }));
|
|
68
|
+
}
|
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, extname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { isRecord } from "../parsing.js";
|
|
4
4
|
import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
|
|
5
|
+
import { parseCommandInfo, redactSensitiveValue } from "../runtime.js";
|
|
5
6
|
export function normalizeRequestedOutputPath(path) {
|
|
6
7
|
return path.startsWith("@") ? path.slice(1) : path;
|
|
7
8
|
}
|
|
@@ -11,7 +12,21 @@ function getTextContent(result) {
|
|
|
11
12
|
.map((item) => item.text)
|
|
12
13
|
.join("\n\n") ?? "";
|
|
13
14
|
}
|
|
15
|
+
function getResultCommand(details) {
|
|
16
|
+
const args = Array.isArray(details?.args) && details.args.every(value => typeof value === "string") ? details.args : [];
|
|
17
|
+
return parseCommandInfo(args);
|
|
18
|
+
}
|
|
19
|
+
function isRecordingReceiptResult(result) {
|
|
20
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
21
|
+
return details?.command === "record" || getResultCommand(details).command === "record" || details?.recordingRecovery !== undefined
|
|
22
|
+
|| (Array.isArray(details?.batchSteps) && details.batchSteps.some((step) => isRecord(step) && Array.isArray(step.command) && step.command[0] === "record"));
|
|
23
|
+
}
|
|
24
|
+
export function canWriteAgentBrowserOutput(result) {
|
|
25
|
+
return isRecordingReceiptResult(result) || (!result.isError && !(isRecord(result.details) && result.details.resultCategory === "failure"));
|
|
26
|
+
}
|
|
14
27
|
function getOutputSource(result) {
|
|
28
|
+
if (isRecordingReceiptResult(result))
|
|
29
|
+
return "recording-receipt";
|
|
15
30
|
return isRecord(result.details) && result.details.data !== undefined ? "details.data" : "content.text";
|
|
16
31
|
}
|
|
17
32
|
async function readCompactedSpill(path, manifest) {
|
|
@@ -42,17 +57,37 @@ async function rehydrateCompactedData(data, details, manifest) {
|
|
|
42
57
|
}
|
|
43
58
|
async function getOutputPayload(result) {
|
|
44
59
|
const details = isRecord(result.details) ? result.details : undefined;
|
|
45
|
-
if (details
|
|
60
|
+
if (!details)
|
|
46
61
|
return { source: "content.text", value: getTextContent(result) };
|
|
47
62
|
const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
|
|
48
|
-
|
|
63
|
+
const data = await rehydrateCompactedData(details.data, details, manifest);
|
|
64
|
+
if (isRecordingReceiptResult(result)) {
|
|
65
|
+
const success = !result.isError && details.resultCategory !== "failure";
|
|
66
|
+
const recovery = isRecord(details.recordingRecovery) ? details.recordingRecovery : undefined;
|
|
67
|
+
const commandInfo = getResultCommand(details);
|
|
68
|
+
return { source: "recording-receipt", value: redactSensitiveValue({
|
|
69
|
+
success, error: details.error ?? (success ? null : details.summary ?? getTextContent(result)),
|
|
70
|
+
command: details.command ?? commandInfo.command, subcommand: details.subcommand ?? commandInfo.subcommand, sessionName: details.sessionName, namespace: details.namespace,
|
|
71
|
+
attempt: recovery?.attempt ?? { success, agentBrowserStarted: details.agentBrowserStarted ?? null, exitCode: details.exitCode ?? null, timedOut: details.timedOut === true, error: details.error ?? details.validationError ?? null, parseError: details.parseError ?? null },
|
|
72
|
+
data: data ?? null, artifacts: details.artifacts, artifactVerification: details.artifactVerification, recordingRecovery: recovery,
|
|
73
|
+
}) };
|
|
74
|
+
}
|
|
75
|
+
return data === undefined ? { source: "content.text", value: getTextContent(result) } : { source: "details.data", value: data };
|
|
49
76
|
}
|
|
50
77
|
function serializeOutputPayload(value) {
|
|
51
78
|
return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
|
|
52
79
|
}
|
|
53
|
-
function appendOutputFileNotice(result, message) {
|
|
80
|
+
function appendOutputFileNotice(result, message, failed = false) {
|
|
54
81
|
const content = [...(result.content ?? [])];
|
|
55
82
|
if (content[0]?.type === "text") {
|
|
83
|
+
try {
|
|
84
|
+
const json = JSON.parse(content[0].text);
|
|
85
|
+
if (isRecord(json) && typeof json.success === "boolean") {
|
|
86
|
+
content[0] = { type: "text", text: JSON.stringify({ ...json, ...(failed ? { success: false, error: message } : {}), outputFileNotice: message }, null, 2) };
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
56
91
|
content[0] = { ...content[0], text: `${content[0].text}\n\n${message}` };
|
|
57
92
|
return content;
|
|
58
93
|
}
|
|
@@ -88,7 +123,7 @@ async function pathsReferToSameFile(left, right) {
|
|
|
88
123
|
export async function applyAgentBrowserOutputPath(options) {
|
|
89
124
|
if (!options.outputPath)
|
|
90
125
|
return options.result;
|
|
91
|
-
if (
|
|
126
|
+
if (!canWriteAgentBrowserOutput(options.result))
|
|
92
127
|
return options.result;
|
|
93
128
|
const requestedPath = normalizeRequestedOutputPath(options.outputPath);
|
|
94
129
|
const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
|
|
@@ -102,7 +137,7 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
102
137
|
delete details.successCategory;
|
|
103
138
|
return {
|
|
104
139
|
...options.result,
|
|
105
|
-
content: appendOutputFileNotice(options.result, `Output file rejected: ${message}
|
|
140
|
+
content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`, true),
|
|
106
141
|
details: { ...details, failureCategory: "validation-error", outputFile, resultCategory: "failure" },
|
|
107
142
|
isError: true,
|
|
108
143
|
};
|
|
@@ -133,7 +168,7 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
133
168
|
: { failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
|
|
134
169
|
return {
|
|
135
170
|
...options.result,
|
|
136
|
-
content:
|
|
171
|
+
content: appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`, true),
|
|
137
172
|
details,
|
|
138
173
|
isError: true,
|
|
139
174
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
2
2
|
import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, } from "./argv-grammar.js";
|
|
3
|
-
import { needsManagedSession } from "./command-policy.js";
|
|
3
|
+
import { isBrowserIndependentRead, needsManagedSession } from "./command-policy.js";
|
|
4
4
|
import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
|
|
5
5
|
import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
|
|
6
6
|
const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly before page-content inspection.";
|
|
@@ -167,23 +167,27 @@ function isRecoveringPageTransitionCommand(command, subcommand) {
|
|
|
167
167
|
&& isUnverifiedPageTransitionCommand(command, subcommand);
|
|
168
168
|
}
|
|
169
169
|
function getUnverifiedPageError(options) {
|
|
170
|
-
|
|
170
|
+
const descriptor = parseArgvDescriptor(options.args);
|
|
171
|
+
if (!options.pageUrlUnknown || !needsManagedSession(descriptor))
|
|
171
172
|
return undefined;
|
|
172
|
-
const { command, subcommand } =
|
|
173
|
+
const { command, subcommand } = descriptor.commandInfo;
|
|
173
174
|
const closesPage = ["close", "exit", "quit"].includes(command ?? "") || (command === "tab" && subcommand === "close");
|
|
174
175
|
const inspectsTarget = (command === "tab" && subcommand === "list") || (command === "get" && subcommand === "url");
|
|
175
176
|
const selectsTab = command === "tab" && subcommand !== undefined && !["close", "list", "new"].includes(subcommand);
|
|
176
177
|
const settlesPendingWebMcp = command === "webmcp" && ["result", "cancel"].includes(subcommand ?? "");
|
|
178
|
+
const stopsRecording = command === "record" && subcommand === "stop";
|
|
177
179
|
const handlesBlockingDialog = command === "dialog" && ["status", "accept", "dismiss"].includes(subcommand ?? "");
|
|
178
180
|
const transitionsPage = options.allowUnverifiedPageTransitions === true && isRecoveringPageTransitionCommand(command, subcommand);
|
|
179
181
|
const navigatesExplicitly = getExplicitNavigationTarget(options.args) !== undefined;
|
|
180
|
-
return closesPage || handlesBlockingDialog || inspectsTarget || selectsTab || settlesPendingWebMcp || transitionsPage || navigatesExplicitly || (options.trustedBatchTabSelection && command === "tab")
|
|
182
|
+
return closesPage || handlesBlockingDialog || inspectsTarget || selectsTab || settlesPendingWebMcp || stopsRecording || transitionsPage || navigatesExplicitly || (options.trustedBatchTabSelection && command === "tab")
|
|
181
183
|
? undefined
|
|
182
184
|
: UNVERIFIED_PAGE_MESSAGE;
|
|
183
185
|
}
|
|
184
186
|
export function getPageTargetValidationError(options) {
|
|
185
187
|
const descriptor = parseArgvDescriptor(options.args);
|
|
186
188
|
const command = descriptor.commandInfo.command;
|
|
189
|
+
if (isBrowserIndependentRead(descriptor.upstreamCommandTokens, options.stdin))
|
|
190
|
+
return undefined;
|
|
187
191
|
if (["close", "exit", "quit"].includes(command ?? ""))
|
|
188
192
|
return undefined;
|
|
189
193
|
if (command === "batch") {
|
|
@@ -254,7 +258,7 @@ export function getPageTargetValidationError(options) {
|
|
|
254
258
|
}
|
|
255
259
|
export function getExplicitSessionPageVerificationRequirement(options) {
|
|
256
260
|
const descriptor = parseArgvDescriptor(options.args);
|
|
257
|
-
if (!needsManagedSession(descriptor))
|
|
261
|
+
if (!needsManagedSession(descriptor, options.stdin))
|
|
258
262
|
return undefined;
|
|
259
263
|
if (isRecoveringPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand))
|
|
260
264
|
return undefined;
|
|
@@ -14,13 +14,14 @@ export const QUICK_START_GUIDELINES = [
|
|
|
14
14
|
"Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, direct current targets such as { semanticAction: { action: \"fill\", selector: \"@e1\", text: \"prompt\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/fill actions to avoid hidden duplicate matches; semanticAction does not expose uncheck while upstream find ... uncheck is not runtime-supported, so use raw uncheck with a stable selector or current ref; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
|
|
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
|
-
"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
|
|
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
|
|
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 and all-read batches skip browser helpers and managed replacement, including timeout recovery; omit the URL for verified 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 finalizes the target. Recording receipts separate captured-frame rate/wall duration from nominal FPS and held/encoded frames; unknown native metrics stay unknown, and sparse or final-only frames cannot prove smoothness. A stop timeout gets one bounded session-info receipt query; follow its exact status/stop actions. Distinct outputPath exports retain failed attempts too. 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
|
+
"Use session info for one read-only preflight distinguishing daemon PID/activity from native browser liveness, Chrome PID, exact profile, tabs and native ownership; Pi cleanup ownership is separate and absent fields stay unknown. For policy-required URL reads, use the exact confirm/deny actions and session they return. Only native explicit-read provenance plus capabilities.readRequiresConfirmation enables browserless confirmation; legacy/DOM confirmations retain normal page checks.",
|
|
19
20
|
"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
21
|
];
|
|
21
22
|
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.";
|
|
22
23
|
export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
23
|
-
"Use top-level script only for one-shot loops, conditional page branches, or multi-page aggregation that would otherwise require several calls: await browser({ args, stdin?, timeoutMs? }), branch on its ok field, and emit one bounded JSON value. Script gets an isolated non-profile browser session that is always closed, cannot use caller session/namespace/lifecycle/attachment controls, inherited agent-browser launch/proxy settings, or host APIs, and is not a reusable named recipe. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear flows.",
|
|
24
|
+
"Use top-level script only for one-shot loops, conditional page branches, or multi-page aggregation that would otherwise require several calls: await browser({ args, stdin?, timeoutMs? }), branch on its ok field, and emit one bounded JSON value. Script gets an isolated non-profile browser session that is always closed, cannot use caller session/namespace/lifecycle/attachment controls, inherited agent-browser launch/proxy settings, user/project config or explicit --config, or host APIs, and is not a reusable named recipe. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear flows.",
|
|
24
25
|
"Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs are page-scoped; the wrapper fails mutation-prone stale/recycled refs before upstream can silently target a different current-page element. On dense pages, use wrapper-side snapshot -i --search <text> or snapshot -i --filter role=<role> to render matching refs while preserving the full ref map in details.refSnapshot, add snapshot --viewport when scroll position or above/below-fold context matters, and add snapshot --diff when a quick before/after ref-map delta would prevent reading a full spill file.",
|
|
25
26
|
"For ordinary forms from one snapshot, batch multiple fill @refs before the submit/click step to avoid serial tool calls; if a fill may autosubmit, navigate, or rerender later fields, split the flow and refresh refs first.",
|
|
26
27
|
"Do not use browser automation to drive public search-engine forms such as Google for discovery; headless jobs that type a query and press Enter can be redirected to anti-bot or CAPTCHA pages. Prefer agent_browser_web_search for live discovery, then agent_browser on a target URL. Do not attempt CAPTCHA bypass.",
|
|
@@ -29,13 +30,13 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
29
30
|
"For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard type for framework-controlled editors that require real key events. keyboard inserttext is paste-like and can change a DOM value without updating application state, so use it only when later application-state evidence proves the edit was accepted. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
|
|
30
31
|
"Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
|
|
31
32
|
"For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data. On macOS, copied Chrome profiles may omit encrypted cookies, so profile selection alone is not proof of authentication; verify the target page and use a user-approved headed login once when needed. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
|
|
32
|
-
"
|
|
33
|
+
"Use bare calls for a configured native shared session; the wrapper honors its session/namespace across Pi agents without owning quit cleanup or idle policy. Otherwise use the implicit session for routine work. Coordinate shared tabs and do not close another agent's browser.",
|
|
33
34
|
`When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.`,
|
|
34
35
|
"Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.",
|
|
35
36
|
"After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume and live-checks the URL before later page reads/interactions because an attached browser can drift externally; caller config, file access, launch arguments, and environment pass through unchanged. A successful close clears the marker. When several named sessions share one Chrome, pass --pin-tab once (AGENT_BROWSER_PIN_TAB) so a closed bound tab fails as tab_gone instead of acting on a neighbor; recover with tab new or tab list. --no-pin-tab turns the sticky pin off. tab list includes each tab's CDP targetId, accepted as a tab ref.",
|
|
36
37
|
`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
38
|
"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.",
|
|
39
|
+
"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
40
|
"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
41
|
"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
42
|
"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 +58,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
57
58
|
"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
59
|
"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
60
|
"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
|
-
"
|
|
61
|
+
"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
62
|
"Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
|
|
62
63
|
];
|
|
63
64
|
export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
@@ -69,7 +70,7 @@ export const INSPECTION_TOOL_CALL_EXAMPLES = [
|
|
|
69
70
|
];
|
|
70
71
|
export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
|
|
71
72
|
"After open/goto/navigate calls with --profile, --restore, --session-name, or --state, agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch or reconnect.",
|
|
72
|
-
"After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list
|
|
73
|
+
"After confirmed shutdown of an automatically restored managed session, the wrapper retains its complete recorded URL, including the fragment, until the first current-page operation (including get url and reload). Non-page calls such as tab list may start a daemon without fulfilling that reopen; explicit URL reads leave the managed browser and pending reopen untouched. The wrapper uses native open once, verifies the observed tab, and discards old refs/frame scope; it does not restore unsaved forms, JavaScript memory, or history. Explicit navigation, caller-owned/attached sessions, and restore-disabled sessions are not auto-reopened.",
|
|
73
74
|
"For a still-live browser after tab drift or resume, the wrapper verifies/selects the intended tab before ref/semantic helpers and page commands; failed selection stops the call without navigating. Local commands, read <url>, URL a11y/vitals, diff url, window new, and explicit tab/navigation/connection/state recovery do not require the prior tab. Batch checks follow effective rows past non-page prefixes and stop at explicit context changes, preserving caller argv/stdin and continue-on-error behavior. Same-tab reselection is avoided because it clears refs. Use exact batch --bail for fail-fast, not --bail=<value>. Routine same-session calls skip tab-list preflights.",
|
|
74
75
|
"For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.",
|
|
75
76
|
"If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
|
|
@@ -79,23 +80,23 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
|
|
|
79
80
|
export const RUNTIME_PROMPT_GUIDELINES = [
|
|
80
81
|
"Use agent_browser with one input mode: script, args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin: batch/eval/auth/wrapper batch only; electron rejects it; never pass --json.",
|
|
81
82
|
"For agent_browser, use open → snapshot -i → @refs; re-snapshot after changes. In authenticated unattended/auto-approved employee flows, ordinary requested non-destructive submissions may proceed. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
|
|
82
|
-
"
|
|
83
|
-
"agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close;
|
|
83
|
+
"agent_browser honors native shared-session defaults on bare calls; do not relaunch/close a shared browser. Without a selected native session, use sessionMode=fresh for launch flags. Use requested/configured profiles only; verify auth. Profile content is model-visible; script is always disposable.",
|
|
84
|
+
"agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; ffmpeg before recording; close keeps files; waited:timeout proves nothing.",
|
|
84
85
|
"When agent_browser details.nextActions exists, use them. Check Omitted high-value controls in dense snapshots. Dashboards: verify scroll via screenshot/snapshot.",
|
|
85
86
|
"agent_browser: read <url> for docs/text or active DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>. Batch 3+ getters; heed visibility warnings.",
|
|
86
87
|
];
|
|
87
88
|
export function buildBrowserExecutablePathGuideline(executablePath) {
|
|
88
89
|
if (!executablePath)
|
|
89
90
|
return undefined;
|
|
90
|
-
return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}; for fresh browser launches that should use that Chromium-compatible executable, add --executable-path ${JSON.stringify(executablePath)} with sessionMode:fresh. The upstream profiles command still lists Chrome profiles only; for non-Chrome Chromium login state, ask the user for an explicit profile/user-data directory path or inspect local setup with profiles/doctor before recommending a profile value.`;
|
|
91
|
+
return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}; reuse a configured native shared session with bare calls; otherwise for fresh browser launches that should use that Chromium-compatible executable, add --executable-path ${JSON.stringify(executablePath)} with sessionMode:fresh. The upstream profiles command still lists Chrome profiles only; for non-Chrome Chromium login state, ask the user for an explicit profile/user-data directory path or inspect local setup with profiles/doctor before recommending a profile value.`;
|
|
91
92
|
}
|
|
92
93
|
export function buildBrowserDefaultProfileGuideline(profile) {
|
|
93
94
|
if (!profile || profile.policy === "explicit-only")
|
|
94
95
|
return undefined;
|
|
95
96
|
if (profile.policy === "always") {
|
|
96
|
-
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always; use --profile ${JSON.stringify(profile.name)} with sessionMode:fresh when a fresh browser launch should use the configured profile, and treat profile content as model-visible user data.`;
|
|
97
|
+
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always; reuse a configured native shared session with bare calls, otherwise use --profile ${JSON.stringify(profile.name)} with sessionMode:fresh when a fresh browser launch should use the configured profile, and treat profile content as model-visible user data.`;
|
|
97
98
|
}
|
|
98
|
-
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)}; for signed-in/account-specific browser tasks, start with --profile ${JSON.stringify(profile.name)} plus sessionMode:fresh unless the user asks for a different profile.`;
|
|
99
|
+
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)}; for signed-in/account-specific browser tasks, reuse a configured native shared session with bare calls, otherwise start with --profile ${JSON.stringify(profile.name)} plus sessionMode:fresh unless the user asks for a different profile.`;
|
|
99
100
|
}
|
|
100
101
|
export function buildToolPromptGuidelines(options) {
|
|
101
102
|
const browserDefaultProfileGuideline = buildBrowserDefaultProfileGuideline(options.browserDefaultProfile);
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { writeSecureTempFile } from "./temp.js";
|
|
2
4
|
const isolatedAgentBrowserEnvironment = new AsyncLocalStorage();
|
|
5
|
+
const agentBrowserProcessEnvironment = new AsyncLocalStorage();
|
|
3
6
|
const PROXY_ENV_NAMES = new Set(["ALL_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"]);
|
|
4
7
|
export function getAgentBrowserProcessEnvironment(baseEnv = process.env) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const isolatedConfig = isolatedAgentBrowserEnvironment.getStore();
|
|
9
|
+
if (isolatedConfig === undefined)
|
|
10
|
+
return { ...baseEnv, ...agentBrowserProcessEnvironment.getStore() };
|
|
11
|
+
return {
|
|
12
|
+
...Object.fromEntries(Object.entries(baseEnv).filter(([name]) => {
|
|
13
|
+
const normalizedName = name.toUpperCase();
|
|
14
|
+
return !normalizedName.startsWith("AGENT_BROWSER_") && !PROXY_ENV_NAMES.has(normalizedName);
|
|
15
|
+
})),
|
|
16
|
+
AGENT_BROWSER_CONFIG: isolatedConfig,
|
|
17
|
+
};
|
|
11
18
|
}
|
|
12
|
-
export function
|
|
13
|
-
return
|
|
19
|
+
export function withAgentBrowserProcessEnvironment(env, run) {
|
|
20
|
+
return agentBrowserProcessEnvironment.run({ ...agentBrowserProcessEnvironment.getStore(), ...env }, run);
|
|
21
|
+
}
|
|
22
|
+
export async function withIsolatedAgentBrowserEnvironment(run) {
|
|
23
|
+
if (isolatedAgentBrowserEnvironment.getStore() !== undefined)
|
|
24
|
+
return await run();
|
|
25
|
+
const path = await writeSecureTempFile({ content: "{}", prefix: "script-config", suffix: ".json" });
|
|
26
|
+
try {
|
|
27
|
+
return await isolatedAgentBrowserEnvironment.run(path, run);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await rm(path, { force: true });
|
|
31
|
+
}
|
|
14
32
|
}
|
|
@@ -286,10 +286,12 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
|
|
|
286
286
|
clampUpstreamDefaultTimeout(childEnv);
|
|
287
287
|
return childEnv;
|
|
288
288
|
}
|
|
289
|
-
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false) {
|
|
289
|
+
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, browserIndependentReadConfirmation = false) {
|
|
290
290
|
if (!validateManagedSessionRestoreContextForSpawn(options)) {
|
|
291
291
|
return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
|
|
292
292
|
}
|
|
293
|
+
if (browserIndependentReadConfirmation)
|
|
294
|
+
return undefined;
|
|
293
295
|
return getPageTargetValidationError({
|
|
294
296
|
args: options.args,
|
|
295
297
|
currentPageUrl,
|
|
@@ -316,7 +318,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
316
318
|
restoreState: managedSessionRestoreState,
|
|
317
319
|
stdin,
|
|
318
320
|
};
|
|
319
|
-
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown);
|
|
321
|
+
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
320
322
|
if (planningPolicyError) {
|
|
321
323
|
return {
|
|
322
324
|
aborted: false,
|
|
@@ -331,7 +333,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
331
333
|
const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
332
334
|
const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
|
|
333
335
|
const processOverrides = {
|
|
334
|
-
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
336
|
+
...(ownedManagedSession ? { [AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()) } : {}),
|
|
335
337
|
...managedSessionRestoreEnv,
|
|
336
338
|
...env,
|
|
337
339
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
@@ -340,7 +342,8 @@ export async function runAgentBrowserProcess(options) {
|
|
|
340
342
|
};
|
|
341
343
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
342
344
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
343
|
-
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
|
|
345
|
+
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
|
|
346
|
+
?? (!ownedManagedSession ? parentEnv[AGENT_BROWSER_SOCKET_DIR_ENV] : undefined) ?? getAgentBrowserSocketDir();
|
|
344
347
|
if (requestedSocketDir !== undefined) {
|
|
345
348
|
const socketDirError = requestedSocketDir.length > 0
|
|
346
349
|
? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
|
|
@@ -463,7 +466,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
463
466
|
});
|
|
464
467
|
};
|
|
465
468
|
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
466
|
-
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown);
|
|
469
|
+
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
467
470
|
if (spawnPolicyError) {
|
|
468
471
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
469
472
|
return;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
|
+
import { extractExplicitSessionName, getAgentBrowserSessionIdentityKey, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./argv-grammar.js";
|
|
3
|
+
import { getExplicitReadUrl } from "./command-policy.js";
|
|
4
|
+
import { isCloseCommand } from "./command-taxonomy.js";
|
|
5
|
+
import { isRecord } from "./parsing.js";
|
|
6
|
+
export function parseReadConfirmation(value) {
|
|
7
|
+
if (!isRecord(value) || value.source !== "native-explicit-url-read" || (value.state !== "pending" && value.state !== "cleared"))
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value.id !== "string" || !value.id || typeof value.sessionName !== "string" || !value.sessionName || (value.namespace !== undefined && typeof value.namespace !== "string"))
|
|
10
|
+
return undefined;
|
|
11
|
+
return { ...(isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: "native-explicit-url-read", state: value.state };
|
|
12
|
+
}
|
|
13
|
+
export function findReadConfirmation(args, confirmations, namespace) {
|
|
14
|
+
const tokens = extractUpstreamCommandTokens(args);
|
|
15
|
+
if (tokens.length !== 2 || !["confirm", "deny"].includes(tokens[0]))
|
|
16
|
+
return undefined;
|
|
17
|
+
const sessionName = extractExplicitSessionName(args);
|
|
18
|
+
const effectiveNamespace = resolveAgentBrowserNamespace(args, namespace);
|
|
19
|
+
const matches = [...confirmations].filter(value => value.state === "pending" && value.id === tokens[1]
|
|
20
|
+
&& (sessionName === undefined || getAgentBrowserSessionIdentityKey(sessionName, value.namespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace))
|
|
21
|
+
&& (effectiveNamespace === undefined || getAgentBrowserSessionIdentityKey(value.sessionName, effectiveNamespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace)));
|
|
22
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function scopeReadConfirmationArgs(args, confirmation) {
|
|
25
|
+
return [
|
|
26
|
+
...(scanUpstreamGlobalFlagOccurrences(args, "--namespace").length === 0 ? ["--namespace", confirmation.namespace ?? ""] : []),
|
|
27
|
+
...(extractExplicitSessionName(args) === undefined ? ["--session", confirmation.sessionName] : []),
|
|
28
|
+
...args,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function nextReadConfirmation(options) {
|
|
32
|
+
const { commandTokens: tokens, current } = options;
|
|
33
|
+
const settlesRead = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
|
|
34
|
+
const confirmedResult = settlesRead && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true && options.data.action === "read" && isRecord(options.data.result)
|
|
35
|
+
? options.data.result.data : options.data;
|
|
36
|
+
// Native control fields only. Never parse response content, snapshot text or nested page JSON as provenance.
|
|
37
|
+
if (isRecord(confirmedResult) && confirmedResult.confirmation_required === true && typeof confirmedResult.confirmation_id === "string" && confirmedResult.confirmation_id && !("content" in confirmedResult)) {
|
|
38
|
+
if (confirmedResult.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || settlesRead)) {
|
|
39
|
+
return { ...(isRecord(confirmedResult.capabilities) && confirmedResult.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: confirmedResult.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
|
|
40
|
+
}
|
|
41
|
+
if (current?.state === "pending")
|
|
42
|
+
return { ...current, state: "cleared" };
|
|
43
|
+
}
|
|
44
|
+
return options.succeeded && current?.state === "pending" && (settlesRead || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
|
|
45
|
+
}
|
|
46
|
+
export function buildReadConfirmationNextActions(confirmation, pendingResponse) {
|
|
47
|
+
if (confirmation.state === "cleared")
|
|
48
|
+
return [];
|
|
49
|
+
const prefix = ["--namespace", confirmation.namespace ?? "", "--session", confirmation.sessionName];
|
|
50
|
+
if (!pendingResponse)
|
|
51
|
+
return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the read confirmation failed; rerun the original URL read if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
|
|
52
|
+
return ["confirm", "deny"].map(command => ({
|
|
53
|
+
id: command === "confirm" ? "approve-confirmation" : "deny-confirmation", tool: "agent_browser", params: { args: [...prefix, command, confirmation.id] },
|
|
54
|
+
reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for this explicit URL read.`,
|
|
55
|
+
safety: confirmation.capabilities?.readRequiresConfirmation === true
|
|
56
|
+
? "Review the requested read first. The native capability proves ID matching; no DOM confirmation is implied."
|
|
57
|
+
: "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
@@ -14,6 +14,8 @@ function getArtifactReservation(artifact) {
|
|
|
14
14
|
cwd: artifact.cwd ?? process.cwd(),
|
|
15
15
|
namespace: artifact.namespace,
|
|
16
16
|
path: artifact.path,
|
|
17
|
+
...(artifact.recording?.recordingId ? { recordingId: artifact.recording.recordingId } : {}),
|
|
18
|
+
...(artifact.recordingStartedAtMs !== undefined ? { startedAtMs: artifact.recordingStartedAtMs } : {}),
|
|
17
19
|
sessionName: artifact.session,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -43,7 +45,7 @@ export function applyRecordingArtifactsToReservations(reservations, artifacts) {
|
|
|
43
45
|
for (const [key, pending] of pendingBySession) {
|
|
44
46
|
const existing = reservations.get(key);
|
|
45
47
|
reservations.set(key, pending);
|
|
46
|
-
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd) {
|
|
48
|
+
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd || existing.recordingId !== pending.recordingId || existing.startedAtMs !== pending.startedAtMs) {
|
|
47
49
|
transitions.push({ reservation: pending, state: "active" });
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -63,6 +65,8 @@ export function appendRecordingReservationTransition(pi, transition) {
|
|
|
63
65
|
cwd: state === "active" ? reservation.cwd : undefined,
|
|
64
66
|
namespace: reservation.namespace,
|
|
65
67
|
path: state === "active" ? reservation.path : undefined,
|
|
68
|
+
recordingId: state === "active" ? reservation.recordingId : undefined,
|
|
69
|
+
startedAtMs: state === "active" ? reservation.startedAtMs : undefined,
|
|
66
70
|
sessionName: reservation.sessionName,
|
|
67
71
|
state,
|
|
68
72
|
version: 1,
|
|
@@ -81,6 +85,10 @@ function parseReservationTransition(data) {
|
|
|
81
85
|
state: "closed",
|
|
82
86
|
};
|
|
83
87
|
}
|
|
88
|
+
if (data.recordingId !== undefined && (typeof data.recordingId !== "string" || !data.recordingId))
|
|
89
|
+
return undefined;
|
|
90
|
+
if (data.startedAtMs !== undefined && (typeof data.startedAtMs !== "number" || !Number.isFinite(data.startedAtMs)))
|
|
91
|
+
return undefined;
|
|
84
92
|
if (typeof data.absolutePath !== "string" || !isAbsolute(data.absolutePath)
|
|
85
93
|
|| typeof data.cwd !== "string" || !isAbsolute(data.cwd) || typeof data.path !== "string")
|
|
86
94
|
return undefined;
|
|
@@ -90,6 +98,8 @@ function parseReservationTransition(data) {
|
|
|
90
98
|
cwd: data.cwd,
|
|
91
99
|
namespace: data.namespace,
|
|
92
100
|
path: data.path,
|
|
101
|
+
...(typeof data.recordingId === "string" ? { recordingId: data.recordingId } : {}),
|
|
102
|
+
...(typeof data.startedAtMs === "number" ? { startedAtMs: data.startedAtMs } : {}),
|
|
93
103
|
sessionName: data.sessionName,
|
|
94
104
|
},
|
|
95
105
|
state: "active",
|
|
@@ -197,6 +197,14 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
197
197
|
break;
|
|
198
198
|
case "timeout":
|
|
199
199
|
{
|
|
200
|
+
if (options.command === "session" && options.subcommand === "info") {
|
|
201
|
+
actions.push(buildNextToolAction({
|
|
202
|
+
args: ["session", "info"],
|
|
203
|
+
id: "retry-session-info",
|
|
204
|
+
reason: "Retry the same session status check without opening a browser or inspecting its page.",
|
|
205
|
+
}));
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
200
208
|
const textAssertion = options.command === "wait" && options.args?.includes("--text") === true;
|
|
201
209
|
const urlAssertion = options.command === "wait" && options.args?.includes("--url") === true;
|
|
202
210
|
actions.push(buildNextToolAction({
|