pi-agent-browser-native 0.3.0 → 0.5.0
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 +127 -0
- package/README.md +63 -20
- package/dist/extensions/agent-browser/index.js +787 -105
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
- 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 +8 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
- 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 +26 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
- 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 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +18 -15
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
- package/dist/extensions/agent-browser/lib/process.js +131 -41
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
- 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 +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/runtime.js +116 -39
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +40 -21
- package/docs/COMMAND_REFERENCE.md +90 -35
- package/docs/RELEASE.md +3 -3
- package/docs/REQUIREMENTS.md +4 -2
- package/docs/SUPPORT_MATRIX.md +26 -19
- package/docs/TOOL_CONTRACT.md +93 -52
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +7 -6
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
|
@@ -6,10 +6,11 @@ import { env as processEnv, platform as processPlatform } from "node:process";
|
|
|
6
6
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
7
7
|
import { needsManagedSession } from "./command-policy.js";
|
|
8
8
|
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 { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
11
|
-
import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "./managed-session-state-policy.js";
|
|
9
|
+
import { extractExplicitSessionName, getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, resolveAgentBrowserNamespace, } from "./argv-grammar.js";
|
|
10
|
+
import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getOwnedManagedSessionRestoreKey, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
11
|
+
import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, invocationMayNavigateToLocalFile, } from "./managed-session-state-policy.js";
|
|
12
12
|
import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
|
|
13
|
+
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
13
14
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
14
15
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
16
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
@@ -20,16 +21,29 @@ const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
|
|
|
20
21
|
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
21
22
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
22
23
|
const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
|
|
24
|
+
const PI_AGENT_BROWSER_SOCKET_DIR_ENV = "PI_AGENT_BROWSER_SOCKET_DIR";
|
|
23
25
|
const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
|
|
26
|
+
const TERMUX_PACKAGE_NAME_PATTERN = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
|
|
24
27
|
export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
|
|
25
28
|
const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
26
29
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
27
30
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
28
31
|
const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
|
|
32
|
+
const UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE = "Local file navigation requires a wrapper-managed local browser because caller-owned or attached sessions can retain unsafe file-access launch flags. Omit the explicit session or attachment and retry with sessionMode fresh.";
|
|
29
33
|
const attachedBrowserSessionContext = new AsyncLocalStorage();
|
|
34
|
+
const WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND = new Set([
|
|
35
|
+
"auth", "clipboard", "cookies", "dashboard", "device", "dialog", "diff", "find", "get", "is", "keyboard",
|
|
36
|
+
"mouse", "network", "plugin", "profiler", "react", "record", "session", "set", "skills", "state", "storage",
|
|
37
|
+
"stream", "tab", "trace", "window",
|
|
38
|
+
]);
|
|
30
39
|
export function withAttachedBrowserSessionContext(preserve, run) {
|
|
31
40
|
return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
|
|
32
41
|
}
|
|
42
|
+
export function getWindowsExplicitDefaultNamespaceEnv(args, parentNamespace, platform = processPlatform) {
|
|
43
|
+
return platform === "win32" && resolveAgentBrowserNamespace(args, parentNamespace) === ""
|
|
44
|
+
? { AGENT_BROWSER_NAMESPACE: "" }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
33
47
|
function appendTail(text, addition, maxChars) {
|
|
34
48
|
const combined = text + addition;
|
|
35
49
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
@@ -43,7 +57,12 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
43
57
|
for (let index = 0; index < args.length; index += 1) {
|
|
44
58
|
const token = args[index];
|
|
45
59
|
if (isKnownCommandToken(token)) {
|
|
46
|
-
|
|
60
|
+
if (index === 0)
|
|
61
|
+
return args;
|
|
62
|
+
const firstPositional = args[index + 1];
|
|
63
|
+
return WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND.has(token) && firstPositional && !firstPositional.startsWith("-")
|
|
64
|
+
? [token, firstPositional, ...leadingGlobals, ...args.slice(index + 2)]
|
|
65
|
+
: [token, ...leadingGlobals, ...args.slice(index + 1)];
|
|
47
66
|
}
|
|
48
67
|
if (!token.startsWith("-"))
|
|
49
68
|
return args;
|
|
@@ -77,6 +96,13 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
77
96
|
const value = args[index + 1];
|
|
78
97
|
if (value === undefined)
|
|
79
98
|
return args;
|
|
99
|
+
// PowerShell -> .cmd drops empty argv values. Planning rejects empty
|
|
100
|
+
// caller --args; keep this defensive skip so an unexpected empty value
|
|
101
|
+
// cannot turn the next flag into its accidental value on native Windows.
|
|
102
|
+
if (value === "" && (flag === "--args" || flag === "--namespace")) {
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
80
106
|
leadingGlobals.push(token, value);
|
|
81
107
|
index += 1;
|
|
82
108
|
continue;
|
|
@@ -103,9 +129,9 @@ export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUser
|
|
|
103
129
|
return filtered;
|
|
104
130
|
// Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
|
|
105
131
|
const browserArgs = wrapperCompatibilityUserAgent
|
|
106
|
-
? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
|
|
107
|
-
:
|
|
108
|
-
return [
|
|
132
|
+
? ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`]
|
|
133
|
+
: [];
|
|
134
|
+
return [...browserArgs, "--allow-file-access", "false", ...filtered];
|
|
109
135
|
}
|
|
110
136
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
111
137
|
if (platform !== "win32") {
|
|
@@ -216,33 +242,43 @@ function clampUpstreamDefaultTimeout(childEnv) {
|
|
|
216
242
|
export function getAgentBrowserProcessTimeoutMs(env = processEnv) {
|
|
217
243
|
return parsePositiveIntegerEnv(env[PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV]) ?? DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS;
|
|
218
244
|
}
|
|
219
|
-
export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
245
|
+
export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined, termuxPackageName = processEnv.TERMUX_APP__PACKAGE_NAME) {
|
|
220
246
|
if (platform === "win32") {
|
|
221
247
|
return undefined;
|
|
222
248
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
249
|
+
const termuxAppRoot = platform === "android" && termuxPackageName && TERMUX_PACKAGE_NAME_PATTERN.test(termuxPackageName);
|
|
250
|
+
const prefix = platform === "darwin"
|
|
251
|
+
? "/private/tmp/piab"
|
|
252
|
+
: termuxAppRoot
|
|
253
|
+
? `/data/data/${termuxPackageName}/piab`
|
|
254
|
+
: DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
|
|
255
|
+
return `${prefix}${!termuxAppRoot && typeof uid === "number" ? `-${uid}` : ""}`;
|
|
256
|
+
}
|
|
257
|
+
export function isTrustedAndroidAppDataRoot(path, metadata, uid, platform = processPlatform) {
|
|
258
|
+
if (platform !== "android" || metadata.uid !== uid || metadata.isSymbolicLink() || !metadata.isDirectory() || (metadata.mode & 0o777) !== 0o700)
|
|
259
|
+
return false;
|
|
260
|
+
const parent = dirname(path);
|
|
261
|
+
return parent === "/data/data" || /^\/data\/user\/\d+$/.test(parent);
|
|
262
|
+
}
|
|
263
|
+
export function isTrustedSocketDirAncestor(metadata, uid, platform = processPlatform) {
|
|
264
|
+
if (metadata.isSymbolicLink())
|
|
265
|
+
return metadata.uid === 0;
|
|
266
|
+
if (!metadata.isDirectory())
|
|
267
|
+
return false;
|
|
268
|
+
const mode = metadata.mode & 0o7777;
|
|
269
|
+
if (platform === "android" && uid !== 0 && metadata.uid === uid && metadata.gid === uid)
|
|
270
|
+
return (mode & 0o002) === 0;
|
|
271
|
+
if (metadata.uid === uid && uid !== 0)
|
|
272
|
+
return (mode & 0o022) === 0;
|
|
273
|
+
return metadata.uid === 0 && ((mode & 0o022) === 0 || (mode & 0o1000) !== 0);
|
|
225
274
|
}
|
|
226
275
|
async function hasTrustedSocketDirAncestry(socketDir, uid) {
|
|
227
276
|
for (let current = dirname(socketDir);;) {
|
|
228
277
|
const metadata = await lstat(current);
|
|
229
|
-
if (metadata
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
else if (!metadata.isDirectory()) {
|
|
278
|
+
if (isTrustedAndroidAppDataRoot(current, metadata, uid))
|
|
279
|
+
return true;
|
|
280
|
+
if (!isTrustedSocketDirAncestor(metadata, uid))
|
|
234
281
|
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
282
|
const parent = dirname(current);
|
|
247
283
|
if (parent === current)
|
|
248
284
|
return true;
|
|
@@ -273,28 +309,64 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
|
|
|
273
309
|
}
|
|
274
310
|
return true;
|
|
275
311
|
}
|
|
276
|
-
export async function
|
|
277
|
-
if (!isAbsolute(socketDir)
|
|
278
|
-
return
|
|
312
|
+
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
313
|
+
if (!isAbsolute(socketDir))
|
|
314
|
+
return "the path is not absolute";
|
|
315
|
+
if (typeof uid !== "number")
|
|
316
|
+
return "POSIX ownership metadata is unavailable";
|
|
279
317
|
try {
|
|
280
318
|
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
281
|
-
return
|
|
319
|
+
return "an ancestor is writable, foreign-owned, a non-directory, or an untrusted symlink";
|
|
282
320
|
try {
|
|
283
321
|
await mkdir(socketDir, { mode: 0o700 });
|
|
284
322
|
}
|
|
285
323
|
catch (error) {
|
|
286
324
|
if (error.code !== "EEXIST")
|
|
287
|
-
return
|
|
325
|
+
return `the directory could not be created (${error.code ?? "unknown error"})`;
|
|
288
326
|
}
|
|
289
327
|
const metadata = await lstat(socketDir);
|
|
290
|
-
if (!metadata.isDirectory()
|
|
291
|
-
return
|
|
292
|
-
|
|
328
|
+
if (!metadata.isDirectory())
|
|
329
|
+
return "the path is not a directory";
|
|
330
|
+
if (metadata.isSymbolicLink())
|
|
331
|
+
return "the directory is a symlink";
|
|
332
|
+
if (metadata.uid !== uid)
|
|
333
|
+
return `the directory is owned by uid ${metadata.uid}, not uid ${uid}`;
|
|
334
|
+
if ((metadata.mode & 0o777) !== 0o700)
|
|
335
|
+
return `the directory mode is ${(metadata.mode & 0o777).toString(8)}, not 700`;
|
|
336
|
+
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
337
|
+
return "an ancestor became untrusted during validation";
|
|
338
|
+
if (!await socketDirEntriesAreOwned(socketDir, uid))
|
|
339
|
+
return "the directory contains a foreign-owned, symlink, special, or excessively deep entry";
|
|
340
|
+
return undefined;
|
|
293
341
|
}
|
|
294
|
-
catch {
|
|
295
|
-
return
|
|
342
|
+
catch (error) {
|
|
343
|
+
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
296
344
|
}
|
|
297
345
|
}
|
|
346
|
+
export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
347
|
+
return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
|
|
348
|
+
}
|
|
349
|
+
export function getAgentBrowserSocketPathValidationError(options) {
|
|
350
|
+
if ((options.platform ?? processPlatform) === "win32")
|
|
351
|
+
return undefined;
|
|
352
|
+
const descriptor = parseArgvDescriptor(options.args);
|
|
353
|
+
const { command } = descriptor.commandInfo;
|
|
354
|
+
// Preflight commands that can start or navigate a browser. Follow-up reads and
|
|
355
|
+
// cleanup may target a daemon created by an earlier wrapper version, so let
|
|
356
|
+
// upstream inspect those identities instead of rejecting them from path math.
|
|
357
|
+
if (!command || !["batch", "connect", "goto", "navigate", "open", "visit"].includes(command))
|
|
358
|
+
return undefined;
|
|
359
|
+
const sessionName = extractExplicitSessionName(options.args);
|
|
360
|
+
if (!sessionName)
|
|
361
|
+
return undefined;
|
|
362
|
+
const namespace = resolveAgentBrowserNamespace(options.args, options.env?.AGENT_BROWSER_NAMESPACE);
|
|
363
|
+
const socketRoot = namespace ? join(options.socketDir, "namespaces", namespace, "run") : options.socketDir;
|
|
364
|
+
const socketPath = join(socketRoot, `${sessionName}.sock`);
|
|
365
|
+
const pathBytes = Buffer.byteLength(socketPath);
|
|
366
|
+
if (pathBytes <= 103)
|
|
367
|
+
return undefined;
|
|
368
|
+
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.`;
|
|
369
|
+
}
|
|
298
370
|
export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
|
|
299
371
|
const childEnv = {};
|
|
300
372
|
for (const [name, value] of Object.entries(baseEnv)) {
|
|
@@ -325,6 +397,7 @@ function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessio
|
|
|
325
397
|
currentPageUrl,
|
|
326
398
|
cwd: options.cwd,
|
|
327
399
|
env: effectiveEnv ?? options.env,
|
|
400
|
+
managedSessionRestoreKey: getOwnedManagedSessionRestoreKey(),
|
|
328
401
|
pageUrlUnknown,
|
|
329
402
|
parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
|
|
330
403
|
stdin: options.stdin,
|
|
@@ -348,11 +421,24 @@ export async function runAgentBrowserProcess(options) {
|
|
|
348
421
|
if (signal?.aborted) {
|
|
349
422
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
350
423
|
}
|
|
424
|
+
if (invocationMayNavigateToLocalFile(args, stdin) && (!ownedManagedSession || preserveAttachedBrowserSession)) {
|
|
425
|
+
return {
|
|
426
|
+
aborted: false,
|
|
427
|
+
agentBrowserStarted: false,
|
|
428
|
+
exitCode: 1,
|
|
429
|
+
spawnError: new Error(UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE),
|
|
430
|
+
stderr: "",
|
|
431
|
+
stdout: "",
|
|
432
|
+
timedOut: false,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const parentEnv = getAgentBrowserProcessEnvironment();
|
|
351
436
|
const managedSessionRestoreOptions = {
|
|
352
437
|
args,
|
|
353
438
|
cwd,
|
|
354
439
|
env,
|
|
355
440
|
ownedManagedSession,
|
|
441
|
+
parentEnv,
|
|
356
442
|
restoreState: managedSessionRestoreState,
|
|
357
443
|
stdin,
|
|
358
444
|
};
|
|
@@ -391,6 +477,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
391
477
|
...managedSessionRestoreConfigEnv,
|
|
392
478
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
393
479
|
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
480
|
+
...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
|
|
394
481
|
...ownedManagedSessionCompatibilityEnv,
|
|
395
482
|
AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
|
|
396
483
|
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
@@ -399,18 +486,21 @@ export async function runAgentBrowserProcess(options) {
|
|
|
399
486
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
400
487
|
if (ownedManagedSessionClose)
|
|
401
488
|
effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
|
|
402
|
-
const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
|
|
489
|
+
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
|
|
403
490
|
if (requestedSocketDir !== undefined) {
|
|
404
|
-
const
|
|
491
|
+
const socketDirError = requestedSocketDir.length > 0
|
|
492
|
+
? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
|
|
493
|
+
: "the configured path is empty";
|
|
405
494
|
if (signal?.aborted) {
|
|
406
495
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
407
496
|
}
|
|
408
|
-
|
|
497
|
+
const socketPathError = socketDirError ? undefined : getAgentBrowserSocketPathValidationError({ args, env: effectiveEnv, socketDir: requestedSocketDir });
|
|
498
|
+
if (socketDirError || socketPathError) {
|
|
409
499
|
return {
|
|
410
500
|
aborted: false,
|
|
411
501
|
agentBrowserStarted: false,
|
|
412
502
|
exitCode: 1,
|
|
413
|
-
spawnError: new Error(
|
|
503
|
+
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
504
|
stderr: "",
|
|
415
505
|
stdout: "",
|
|
416
506
|
timedOut: false,
|
|
@@ -521,7 +611,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
521
611
|
});
|
|
522
612
|
});
|
|
523
613
|
};
|
|
524
|
-
const childEnv = buildAgentBrowserProcessEnv(
|
|
614
|
+
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
525
615
|
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
|
|
526
616
|
if (spawnPolicyError) {
|
|
527
617
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
|
|
3
|
+
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
|
|
4
|
+
import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
|
|
5
|
+
import { isRecord } from "./parsing.js";
|
|
6
|
+
import { isPendingRecordingArtifact, isPendingRecordingCommand, isSessionArtifactManifest } from "./results/artifact-manifest.js";
|
|
7
|
+
export const RECORDING_RESERVATION_ENTRY_TYPE = "agent-browser-recording-reservation";
|
|
8
|
+
function getReservationKey(reservation) {
|
|
9
|
+
return getAgentBrowserSessionIdentityKey(reservation.sessionName, reservation.namespace);
|
|
10
|
+
}
|
|
11
|
+
function getArtifactReservation(artifact) {
|
|
12
|
+
if (!artifact.session || artifact.command !== "record" || artifact.kind !== "video")
|
|
13
|
+
return undefined;
|
|
14
|
+
return {
|
|
15
|
+
absolutePath: artifact.absolutePath,
|
|
16
|
+
cwd: artifact.cwd ?? process.cwd(),
|
|
17
|
+
namespace: artifact.namespace,
|
|
18
|
+
path: artifact.path,
|
|
19
|
+
sessionName: artifact.session,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function applyRecordingArtifactsToReservations(reservations, artifacts) {
|
|
23
|
+
const pendingBySession = new Map();
|
|
24
|
+
const terminalBySession = new Map();
|
|
25
|
+
for (const artifact of artifacts) {
|
|
26
|
+
const reservation = getArtifactReservation(artifact);
|
|
27
|
+
if (!reservation)
|
|
28
|
+
continue;
|
|
29
|
+
const key = getReservationKey(reservation);
|
|
30
|
+
if (isPendingRecordingArtifact(artifact))
|
|
31
|
+
pendingBySession.set(key, reservation);
|
|
32
|
+
else
|
|
33
|
+
terminalBySession.set(key, reservation);
|
|
34
|
+
}
|
|
35
|
+
const transitions = [];
|
|
36
|
+
for (const key of terminalBySession.keys()) {
|
|
37
|
+
if (pendingBySession.has(key))
|
|
38
|
+
continue;
|
|
39
|
+
const existing = reservations.get(key);
|
|
40
|
+
if (!existing)
|
|
41
|
+
continue;
|
|
42
|
+
reservations.delete(key);
|
|
43
|
+
transitions.push({ reservation: existing, state: "closed" });
|
|
44
|
+
}
|
|
45
|
+
for (const [key, pending] of pendingBySession) {
|
|
46
|
+
const existing = reservations.get(key);
|
|
47
|
+
reservations.set(key, pending);
|
|
48
|
+
if (!existing || existing.absolutePath !== pending.absolutePath || existing.cwd !== pending.cwd) {
|
|
49
|
+
transitions.push({ reservation: pending, state: "active" });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return transitions;
|
|
53
|
+
}
|
|
54
|
+
export function retireRecordingReservation(reservations, sessionName, namespace) {
|
|
55
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
56
|
+
const reservation = reservations.get(key);
|
|
57
|
+
if (reservation)
|
|
58
|
+
reservations.delete(key);
|
|
59
|
+
return reservation;
|
|
60
|
+
}
|
|
61
|
+
export function appendRecordingReservationTransition(pi, transition) {
|
|
62
|
+
const { reservation, state } = transition;
|
|
63
|
+
pi.appendEntry(RECORDING_RESERVATION_ENTRY_TYPE, {
|
|
64
|
+
absolutePath: state === "active" ? reservation.absolutePath : undefined,
|
|
65
|
+
cwd: state === "active" ? reservation.cwd : undefined,
|
|
66
|
+
namespace: reservation.namespace,
|
|
67
|
+
path: state === "active" ? reservation.path : undefined,
|
|
68
|
+
sessionName: reservation.sessionName,
|
|
69
|
+
state,
|
|
70
|
+
version: 1,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function parseReservationTransition(data) {
|
|
74
|
+
if (!isRecord(data) || data.version !== 1 || (data.state !== "active" && data.state !== "closed"))
|
|
75
|
+
return undefined;
|
|
76
|
+
if (typeof data.sessionName !== "string" || data.sessionName.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (data.namespace !== undefined && typeof data.namespace !== "string")
|
|
79
|
+
return undefined;
|
|
80
|
+
if (data.state === "closed") {
|
|
81
|
+
return {
|
|
82
|
+
reservation: { absolutePath: "", cwd: "", namespace: data.namespace, path: "", sessionName: data.sessionName },
|
|
83
|
+
state: "closed",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof data.absolutePath !== "string" || typeof data.cwd !== "string" || typeof data.path !== "string")
|
|
87
|
+
return undefined;
|
|
88
|
+
return {
|
|
89
|
+
reservation: {
|
|
90
|
+
absolutePath: data.absolutePath,
|
|
91
|
+
cwd: data.cwd,
|
|
92
|
+
namespace: data.namespace,
|
|
93
|
+
path: data.path,
|
|
94
|
+
sessionName: data.sessionName,
|
|
95
|
+
},
|
|
96
|
+
state: "active",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function restoreRecordingReservationStateFromBranch(branch) {
|
|
100
|
+
const reservations = new Map();
|
|
101
|
+
const terminal = new Map();
|
|
102
|
+
for (const entry of branch) {
|
|
103
|
+
if (!isRecord(entry))
|
|
104
|
+
continue;
|
|
105
|
+
if (entry.type === "custom" && entry.customType === RECORDING_RESERVATION_ENTRY_TYPE) {
|
|
106
|
+
const transition = parseReservationTransition(entry.data);
|
|
107
|
+
if (!transition)
|
|
108
|
+
continue;
|
|
109
|
+
const key = getReservationKey(transition.reservation);
|
|
110
|
+
if (transition.state === "active") {
|
|
111
|
+
reservations.set(key, transition.reservation);
|
|
112
|
+
terminal.delete(key);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
reservations.delete(key);
|
|
116
|
+
terminal.set(key, transition.reservation);
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (entry.type !== "message")
|
|
121
|
+
continue;
|
|
122
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
123
|
+
const details = message?.toolName === "agent_browser" && isRecord(message.details) ? message.details : undefined;
|
|
124
|
+
if (!details)
|
|
125
|
+
continue;
|
|
126
|
+
if (isSessionArtifactManifest(details.artifactManifest)) {
|
|
127
|
+
const artifacts = [...details.artifactManifest.entries]
|
|
128
|
+
.sort((left, right) => left.createdAtMs - right.createdAtMs
|
|
129
|
+
|| Number(isPendingRecordingCommand(left.command, left.subcommand, left.kind)) - Number(isPendingRecordingCommand(right.command, right.subcommand, right.kind))
|
|
130
|
+
|| left.path.localeCompare(right.path))
|
|
131
|
+
.filter((manifestEntry) => manifestEntry.command === "record" && manifestEntry.kind === "video" && manifestEntry.session)
|
|
132
|
+
.map((manifestEntry) => ({
|
|
133
|
+
absolutePath: manifestEntry.absolutePath ?? manifestEntry.path,
|
|
134
|
+
command: manifestEntry.command,
|
|
135
|
+
cwd: manifestEntry.cwd,
|
|
136
|
+
kind: "video",
|
|
137
|
+
namespace: manifestEntry.namespace,
|
|
138
|
+
path: manifestEntry.path,
|
|
139
|
+
session: manifestEntry.session,
|
|
140
|
+
status: isPendingRecordingCommand(manifestEntry.command, manifestEntry.subcommand, "video") ? "pending" : "saved",
|
|
141
|
+
subcommand: manifestEntry.subcommand,
|
|
142
|
+
}));
|
|
143
|
+
for (const artifact of artifacts) {
|
|
144
|
+
const reservation = getArtifactReservation(artifact);
|
|
145
|
+
if (!reservation)
|
|
146
|
+
continue;
|
|
147
|
+
const key = getReservationKey(reservation);
|
|
148
|
+
applyRecordingArtifactsToReservations(reservations, [artifact]);
|
|
149
|
+
if (isPendingRecordingArtifact(artifact))
|
|
150
|
+
terminal.delete(key);
|
|
151
|
+
else
|
|
152
|
+
terminal.set(key, reservation);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const messageSucceeded = typeof message?.isError === "boolean"
|
|
156
|
+
? !message.isError
|
|
157
|
+
: typeof details.exitCode !== "number" || details.exitCode === 0;
|
|
158
|
+
const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
|
|
159
|
+
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
160
|
+
const closeAllApplied = details.closeAllApplied === true
|
|
161
|
+
|| (messageSucceeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
|
|
162
|
+
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
163
|
+
if (closeAllApplied) {
|
|
164
|
+
for (const [key, reservation] of reservations) {
|
|
165
|
+
if (!isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
166
|
+
continue;
|
|
167
|
+
terminal.set(key, reservation);
|
|
168
|
+
reservations.delete(key);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const directCloseSucceeded = messageSucceeded && isCloseCommand(typeof details.command === "string" ? details.command : undefined);
|
|
173
|
+
const batchRecordingClosed = getSuccessfulBatchCloseLifecycle(details.batchSteps)?.recordingClosedAfterBatch === true;
|
|
174
|
+
if ((directCloseSucceeded || batchRecordingClosed) && typeof details.sessionName === "string") {
|
|
175
|
+
const key = getAgentBrowserSessionIdentityKey(details.sessionName, namespace);
|
|
176
|
+
const reservation = reservations.get(key);
|
|
177
|
+
if (reservation)
|
|
178
|
+
terminal.set(key, reservation);
|
|
179
|
+
reservations.delete(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { active: reservations, terminal };
|
|
183
|
+
}
|
|
@@ -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 {
|
|
@@ -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
|
}
|