pi-agent-browser-native 0.3.0 → 0.6.5
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 +265 -0
- package/README.md +130 -54
- package/dist/extensions/agent-browser/index.js +781 -169
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
- package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +29 -25
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
- package/dist/extensions/agent-browser/lib/process.js +130 -104
- package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
- package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +186 -108
- package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
- package/dist/extensions/agent-browser/lib/temp.js +1 -2
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +108 -24
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +21 -0
- package/docs/ARCHITECTURE.md +57 -34
- package/docs/COMMAND_REFERENCE.md +255 -68
- package/docs/ELECTRON.md +2 -2
- package/docs/RELEASE.md +12 -10
- package/docs/REQUIREMENTS.md +11 -8
- package/docs/SUPPORT_MATRIX.md +36 -24
- package/docs/TOOL_CONTRACT.md +169 -95
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +87 -9
- package/scripts/agent-browser-target.mjs +21 -0
- package/scripts/build.mjs +41 -0
- package/scripts/config.mjs +1 -0
- package/scripts/doctor.mjs +16 -9
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
|
@@ -4,32 +4,43 @@ import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
import { env as processEnv, platform as processPlatform } from "node:process";
|
|
6
6
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
7
|
-
import { needsManagedSession } from "./command-policy.js";
|
|
8
7
|
import { isKnownCommandToken } from "./command-taxonomy.js";
|
|
9
|
-
import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { getImplicitSessionIdleTimeoutMs
|
|
8
|
+
import { extractExplicitSessionName, getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, resolveAgentBrowserNamespace, } from "./argv-grammar.js";
|
|
9
|
+
import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
10
|
+
import { getPageTargetValidationError, } from "./page-target-validation.js";
|
|
11
|
+
import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
|
|
12
|
+
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
13
13
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
14
14
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
15
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
16
16
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
17
17
|
const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
|
|
18
18
|
const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
|
|
19
|
-
const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
|
|
20
19
|
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
21
20
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
22
21
|
const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
|
|
22
|
+
const PI_AGENT_BROWSER_SOCKET_DIR_ENV = "PI_AGENT_BROWSER_SOCKET_DIR";
|
|
23
23
|
const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
|
|
24
|
+
const TERMUX_PACKAGE_NAME_PATTERN = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
|
|
24
25
|
export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
|
|
25
26
|
const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
26
27
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
27
28
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
28
29
|
const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
|
|
29
30
|
const attachedBrowserSessionContext = new AsyncLocalStorage();
|
|
31
|
+
const WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND = new Set([
|
|
32
|
+
"auth", "clipboard", "cookies", "dashboard", "device", "dialog", "diff", "find", "get", "is", "keyboard",
|
|
33
|
+
"mouse", "network", "plugin", "profiler", "react", "record", "session", "set", "skills", "state", "storage",
|
|
34
|
+
"stream", "tab", "trace", "webmcp", "window",
|
|
35
|
+
]);
|
|
30
36
|
export function withAttachedBrowserSessionContext(preserve, run) {
|
|
31
37
|
return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
|
|
32
38
|
}
|
|
39
|
+
export function getWindowsExplicitDefaultNamespaceEnv(args, parentNamespace, platform = processPlatform) {
|
|
40
|
+
return platform === "win32" && resolveAgentBrowserNamespace(args, parentNamespace) === ""
|
|
41
|
+
? { AGENT_BROWSER_NAMESPACE: "" }
|
|
42
|
+
: {};
|
|
43
|
+
}
|
|
33
44
|
function appendTail(text, addition, maxChars) {
|
|
34
45
|
const combined = text + addition;
|
|
35
46
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
@@ -43,7 +54,12 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
43
54
|
for (let index = 0; index < args.length; index += 1) {
|
|
44
55
|
const token = args[index];
|
|
45
56
|
if (isKnownCommandToken(token)) {
|
|
46
|
-
|
|
57
|
+
if (index === 0)
|
|
58
|
+
return args;
|
|
59
|
+
const firstPositional = args[index + 1];
|
|
60
|
+
return WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND.has(token) && firstPositional && !firstPositional.startsWith("-")
|
|
61
|
+
? [token, firstPositional, ...leadingGlobals, ...args.slice(index + 2)]
|
|
62
|
+
: [token, ...leadingGlobals, ...args.slice(index + 1)];
|
|
47
63
|
}
|
|
48
64
|
if (!token.startsWith("-"))
|
|
49
65
|
return args;
|
|
@@ -77,6 +93,13 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
77
93
|
const value = args[index + 1];
|
|
78
94
|
if (value === undefined)
|
|
79
95
|
return args;
|
|
96
|
+
// PowerShell -> .cmd drops empty argv values. Planning rejects empty
|
|
97
|
+
// caller --args; keep this defensive skip so an unexpected empty value
|
|
98
|
+
// cannot turn the next flag into its accidental value on native Windows.
|
|
99
|
+
if (value === "" && (flag === "--args" || flag === "--namespace")) {
|
|
100
|
+
index += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
80
103
|
leadingGlobals.push(token, value);
|
|
81
104
|
index += 1;
|
|
82
105
|
continue;
|
|
@@ -85,27 +108,10 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
85
108
|
}
|
|
86
109
|
return args;
|
|
87
110
|
}
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (token.startsWith("--allow-file-access="))
|
|
93
|
-
continue;
|
|
94
|
-
if (token === "--allow-file-access") {
|
|
95
|
-
if (["false", "true"].includes(args[index + 1] ?? ""))
|
|
96
|
-
index += 1;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
filtered.push(token);
|
|
100
|
-
}
|
|
101
|
-
// These are launch-only controls. Sending them on an attached-session follow-up makes upstream replace the CDP connection with a local browser.
|
|
102
|
-
if (preserveAttachedBrowserSession)
|
|
103
|
-
return filtered;
|
|
104
|
-
// Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
|
|
105
|
-
const browserArgs = wrapperCompatibilityUserAgent
|
|
106
|
-
? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
|
|
107
|
-
: "";
|
|
108
|
-
return ["--args", browserArgs, "--allow-file-access", "false", ...filtered];
|
|
111
|
+
export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
|
|
112
|
+
if (preserveAttachedBrowserSession || !wrapperCompatibilityUserAgent)
|
|
113
|
+
return args;
|
|
114
|
+
return ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`, ...args];
|
|
109
115
|
}
|
|
110
116
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
111
117
|
if (platform !== "win32") {
|
|
@@ -216,33 +222,43 @@ function clampUpstreamDefaultTimeout(childEnv) {
|
|
|
216
222
|
export function getAgentBrowserProcessTimeoutMs(env = processEnv) {
|
|
217
223
|
return parsePositiveIntegerEnv(env[PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV]) ?? DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS;
|
|
218
224
|
}
|
|
219
|
-
export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
225
|
+
export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined, termuxPackageName = processEnv.TERMUX_APP__PACKAGE_NAME) {
|
|
220
226
|
if (platform === "win32") {
|
|
221
227
|
return undefined;
|
|
222
228
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
229
|
+
const termuxAppRoot = platform === "android" && termuxPackageName && TERMUX_PACKAGE_NAME_PATTERN.test(termuxPackageName);
|
|
230
|
+
const prefix = platform === "darwin"
|
|
231
|
+
? "/private/tmp/piab"
|
|
232
|
+
: termuxAppRoot
|
|
233
|
+
? `/data/data/${termuxPackageName}/piab`
|
|
234
|
+
: DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
|
|
235
|
+
return `${prefix}${!termuxAppRoot && typeof uid === "number" ? `-${uid}` : ""}`;
|
|
236
|
+
}
|
|
237
|
+
export function isTrustedAndroidAppDataRoot(path, metadata, uid, platform = processPlatform) {
|
|
238
|
+
if (platform !== "android" || metadata.uid !== uid || metadata.isSymbolicLink() || !metadata.isDirectory() || (metadata.mode & 0o777) !== 0o700)
|
|
239
|
+
return false;
|
|
240
|
+
const parent = dirname(path);
|
|
241
|
+
return parent === "/data/data" || /^\/data\/user\/\d+$/.test(parent);
|
|
242
|
+
}
|
|
243
|
+
export function isTrustedSocketDirAncestor(metadata, uid, platform = processPlatform) {
|
|
244
|
+
if (metadata.isSymbolicLink())
|
|
245
|
+
return metadata.uid === 0;
|
|
246
|
+
if (!metadata.isDirectory())
|
|
247
|
+
return false;
|
|
248
|
+
const mode = metadata.mode & 0o7777;
|
|
249
|
+
if (platform === "android" && uid !== 0 && metadata.uid === uid && metadata.gid === uid)
|
|
250
|
+
return (mode & 0o002) === 0;
|
|
251
|
+
if (metadata.uid === uid && uid !== 0)
|
|
252
|
+
return (mode & 0o022) === 0;
|
|
253
|
+
return metadata.uid === 0 && ((mode & 0o022) === 0 || (mode & 0o1000) !== 0);
|
|
225
254
|
}
|
|
226
255
|
async function hasTrustedSocketDirAncestry(socketDir, uid) {
|
|
227
256
|
for (let current = dirname(socketDir);;) {
|
|
228
257
|
const metadata = await lstat(current);
|
|
229
|
-
if (metadata
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
else if (!metadata.isDirectory()) {
|
|
258
|
+
if (isTrustedAndroidAppDataRoot(current, metadata, uid))
|
|
259
|
+
return true;
|
|
260
|
+
if (!isTrustedSocketDirAncestor(metadata, uid))
|
|
234
261
|
return false;
|
|
235
|
-
}
|
|
236
|
-
if (!metadata.isSymbolicLink()) {
|
|
237
|
-
const mode = metadata.mode & 0o7777;
|
|
238
|
-
if (metadata.uid === uid) {
|
|
239
|
-
if ((mode & 0o022) !== 0)
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
else if (metadata.uid !== 0 || ((mode & 0o022) !== 0 && (mode & 0o1000) === 0)) {
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
262
|
const parent = dirname(current);
|
|
247
263
|
if (parent === current)
|
|
248
264
|
return true;
|
|
@@ -273,28 +289,64 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
|
|
|
273
289
|
}
|
|
274
290
|
return true;
|
|
275
291
|
}
|
|
276
|
-
export async function
|
|
277
|
-
if (!isAbsolute(socketDir)
|
|
278
|
-
return
|
|
292
|
+
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
293
|
+
if (!isAbsolute(socketDir))
|
|
294
|
+
return "the path is not absolute";
|
|
295
|
+
if (typeof uid !== "number")
|
|
296
|
+
return "POSIX ownership metadata is unavailable";
|
|
279
297
|
try {
|
|
280
298
|
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
281
|
-
return
|
|
299
|
+
return "an ancestor is writable, foreign-owned, a non-directory, or an untrusted symlink";
|
|
282
300
|
try {
|
|
283
301
|
await mkdir(socketDir, { mode: 0o700 });
|
|
284
302
|
}
|
|
285
303
|
catch (error) {
|
|
286
304
|
if (error.code !== "EEXIST")
|
|
287
|
-
return
|
|
305
|
+
return `the directory could not be created (${error.code ?? "unknown error"})`;
|
|
288
306
|
}
|
|
289
307
|
const metadata = await lstat(socketDir);
|
|
290
|
-
if (!metadata.isDirectory()
|
|
291
|
-
return
|
|
292
|
-
|
|
308
|
+
if (!metadata.isDirectory())
|
|
309
|
+
return "the path is not a directory";
|
|
310
|
+
if (metadata.isSymbolicLink())
|
|
311
|
+
return "the directory is a symlink";
|
|
312
|
+
if (metadata.uid !== uid)
|
|
313
|
+
return `the directory is owned by uid ${metadata.uid}, not uid ${uid}`;
|
|
314
|
+
if ((metadata.mode & 0o777) !== 0o700)
|
|
315
|
+
return `the directory mode is ${(metadata.mode & 0o777).toString(8)}, not 700`;
|
|
316
|
+
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
317
|
+
return "an ancestor became untrusted during validation";
|
|
318
|
+
if (!await socketDirEntriesAreOwned(socketDir, uid))
|
|
319
|
+
return "the directory contains a foreign-owned, symlink, special, or excessively deep entry";
|
|
320
|
+
return undefined;
|
|
293
321
|
}
|
|
294
|
-
catch {
|
|
295
|
-
return
|
|
322
|
+
catch (error) {
|
|
323
|
+
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
296
324
|
}
|
|
297
325
|
}
|
|
326
|
+
export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
327
|
+
return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
|
|
328
|
+
}
|
|
329
|
+
export function getAgentBrowserSocketPathValidationError(options) {
|
|
330
|
+
if ((options.platform ?? processPlatform) === "win32")
|
|
331
|
+
return undefined;
|
|
332
|
+
const descriptor = parseArgvDescriptor(options.args);
|
|
333
|
+
const { command } = descriptor.commandInfo;
|
|
334
|
+
// Preflight commands that can start or navigate a browser. Follow-up reads and
|
|
335
|
+
// cleanup may target a daemon created by an earlier wrapper version, so let
|
|
336
|
+
// upstream inspect those identities instead of rejecting them from path math.
|
|
337
|
+
if (!command || !["batch", "connect", "goto", "navigate", "open", "visit"].includes(command))
|
|
338
|
+
return undefined;
|
|
339
|
+
const sessionName = extractExplicitSessionName(options.args);
|
|
340
|
+
if (!sessionName)
|
|
341
|
+
return undefined;
|
|
342
|
+
const namespace = resolveAgentBrowserNamespace(options.args, options.env?.AGENT_BROWSER_NAMESPACE);
|
|
343
|
+
const socketRoot = namespace ? join(options.socketDir, "namespaces", namespace, "run") : options.socketDir;
|
|
344
|
+
const socketPath = join(socketRoot, `${sessionName}.sock`);
|
|
345
|
+
const pathBytes = Buffer.byteLength(socketPath);
|
|
346
|
+
if (pathBytes <= 103)
|
|
347
|
+
return undefined;
|
|
348
|
+
return `Agent-browser Unix socket path would be ${pathBytes} bytes (max 103) for session ${JSON.stringify(sessionName)} under ${JSON.stringify(options.socketDir)}. Set PI_AGENT_BROWSER_SOCKET_DIR to a shorter absolute private directory such as /tmp/piab-<uid> with mode 0700; retrying sessionMode \"fresh\" cannot shorten this configured root.`;
|
|
349
|
+
}
|
|
298
350
|
export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
|
|
299
351
|
const childEnv = {};
|
|
300
352
|
for (const [name, value] of Object.entries(baseEnv)) {
|
|
@@ -312,51 +364,38 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
|
|
|
312
364
|
clampUpstreamDefaultTimeout(childEnv);
|
|
313
365
|
return childEnv;
|
|
314
366
|
}
|
|
315
|
-
function getManagedPreSpawnPolicyError(options,
|
|
316
|
-
const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
|
|
317
|
-
const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
|
|
318
|
-
if (managedSessionTargetError)
|
|
319
|
-
return managedSessionTargetError;
|
|
367
|
+
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false) {
|
|
320
368
|
if (!validateManagedSessionRestoreContextForSpawn(options)) {
|
|
321
369
|
return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
|
|
322
370
|
}
|
|
323
|
-
return
|
|
371
|
+
return getPageTargetValidationError({
|
|
324
372
|
args: options.args,
|
|
325
373
|
currentPageUrl,
|
|
326
|
-
cwd: options.cwd,
|
|
327
|
-
env: effectiveEnv ?? options.env,
|
|
328
374
|
pageUrlUnknown,
|
|
329
|
-
parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
|
|
330
375
|
stdin: options.stdin,
|
|
331
376
|
trustedFirstBatchTabSelection,
|
|
332
|
-
trustedPinnedEmptyConfig,
|
|
333
377
|
});
|
|
334
378
|
}
|
|
335
379
|
export async function runAgentBrowserProcess(options) {
|
|
336
|
-
const {
|
|
380
|
+
const { cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
|
|
337
381
|
const preserveAttachedBrowserSession = options.preserveAttachedBrowserSession === true || attachedBrowserSessionContext.getStore() === true;
|
|
338
382
|
const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
|
|
339
|
-
const args =
|
|
340
|
-
args: options.args,
|
|
341
|
-
cwd,
|
|
342
|
-
env,
|
|
343
|
-
ownedManagedSession,
|
|
344
|
-
restoreState: managedSessionRestoreState,
|
|
345
|
-
stdin,
|
|
346
|
-
});
|
|
383
|
+
const args = options.args;
|
|
347
384
|
const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
|
|
348
385
|
if (signal?.aborted) {
|
|
349
386
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
350
387
|
}
|
|
388
|
+
const parentEnv = getAgentBrowserProcessEnvironment();
|
|
351
389
|
const managedSessionRestoreOptions = {
|
|
352
390
|
args,
|
|
353
391
|
cwd,
|
|
354
392
|
env,
|
|
355
393
|
ownedManagedSession,
|
|
394
|
+
parentEnv,
|
|
356
395
|
restoreState: managedSessionRestoreState,
|
|
357
396
|
stdin,
|
|
358
397
|
};
|
|
359
|
-
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions,
|
|
398
|
+
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
|
|
360
399
|
if (planningPolicyError) {
|
|
361
400
|
return {
|
|
362
401
|
aborted: false,
|
|
@@ -369,48 +408,33 @@ export async function runAgentBrowserProcess(options) {
|
|
|
369
408
|
};
|
|
370
409
|
}
|
|
371
410
|
const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
372
|
-
const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
373
|
-
const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
|
|
374
|
-
const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
|
|
375
|
-
if (managedSessionRestoreConfigEnv === undefined) {
|
|
376
|
-
return {
|
|
377
|
-
aborted: false,
|
|
378
|
-
agentBrowserStarted: false,
|
|
379
|
-
exitCode: 1,
|
|
380
|
-
spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
|
|
381
|
-
stderr: "",
|
|
382
|
-
stdout: "",
|
|
383
|
-
timedOut: false,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
411
|
const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
|
|
387
412
|
const processOverrides = {
|
|
388
413
|
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
389
414
|
...managedSessionRestoreEnv,
|
|
390
415
|
...env,
|
|
391
|
-
...managedSessionRestoreConfigEnv,
|
|
392
416
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
393
417
|
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
418
|
+
...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
|
|
394
419
|
...ownedManagedSessionCompatibilityEnv,
|
|
395
|
-
AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
|
|
396
|
-
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
397
420
|
};
|
|
398
421
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
399
422
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
400
|
-
|
|
401
|
-
effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
|
|
402
|
-
const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
|
|
423
|
+
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
|
|
403
424
|
if (requestedSocketDir !== undefined) {
|
|
404
|
-
const
|
|
425
|
+
const socketDirError = requestedSocketDir.length > 0
|
|
426
|
+
? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
|
|
427
|
+
: "the configured path is empty";
|
|
405
428
|
if (signal?.aborted) {
|
|
406
429
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
407
430
|
}
|
|
408
|
-
|
|
431
|
+
const socketPathError = socketDirError ? undefined : getAgentBrowserSocketPathValidationError({ args, env: effectiveEnv, socketDir: requestedSocketDir });
|
|
432
|
+
if (socketDirError || socketPathError) {
|
|
409
433
|
return {
|
|
410
434
|
aborted: false,
|
|
411
435
|
agentBrowserStarted: false,
|
|
412
436
|
exitCode: 1,
|
|
413
|
-
spawnError: new Error(
|
|
437
|
+
spawnError: new Error(socketPathError ?? `Agent-browser socket storage ${JSON.stringify(requestedSocketDir)} is unusable: ${socketDirError}. Use an absolute directory owned by the current uid with mode 0700 and remove foreign, symlink, or special entries.`),
|
|
414
438
|
stderr: "",
|
|
415
439
|
stdout: "",
|
|
416
440
|
timedOut: false,
|
|
@@ -432,6 +456,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
432
456
|
let stdoutTail = "";
|
|
433
457
|
let stdoutSpillHandle;
|
|
434
458
|
let stdoutSpillPath;
|
|
459
|
+
let stdoutSpillPending = false;
|
|
435
460
|
let pendingStdoutWrite = Promise.resolve();
|
|
436
461
|
let stdoutSpillError;
|
|
437
462
|
let killTimer;
|
|
@@ -443,11 +468,12 @@ export async function runAgentBrowserProcess(options) {
|
|
|
443
468
|
stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
|
|
444
469
|
if (stdoutSpillError)
|
|
445
470
|
return;
|
|
446
|
-
if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
471
|
+
if (!stdoutSpillPending && !stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
447
472
|
stdoutBuffers.push(buffer);
|
|
448
473
|
stdoutBufferedBytes += buffer.length;
|
|
449
474
|
return;
|
|
450
475
|
}
|
|
476
|
+
stdoutSpillPending = true;
|
|
451
477
|
pendingStdoutWrite = pendingStdoutWrite
|
|
452
478
|
.then(async () => {
|
|
453
479
|
if (stdoutSpillError)
|
|
@@ -521,13 +547,13 @@ export async function runAgentBrowserProcess(options) {
|
|
|
521
547
|
});
|
|
522
548
|
});
|
|
523
549
|
};
|
|
524
|
-
const childEnv = buildAgentBrowserProcessEnv(
|
|
525
|
-
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions,
|
|
550
|
+
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
551
|
+
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
|
|
526
552
|
if (spawnPolicyError) {
|
|
527
553
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
528
554
|
return;
|
|
529
555
|
}
|
|
530
|
-
const spawnCommand = buildAgentBrowserSpawnCommand(
|
|
556
|
+
const spawnCommand = buildAgentBrowserSpawnCommand(prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
|
|
531
557
|
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
532
558
|
cwd,
|
|
533
559
|
env: childEnv,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
2
|
+
import { isRecord } from "./parsing.js";
|
|
3
|
+
import { isPendingRecordingArtifact } from "./results/artifact-manifest.js";
|
|
4
|
+
export const RECORDING_RESERVATION_ENTRY_TYPE = "agent-browser-recording-reservation";
|
|
5
|
+
function getReservationKey(reservation) {
|
|
6
|
+
return getAgentBrowserSessionIdentityKey(reservation.sessionName, reservation.namespace);
|
|
7
|
+
}
|
|
8
|
+
function getArtifactReservation(artifact) {
|
|
9
|
+
if (!artifact.session || artifact.command !== "record" || artifact.kind !== "video")
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
absolutePath: artifact.absolutePath,
|
|
13
|
+
cwd: artifact.cwd ?? process.cwd(),
|
|
14
|
+
namespace: artifact.namespace,
|
|
15
|
+
path: artifact.path,
|
|
16
|
+
sessionName: artifact.session,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function applyRecordingArtifactsToReservations(reservations, artifacts) {
|
|
20
|
+
const pendingBySession = new Map();
|
|
21
|
+
const terminalBySession = new Map();
|
|
22
|
+
for (const artifact of artifacts) {
|
|
23
|
+
const reservation = getArtifactReservation(artifact);
|
|
24
|
+
if (!reservation)
|
|
25
|
+
continue;
|
|
26
|
+
const key = getReservationKey(reservation);
|
|
27
|
+
if (isPendingRecordingArtifact(artifact))
|
|
28
|
+
pendingBySession.set(key, reservation);
|
|
29
|
+
else
|
|
30
|
+
terminalBySession.set(key, reservation);
|
|
31
|
+
}
|
|
32
|
+
const transitions = [];
|
|
33
|
+
for (const key of terminalBySession.keys()) {
|
|
34
|
+
if (pendingBySession.has(key))
|
|
35
|
+
continue;
|
|
36
|
+
const existing = reservations.get(key);
|
|
37
|
+
if (!existing)
|
|
38
|
+
continue;
|
|
39
|
+
reservations.delete(key);
|
|
40
|
+
transitions.push({ reservation: existing, state: "closed" });
|
|
41
|
+
}
|
|
42
|
+
for (const [key, pending] of pendingBySession) {
|
|
43
|
+
const existing = reservations.get(key);
|
|
44
|
+
reservations.set(key, pending);
|
|
45
|
+
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd) {
|
|
46
|
+
transitions.push({ reservation: pending, state: "active" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return transitions;
|
|
50
|
+
}
|
|
51
|
+
export function retireRecordingReservation(reservations, sessionName, namespace) {
|
|
52
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
53
|
+
const reservation = reservations.get(key);
|
|
54
|
+
if (reservation)
|
|
55
|
+
reservations.delete(key);
|
|
56
|
+
return reservation;
|
|
57
|
+
}
|
|
58
|
+
export function appendRecordingReservationTransition(pi, transition) {
|
|
59
|
+
const { reservation, state } = transition;
|
|
60
|
+
pi.appendEntry(RECORDING_RESERVATION_ENTRY_TYPE, {
|
|
61
|
+
absolutePath: state === "active" ? reservation.absolutePath : undefined,
|
|
62
|
+
cwd: state === "active" ? reservation.cwd : undefined,
|
|
63
|
+
namespace: reservation.namespace,
|
|
64
|
+
path: state === "active" ? reservation.path : undefined,
|
|
65
|
+
sessionName: reservation.sessionName,
|
|
66
|
+
state,
|
|
67
|
+
version: 1,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseReservationTransition(data) {
|
|
71
|
+
if (!isRecord(data) || data.version !== 1 || (data.state !== "active" && data.state !== "closed"))
|
|
72
|
+
return undefined;
|
|
73
|
+
if (typeof data.sessionName !== "string" || data.sessionName.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (data.namespace !== undefined && typeof data.namespace !== "string")
|
|
76
|
+
return undefined;
|
|
77
|
+
if (data.state === "closed") {
|
|
78
|
+
return {
|
|
79
|
+
reservation: { absolutePath: "", cwd: "", namespace: data.namespace, path: "", sessionName: data.sessionName },
|
|
80
|
+
state: "closed",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (typeof data.absolutePath !== "string" || typeof data.cwd !== "string" || typeof data.path !== "string")
|
|
84
|
+
return undefined;
|
|
85
|
+
return {
|
|
86
|
+
reservation: {
|
|
87
|
+
absolutePath: data.absolutePath,
|
|
88
|
+
cwd: data.cwd,
|
|
89
|
+
namespace: data.namespace,
|
|
90
|
+
path: data.path,
|
|
91
|
+
sessionName: data.sessionName,
|
|
92
|
+
},
|
|
93
|
+
state: "active",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function restoreRecordingReservationStateFromBranch(branch) {
|
|
97
|
+
const reservations = new Map();
|
|
98
|
+
const terminal = new Map();
|
|
99
|
+
for (const entry of branch) {
|
|
100
|
+
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== RECORDING_RESERVATION_ENTRY_TYPE)
|
|
101
|
+
continue;
|
|
102
|
+
const transition = parseReservationTransition(entry.data);
|
|
103
|
+
if (!transition)
|
|
104
|
+
continue;
|
|
105
|
+
const key = getReservationKey(transition.reservation);
|
|
106
|
+
if (transition.state === "active") {
|
|
107
|
+
reservations.set(key, transition.reservation);
|
|
108
|
+
terminal.delete(key);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reservations.delete(key);
|
|
112
|
+
terminal.set(key, transition.reservation);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { active: reservations, terminal };
|
|
116
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isOpenNavigationCommand, isPageMutationCommand } from "../command-taxonomy.js";
|
|
2
2
|
import { isPendingRecordingArtifact } from "./artifact-manifest.js";
|
|
3
|
-
import { buildNextToolAction } from "./next-actions.js";
|
|
3
|
+
import { applySessionToNextActions, buildNextToolAction } from "./next-actions.js";
|
|
4
4
|
import { AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS, buildRecoveryNextActions, } from "./recovery-actions.js";
|
|
5
5
|
function buildArtifactAction(path) {
|
|
6
6
|
return {
|
|
@@ -106,7 +106,7 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
106
106
|
reason: "Inspect the opened page before choosing interactive refs.",
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
-
else if (isPageMutationCommand(options.command)) {
|
|
109
|
+
else if (isPageMutationCommand(options.command, options.subcommand)) {
|
|
110
110
|
actions.push(buildNextToolAction({
|
|
111
111
|
args: ["snapshot", "-i"],
|
|
112
112
|
id: "inspect-after-mutation",
|
|
@@ -120,9 +120,8 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
120
120
|
actions.push(buildArtifactAction(options.savedFilePath));
|
|
121
121
|
}
|
|
122
122
|
for (const artifact of artifacts) {
|
|
123
|
-
if (isPendingRecordingArtifact(artifact))
|
|
123
|
+
if (isPendingRecordingArtifact(artifact))
|
|
124
124
|
continue;
|
|
125
|
-
}
|
|
126
125
|
if (artifact.exists === false) {
|
|
127
126
|
if (artifact.kind === "download") {
|
|
128
127
|
actions.push(buildNextToolAction({
|
|
@@ -146,7 +145,7 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
146
145
|
switch (options.failureCategory) {
|
|
147
146
|
case "artifact-missing":
|
|
148
147
|
for (const artifact of options.artifacts ?? []) {
|
|
149
|
-
if (isPendingRecordingArtifact(artifact) || artifact.exists !== false)
|
|
148
|
+
if (isPendingRecordingArtifact(artifact) || (artifact.exists !== false && artifact.status !== "stale"))
|
|
150
149
|
continue;
|
|
151
150
|
if (artifact.kind === "download") {
|
|
152
151
|
actions.push(buildNextToolAction({
|
|
@@ -196,6 +195,56 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
196
195
|
}));
|
|
197
196
|
}
|
|
198
197
|
break;
|
|
198
|
+
case "timeout":
|
|
199
|
+
{
|
|
200
|
+
const textAssertion = options.command === "wait" && options.args?.includes("--text") === true;
|
|
201
|
+
const urlAssertion = options.command === "wait" && options.args?.includes("--url") === true;
|
|
202
|
+
actions.push(buildNextToolAction({
|
|
203
|
+
args: ["snapshot", "-i"],
|
|
204
|
+
id: textAssertion ? "inspect-after-text-assertion-failure" : "inspect-after-timeout",
|
|
205
|
+
reason: textAssertion
|
|
206
|
+
? "Inspect the current page after the text assertion failed before concluding the expected text is absent."
|
|
207
|
+
: options.command === "wait"
|
|
208
|
+
? "Inspect the current page after the wait condition timed out before retrying with a different selector or timeout."
|
|
209
|
+
: "Inspect the current page after the timed-out browser operation.",
|
|
210
|
+
safety: textAssertion
|
|
211
|
+
? "Read-only snapshot; use current refs or visible text from this page before retrying the assertion."
|
|
212
|
+
: "Read-only snapshot; do not assume the timed-out interaction completed.",
|
|
213
|
+
}));
|
|
214
|
+
if (urlAssertion) {
|
|
215
|
+
actions.push(buildNextToolAction({
|
|
216
|
+
args: ["open", "about:blank"],
|
|
217
|
+
id: "fresh-session-after-url-wait-timeout",
|
|
218
|
+
reason: "If a preceding click or form submit reported success but the page never navigated, upstream click dispatch may have silently missed (observed with agent-browser 0.34 after many spaced commands); replace about:blank with the target URL and replay the flow as one batch in a fresh session instead of retrying the wait.",
|
|
219
|
+
safety: "Abandons the current browser session; capture page evidence with the inspect action first, and only abandon when the wait target is not simply wrong or slow.",
|
|
220
|
+
sessionMode: "fresh",
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
case "upstream-error":
|
|
226
|
+
if (isOpenNavigationCommand(options.command)) {
|
|
227
|
+
actions.push(buildNextToolAction({
|
|
228
|
+
args: ["get", "url"],
|
|
229
|
+
id: "inspect-page-after-navigation-error",
|
|
230
|
+
reason: "Check which page, if any, remains active after the navigation or network error.",
|
|
231
|
+
safety: "Read-only URL inspection; verify connectivity and the target URL before retrying navigation.",
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
case "tab-gone":
|
|
236
|
+
actions.push(buildNextToolAction({
|
|
237
|
+
args: ["tab", "list"],
|
|
238
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.tabGoneListTabs,
|
|
239
|
+
reason: "The pinned bound tab is gone; inspect remaining tabs before acting on a neighbor.",
|
|
240
|
+
safety: "Read-only. Prefer a listed tab id, label, or CDP targetId, or open a new tab to rebind.",
|
|
241
|
+
}), buildNextToolAction({
|
|
242
|
+
args: ["tab", "new"],
|
|
243
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.tabGoneNewTab,
|
|
244
|
+
reason: "Bind a fresh tab after tab_gone instead of continuing on another session's page.",
|
|
245
|
+
safety: "Opens a new tab in this session and rebinds the pin; pass a URL if you know the intended page.",
|
|
246
|
+
}));
|
|
247
|
+
break;
|
|
199
248
|
case "tab-drift":
|
|
200
249
|
if (options.recovery?.kind === "about-blank" || options.recovery?.kind === "tab-drift") {
|
|
201
250
|
break;
|
|
@@ -209,5 +258,13 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
209
258
|
break;
|
|
210
259
|
}
|
|
211
260
|
}
|
|
212
|
-
|
|
261
|
+
if ((options.artifacts ?? []).some(isPendingRecordingArtifact)) {
|
|
262
|
+
actions.push(buildNextToolAction({
|
|
263
|
+
args: ["record", "stop"],
|
|
264
|
+
id: "stop-pending-recording",
|
|
265
|
+
reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
|
|
266
|
+
safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
return applySessionToNextActions(actions.length > 0 ? actions : undefined, options.sessionName);
|
|
213
270
|
}
|