pi-agent-browser-native 0.2.77 → 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 +154 -0
- package/README.md +69 -35
- package/dist/extensions/agent-browser/index.js +911 -121
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
- package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
- package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
- package/dist/extensions/agent-browser/lib/config.js +0 -6
- package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
- package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
- 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 +27 -26
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
- 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 +22 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/parsing.js +0 -7
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
- package/dist/extensions/agent-browser/lib/playbook.js +20 -24
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
- package/dist/extensions/agent-browser/lib/process.js +131 -48
- package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
- package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
- package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
- package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
- package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
- package/dist/extensions/agent-browser/lib/results/network.js +0 -7
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
- package/dist/extensions/agent-browser/lib/results/text.js +0 -7
- package/dist/extensions/agent-browser/lib/runtime.js +119 -52
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/temp.js +0 -7
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +0 -5
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +50 -31
- package/docs/COMMAND_REFERENCE.md +97 -41
- package/docs/ELECTRON.md +3 -19
- package/docs/RELEASE.md +8 -20
- package/docs/REQUIREMENTS.md +8 -6
- package/docs/SUPPORT_MATRIX.md +32 -25
- package/docs/TOOL_CONTRACT.md +107 -64
- package/docs/platform-smoke.md +1 -1
- package/package.json +7 -6
- 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 +8 -7
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
- package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
- package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
- package/dist/extensions/agent-browser/lib/results.js +0 -14
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
|
|
3
|
-
* Responsibilities: Validate POSIX socket storage, spawn the agent-browser subprocess, forward parent environment variables plus wrapper overrides, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
|
|
4
|
-
* Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
|
|
5
|
-
* Usage: Called by the extension tool after argument validation and session planning are complete.
|
|
6
|
-
* Invariants/Assumptions: The binary name is always `agent-browser`; Windows routes through PowerShell to invoke npm launchers with escaped argv; callers handle semantic success/error interpretation.
|
|
7
|
-
*/
|
|
8
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
2
|
import { spawn } from "node:child_process";
|
|
10
3
|
import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
@@ -13,10 +6,11 @@ import { env as processEnv, platform as processPlatform } from "node:process";
|
|
|
13
6
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
14
7
|
import { needsManagedSession } from "./command-policy.js";
|
|
15
8
|
import { isKnownCommandToken } from "./command-taxonomy.js";
|
|
16
|
-
import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
|
|
17
|
-
import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
18
|
-
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";
|
|
19
12
|
import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
|
|
13
|
+
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
20
14
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
21
15
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
22
16
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
@@ -27,16 +21,29 @@ const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
|
|
|
27
21
|
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
28
22
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
29
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";
|
|
30
25
|
const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
|
|
26
|
+
const TERMUX_PACKAGE_NAME_PATTERN = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
|
|
31
27
|
export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
|
|
32
28
|
const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
33
29
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
34
30
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
35
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.";
|
|
36
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
|
+
]);
|
|
37
39
|
export function withAttachedBrowserSessionContext(preserve, run) {
|
|
38
40
|
return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
|
|
39
41
|
}
|
|
42
|
+
export function getWindowsExplicitDefaultNamespaceEnv(args, parentNamespace, platform = processPlatform) {
|
|
43
|
+
return platform === "win32" && resolveAgentBrowserNamespace(args, parentNamespace) === ""
|
|
44
|
+
? { AGENT_BROWSER_NAMESPACE: "" }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
40
47
|
function appendTail(text, addition, maxChars) {
|
|
41
48
|
const combined = text + addition;
|
|
42
49
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
@@ -50,7 +57,12 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
50
57
|
for (let index = 0; index < args.length; index += 1) {
|
|
51
58
|
const token = args[index];
|
|
52
59
|
if (isKnownCommandToken(token)) {
|
|
53
|
-
|
|
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)];
|
|
54
66
|
}
|
|
55
67
|
if (!token.startsWith("-"))
|
|
56
68
|
return args;
|
|
@@ -84,6 +96,13 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
84
96
|
const value = args[index + 1];
|
|
85
97
|
if (value === undefined)
|
|
86
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
|
+
}
|
|
87
106
|
leadingGlobals.push(token, value);
|
|
88
107
|
index += 1;
|
|
89
108
|
continue;
|
|
@@ -110,9 +129,9 @@ export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUser
|
|
|
110
129
|
return filtered;
|
|
111
130
|
// Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
|
|
112
131
|
const browserArgs = wrapperCompatibilityUserAgent
|
|
113
|
-
? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
|
|
114
|
-
:
|
|
115
|
-
return [
|
|
132
|
+
? ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`]
|
|
133
|
+
: [];
|
|
134
|
+
return [...browserArgs, "--allow-file-access", "false", ...filtered];
|
|
116
135
|
}
|
|
117
136
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
118
137
|
if (platform !== "win32") {
|
|
@@ -223,33 +242,43 @@ function clampUpstreamDefaultTimeout(childEnv) {
|
|
|
223
242
|
export function getAgentBrowserProcessTimeoutMs(env = processEnv) {
|
|
224
243
|
return parsePositiveIntegerEnv(env[PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV]) ?? DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS;
|
|
225
244
|
}
|
|
226
|
-
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) {
|
|
227
246
|
if (platform === "win32") {
|
|
228
247
|
return undefined;
|
|
229
248
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
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);
|
|
232
274
|
}
|
|
233
275
|
async function hasTrustedSocketDirAncestry(socketDir, uid) {
|
|
234
276
|
for (let current = dirname(socketDir);;) {
|
|
235
277
|
const metadata = await lstat(current);
|
|
236
|
-
if (metadata
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
else if (!metadata.isDirectory()) {
|
|
278
|
+
if (isTrustedAndroidAppDataRoot(current, metadata, uid))
|
|
279
|
+
return true;
|
|
280
|
+
if (!isTrustedSocketDirAncestor(metadata, uid))
|
|
241
281
|
return false;
|
|
242
|
-
}
|
|
243
|
-
if (!metadata.isSymbolicLink()) {
|
|
244
|
-
const mode = metadata.mode & 0o7777;
|
|
245
|
-
if (metadata.uid === uid) {
|
|
246
|
-
if ((mode & 0o022) !== 0)
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
else if (metadata.uid !== 0 || ((mode & 0o022) !== 0 && (mode & 0o1000) === 0)) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
282
|
const parent = dirname(current);
|
|
254
283
|
if (parent === current)
|
|
255
284
|
return true;
|
|
@@ -280,28 +309,64 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
|
|
|
280
309
|
}
|
|
281
310
|
return true;
|
|
282
311
|
}
|
|
283
|
-
export async function
|
|
284
|
-
if (!isAbsolute(socketDir)
|
|
285
|
-
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";
|
|
286
317
|
try {
|
|
287
318
|
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
288
|
-
return
|
|
319
|
+
return "an ancestor is writable, foreign-owned, a non-directory, or an untrusted symlink";
|
|
289
320
|
try {
|
|
290
321
|
await mkdir(socketDir, { mode: 0o700 });
|
|
291
322
|
}
|
|
292
323
|
catch (error) {
|
|
293
324
|
if (error.code !== "EEXIST")
|
|
294
|
-
return
|
|
325
|
+
return `the directory could not be created (${error.code ?? "unknown error"})`;
|
|
295
326
|
}
|
|
296
327
|
const metadata = await lstat(socketDir);
|
|
297
|
-
if (!metadata.isDirectory()
|
|
298
|
-
return
|
|
299
|
-
|
|
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;
|
|
300
341
|
}
|
|
301
|
-
catch {
|
|
302
|
-
return
|
|
342
|
+
catch (error) {
|
|
343
|
+
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
303
344
|
}
|
|
304
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
|
+
}
|
|
305
370
|
export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
|
|
306
371
|
const childEnv = {};
|
|
307
372
|
for (const [name, value] of Object.entries(baseEnv)) {
|
|
@@ -332,6 +397,7 @@ function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessio
|
|
|
332
397
|
currentPageUrl,
|
|
333
398
|
cwd: options.cwd,
|
|
334
399
|
env: effectiveEnv ?? options.env,
|
|
400
|
+
managedSessionRestoreKey: getOwnedManagedSessionRestoreKey(),
|
|
335
401
|
pageUrlUnknown,
|
|
336
402
|
parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
|
|
337
403
|
stdin: options.stdin,
|
|
@@ -355,11 +421,24 @@ export async function runAgentBrowserProcess(options) {
|
|
|
355
421
|
if (signal?.aborted) {
|
|
356
422
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
357
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();
|
|
358
436
|
const managedSessionRestoreOptions = {
|
|
359
437
|
args,
|
|
360
438
|
cwd,
|
|
361
439
|
env,
|
|
362
440
|
ownedManagedSession,
|
|
441
|
+
parentEnv,
|
|
363
442
|
restoreState: managedSessionRestoreState,
|
|
364
443
|
stdin,
|
|
365
444
|
};
|
|
@@ -398,6 +477,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
398
477
|
...managedSessionRestoreConfigEnv,
|
|
399
478
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
400
479
|
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
480
|
+
...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
|
|
401
481
|
...ownedManagedSessionCompatibilityEnv,
|
|
402
482
|
AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
|
|
403
483
|
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
@@ -406,18 +486,21 @@ export async function runAgentBrowserProcess(options) {
|
|
|
406
486
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
407
487
|
if (ownedManagedSessionClose)
|
|
408
488
|
effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
|
|
409
|
-
const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
|
|
489
|
+
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
|
|
410
490
|
if (requestedSocketDir !== undefined) {
|
|
411
|
-
const
|
|
491
|
+
const socketDirError = requestedSocketDir.length > 0
|
|
492
|
+
? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
|
|
493
|
+
: "the configured path is empty";
|
|
412
494
|
if (signal?.aborted) {
|
|
413
495
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
414
496
|
}
|
|
415
|
-
|
|
497
|
+
const socketPathError = socketDirError ? undefined : getAgentBrowserSocketPathValidationError({ args, env: effectiveEnv, socketDir: requestedSocketDir });
|
|
498
|
+
if (socketDirError || socketPathError) {
|
|
416
499
|
return {
|
|
417
500
|
aborted: false,
|
|
418
501
|
agentBrowserStarted: false,
|
|
419
502
|
exitCode: 1,
|
|
420
|
-
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.`),
|
|
421
504
|
stderr: "",
|
|
422
505
|
stdout: "",
|
|
423
506
|
timedOut: false,
|
|
@@ -528,7 +611,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
528
611
|
});
|
|
529
612
|
});
|
|
530
613
|
};
|
|
531
|
-
const childEnv = buildAgentBrowserProcessEnv(
|
|
614
|
+
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
532
615
|
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
|
|
533
616
|
if (spawnPolicyError) {
|
|
534
617
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Derive operator prompt constraints for browser-run preflight guards and legacy bash policy.
|
|
3
|
-
* Responsibilities: Parse the latest user message into requested artifact paths and legacy bash allowance.
|
|
4
|
-
* Scope: Pure prompt-text policy; enforcement lives in orchestration prompt-guards and the extension entrypoint.
|
|
5
|
-
*/
|
|
6
1
|
const BROWSER_PROMPT_PATTERNS = [
|
|
7
2
|
/\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
|
|
8
3
|
/\b(?:react\s+(?:tree|inspect|renders|suspense)|web\s+vitals|core\s+web\s+vitals|pushstate)\b/i,
|
|
@@ -17,41 +12,190 @@ const LEGACY_BASH_ALLOW_PATTERNS = [
|
|
|
17
12
|
/\bagent-browser\s+--(?:help|version)\b/i,
|
|
18
13
|
/\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
|
|
19
14
|
];
|
|
20
|
-
const PROMPT_ARTIFACT_PATH_PATTERN = /(?:^|[\s"'`(:])((?:\/[^\s"'`),;]+|[A-Za-z]:[\\/][^\s"'`),;]+|\.{1,2}[\\/][^\s"'`),;]+|[^\s"'`),;:\\/]+(?:[\\/][^\s"'`)
|
|
21
|
-
|
|
15
|
+
const PROMPT_ARTIFACT_PATH_PATTERN = /(?:^|[\s"'`(:])((?:\/[^\s"'`),;]+|[A-Za-z]:[\\/][^\s"'`),;]+|\.{1,2}[\\/][^\s"'`),;]+|[^\s"'`()[\],;:\\/]+(?:[\\/][^\s"'`()[\],;\\/]+)+|[^\s"'`()[\],;:\\/]+)\.(?:png|jpe?g|webp|gif|webm|mp4|har|pdf|trace|json))(?=[\s"'`),;.!?]|$)/gi;
|
|
16
|
+
const PROMPT_ARTIFACT_COLON_OUTPUT_INTENT_PATTERN = /\b(?:capture|create|export|generate|output|record|render|save|screenshot|start|take|write)\s+(?:(?:a|an|another|the)\s+)?(?:short\s+)?(?:(?:full[- ]page|page|screen)\s+)?(?:image|page|recordings?|screenshots?|screen|video)\s*:\s*$/i;
|
|
17
|
+
const PROMPT_ARTIFACT_OUTPUT_INTENT_PATTERN = /\b(?:capture|create|export|generate|output|record|render|save|screenshot|start|take|write)\s+(?:(?:a|an|another|the|this)\s+)?(?:short\s+)?(?:(?:full[- ]page|page|screen)\s+)?(?:image|page|recordings?|screenshots?|screen|video)\s+(?:directly\s+)?(?:\b(?:at|as|to)\b\s*[:=-]?|\bhere\b(?:\s+(?:if|when)\s+(?:recordings?\s+)?(?:(?:are|is)\s+)?available)?\s*[:=-]?)\s*$|\b(?:export|output|save|write)\s+(?:it\s+)?(?:at|as|to)\s*[:=-]?\s*$/i;
|
|
18
|
+
const PROMPT_ARTIFACT_UNSAFE_OUTPUT_PREFIX_PATTERN = /n['’]t\b|\b(?:cannot|disallowed|forbidden|maybe|needed|never|no|not|optional(?:ly)?|perhaps|prohibited|rather|refrain|unable|without)\b|\b(?:he|i|it|she|they|we|you)\s+(?:can|could|may|might)\b/i;
|
|
19
|
+
const PROMPT_ARTIFACT_AFFIRMATIVE_PREFIX_PATTERN = /^(?:(?:and(?:\s+then)?|then)(?:\s+please)?|please|you|(?:be|make)\s+sure\s+to|(?:can|could|will|would)\s+you(?:\s+please)?|(?:i|we)\s+(?:need|want)\s+you\s+to|you\s+(?:must|should))$/i;
|
|
20
|
+
const PROMPT_ARTIFACT_AFFIRMATIVE_LEAD_IN_PATTERN = /^(?:please\s+)?(?:capture|create|export|generate|output|record|render|save|screenshot|start|take|write)\b.*(?:[,\-:–—]|\b(?:and|then))\s*$/i;
|
|
21
|
+
const PROMPT_ARTIFACT_LIST_CONNECTOR_PATTERN = /^[\s"'`()\[\]{},;:.*!?\/&+>-]*(?:(?:and|or)[\s"'`()\[\]{},;:.*!?\/&+>-]*)?$/i;
|
|
22
|
+
const PROMPT_ARTIFACT_LIST_PREFIX_PATTERN = /^\s*(?:(?:[-*+]|\d+[.)])\s*)/;
|
|
23
|
+
const PROMPT_ARTIFACT_OPTIONAL_RECORDING_PATTERN = /\b(?:if|when)\s+(?:recordings?\s+)?(?:(?:are|is)\s+)?available\b/i;
|
|
24
|
+
const PROMPT_ARTIFACT_STANDALONE_OPTIONAL_RECORDING_PATTERN = /^\s*(?:if|when)\s+(?:recordings?\s+)?(?:(?:are|is)\s+)?available[.:;]?\s*$/i;
|
|
25
|
+
const PROMPT_ARTIFACT_EXPLICIT_OPTIONAL_PATTERN = /\b(?:optionally|if\s+(?:convenient|desired|needed|possible|you\s+can|you\s+want\s+to)|when\s+(?:convenient|desired|needed|possible)|only\s+if\s+you\s+(?:can|want\s+to))\b/i;
|
|
26
|
+
const PROMPT_ARTIFACT_REFERENCE_INTENT_PATTERN = /\btake\s+the\s+(?:image|recording|screenshot|video)\s+(?:at|from)\b/i;
|
|
27
|
+
const PROMPT_ARTIFACT_CLAUSE_BOUNDARY_PATTERN = /(?:[;.!?](?:\s|$)|\b(?:but|instead)\b)/i;
|
|
28
|
+
function getPromptArtifactKind(path) {
|
|
22
29
|
const lowerPath = path.toLowerCase();
|
|
23
30
|
if (/\.(?:webm|mp4)$/.test(lowerPath))
|
|
24
31
|
return "recording";
|
|
25
32
|
if (/\.(?:png|jpe?g|webp|gif)$/.test(lowerPath))
|
|
26
33
|
return "screenshot";
|
|
27
|
-
const lowerLine = line.toLowerCase();
|
|
28
|
-
if (lowerLine.includes("screenshot"))
|
|
29
|
-
return "screenshot";
|
|
30
|
-
if (/\b(?:screen\s+recording|recording|webm|video)\b/.test(lowerLine))
|
|
31
|
-
return "recording";
|
|
32
34
|
return undefined;
|
|
33
35
|
}
|
|
36
|
+
function getPromptArtifactIntentClause(context) {
|
|
37
|
+
return context.split(PROMPT_ARTIFACT_CLAUSE_BOUNDARY_PATTERN).at(-1) ?? context;
|
|
38
|
+
}
|
|
39
|
+
function getPromptArtifactTrailingClause(context) {
|
|
40
|
+
return context.split(PROMPT_ARTIFACT_CLAUSE_BOUNDARY_PATTERN)[0] ?? context;
|
|
41
|
+
}
|
|
42
|
+
function hasPromptArtifactOutputIntent(context) {
|
|
43
|
+
const clause = getPromptArtifactIntentClause(context)
|
|
44
|
+
.replace(/\[[^\]\r\n]*\]\(\s*$/, "")
|
|
45
|
+
.replace(/[([{\"'`]+\s*$/, "");
|
|
46
|
+
const intentMatch = clause.match(PROMPT_ARTIFACT_OUTPUT_INTENT_PATTERN) ?? clause.match(PROMPT_ARTIFACT_COLON_OUTPUT_INTENT_PATTERN);
|
|
47
|
+
if (!intentMatch)
|
|
48
|
+
return false;
|
|
49
|
+
const prefix = clause.slice(0, intentMatch.index ?? 0);
|
|
50
|
+
const governingPrefix = prefix.split(/\b(?:before|unless|until)\b/i).at(-1) ?? prefix;
|
|
51
|
+
if (PROMPT_ARTIFACT_UNSAFE_OUTPUT_PREFIX_PATTERN.test(governingPrefix))
|
|
52
|
+
return false;
|
|
53
|
+
const normalizedPrefix = governingPrefix.trim();
|
|
54
|
+
return normalizedPrefix.length === 0
|
|
55
|
+
|| PROMPT_ARTIFACT_AFFIRMATIVE_PREFIX_PATTERN.test(normalizedPrefix)
|
|
56
|
+
|| PROMPT_ARTIFACT_AFFIRMATIVE_LEAD_IN_PATTERN.test(governingPrefix);
|
|
57
|
+
}
|
|
58
|
+
function stripPromptArtifactListPrefix(context) {
|
|
59
|
+
return context.replace(PROMPT_ARTIFACT_LIST_PREFIX_PATTERN, "");
|
|
60
|
+
}
|
|
61
|
+
function normalizePromptArtifactPath(path) {
|
|
62
|
+
return path.replace(/^[([{]+/, "");
|
|
63
|
+
}
|
|
64
|
+
function isLikelyInboundPromptArtifact(path) {
|
|
65
|
+
return /(?:^|[\\/])pi-(?:attachment|clipboard|paste|upload)-/i.test(path);
|
|
66
|
+
}
|
|
34
67
|
function extractPromptRequestedArtifacts(prompt) {
|
|
35
68
|
const artifacts = [];
|
|
36
|
-
const seen = new
|
|
37
|
-
|
|
69
|
+
const seen = new Map();
|
|
70
|
+
const lines = prompt.split(/\r?\n/);
|
|
71
|
+
let inCodeFence = false;
|
|
72
|
+
let listArtifactIndexes = [];
|
|
73
|
+
let listContinuation;
|
|
74
|
+
let pendingRecordingAvailability = false;
|
|
75
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
76
|
+
const line = lines[lineIndex] ?? "";
|
|
77
|
+
if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
|
|
78
|
+
inCodeFence = !inCodeFence;
|
|
79
|
+
listArtifactIndexes = [];
|
|
80
|
+
listContinuation = undefined;
|
|
81
|
+
pendingRecordingAvailability = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (inCodeFence)
|
|
85
|
+
continue;
|
|
38
86
|
PROMPT_ARTIFACT_PATH_PATTERN.lastIndex = 0;
|
|
87
|
+
const pathMatches = [];
|
|
39
88
|
for (const match of line.matchAll(PROMPT_ARTIFACT_PATH_PATTERN)) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const kind = inferPromptArtifactKind(line, path);
|
|
44
|
-
if (!kind)
|
|
89
|
+
const rawPath = match[1]?.trim();
|
|
90
|
+
const path = rawPath ? normalizePromptArtifactPath(rawPath) : undefined;
|
|
91
|
+
if (!path || !rawPath)
|
|
45
92
|
continue;
|
|
46
|
-
const
|
|
47
|
-
|
|
93
|
+
const start = (match.index ?? 0) + match[0].indexOf(rawPath) + rawPath.indexOf(path);
|
|
94
|
+
pathMatches.push({ end: start + path.length, path, start });
|
|
95
|
+
}
|
|
96
|
+
if (pathMatches.length === 0 && PROMPT_ARTIFACT_STANDALONE_OPTIONAL_RECORDING_PATTERN.test(line)) {
|
|
97
|
+
if (listContinuation?.kind === "recording") {
|
|
98
|
+
for (const artifactIndex of listArtifactIndexes) {
|
|
99
|
+
if (artifacts[artifactIndex]?.kind === "recording")
|
|
100
|
+
artifacts[artifactIndex].required = false;
|
|
101
|
+
}
|
|
102
|
+
listContinuation = { ...listContinuation, required: false };
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
pendingRecordingAvailability = true;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (pathMatches.length === 0 && pendingRecordingAvailability && /\brecordings?\b/i.test(line) && hasPromptArtifactOutputIntent(line))
|
|
110
|
+
continue;
|
|
111
|
+
const pendingRecordingAvailabilityForLine = pathMatches.length > 0 && pendingRecordingAvailability;
|
|
112
|
+
pendingRecordingAvailability = false;
|
|
113
|
+
const pathlessLine = stripPromptArtifactListPrefix(line.replace(PROMPT_ARTIFACT_PATH_PATTERN, ""));
|
|
114
|
+
const remainder = pathlessLine.replace(/[\s"'`()\[\],;:.*!?>-]+/g, "").toLowerCase();
|
|
115
|
+
const listSyntax = pathlessLine
|
|
116
|
+
.replace(PROMPT_ARTIFACT_OPTIONAL_RECORDING_PATTERN, "")
|
|
117
|
+
.replace(PROMPT_ARTIFACT_EXPLICIT_OPTIONAL_PATTERN, "");
|
|
118
|
+
const isPathList = pathMatches.length > 0 && PROMPT_ARTIFACT_LIST_CONNECTOR_PATTERN.test(listSyntax);
|
|
119
|
+
const groupOptional = new Map();
|
|
120
|
+
const candidates = [];
|
|
121
|
+
let nextGroup = 0;
|
|
122
|
+
let previousGroup;
|
|
123
|
+
let previousKind;
|
|
124
|
+
let previousPathEnd = 0;
|
|
125
|
+
for (let matchIndex = 0; matchIndex < pathMatches.length; matchIndex += 1) {
|
|
126
|
+
const { end, path, start } = pathMatches[matchIndex];
|
|
127
|
+
const localContext = stripPromptArtifactListPrefix(line.slice(previousPathEnd, start));
|
|
128
|
+
const intentContext = matchIndex === 0 && (isPathList || !remainder || ["file", "output", "path"].includes(remainder))
|
|
129
|
+
? `${lines[lineIndex - 1] ?? ""}\n${localContext}`
|
|
130
|
+
: localContext;
|
|
131
|
+
const kind = getPromptArtifactKind(path);
|
|
132
|
+
const directIntent = hasPromptArtifactOutputIntent(intentContext);
|
|
133
|
+
const sameLineContinuation = kind === previousKind && previousGroup !== undefined && PROMPT_ARTIFACT_LIST_CONNECTOR_PATTERN.test(localContext);
|
|
134
|
+
const priorLineContinuation = matchIndex === 0 && isPathList && kind === listContinuation?.kind && PROMPT_ARTIFACT_LIST_CONNECTOR_PATTERN.test(localContext);
|
|
135
|
+
let group;
|
|
136
|
+
if (directIntent) {
|
|
137
|
+
group = nextGroup++;
|
|
138
|
+
groupOptional.set(group, pendingRecordingAvailabilityForLine || PROMPT_ARTIFACT_OPTIONAL_RECORDING_PATTERN.test(getPromptArtifactIntentClause(intentContext)));
|
|
139
|
+
}
|
|
140
|
+
else if (sameLineContinuation) {
|
|
141
|
+
group = previousGroup;
|
|
142
|
+
}
|
|
143
|
+
else if (priorLineContinuation) {
|
|
144
|
+
group = nextGroup++;
|
|
145
|
+
groupOptional.set(group, listContinuation?.required === false);
|
|
146
|
+
}
|
|
147
|
+
const referenceReading = directIntent && PROMPT_ARTIFACT_REFERENCE_INTENT_PATTERN.test(getPromptArtifactIntentClause(intentContext));
|
|
148
|
+
const trailingContext = getPromptArtifactTrailingClause(line.slice(end, pathMatches[matchIndex + 1]?.start ?? line.length));
|
|
149
|
+
const explicitlyOptional = directIntent && (PROMPT_ARTIFACT_EXPLICIT_OPTIONAL_PATTERN.test(getPromptArtifactIntentClause(intentContext))
|
|
150
|
+
|| PROMPT_ARTIFACT_EXPLICIT_OPTIONAL_PATTERN.test(trailingContext));
|
|
151
|
+
if (!kind || group === undefined || referenceReading || explicitlyOptional || isLikelyInboundPromptArtifact(path)) {
|
|
152
|
+
previousGroup = undefined;
|
|
153
|
+
previousKind = undefined;
|
|
154
|
+
previousPathEnd = end;
|
|
48
155
|
continue;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
}
|
|
157
|
+
candidates.push({ continuedFromPriorLine: priorLineContinuation && !directIntent, group, kind, matchIndex, path });
|
|
158
|
+
previousGroup = group;
|
|
159
|
+
previousKind = kind;
|
|
160
|
+
previousPathEnd = end;
|
|
161
|
+
}
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
const match = pathMatches[candidate.matchIndex];
|
|
164
|
+
const nextStart = pathMatches[candidate.matchIndex + 1]?.start ?? line.length;
|
|
165
|
+
if (PROMPT_ARTIFACT_OPTIONAL_RECORDING_PATTERN.test(getPromptArtifactTrailingClause(line.slice(match.end, nextStart)))) {
|
|
166
|
+
groupOptional.set(candidate.group, true);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const lineArtifactIndexes = [];
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
const required = candidate.kind === "screenshot" || groupOptional.get(candidate.group) !== true;
|
|
172
|
+
const key = `${candidate.kind}:${candidate.path}`;
|
|
173
|
+
let artifactIndex = seen.get(key);
|
|
174
|
+
if (artifactIndex === undefined) {
|
|
175
|
+
artifactIndex = artifacts.length;
|
|
176
|
+
seen.set(key, artifactIndex);
|
|
177
|
+
artifacts.push({ kind: candidate.kind, path: candidate.path, required });
|
|
178
|
+
}
|
|
179
|
+
else if (required) {
|
|
180
|
+
artifacts[artifactIndex].required = true;
|
|
181
|
+
}
|
|
182
|
+
lineArtifactIndexes.push(artifactIndex);
|
|
183
|
+
}
|
|
184
|
+
const lastCandidate = candidates.at(-1);
|
|
185
|
+
if (isPathList && lastCandidate?.matchIndex === pathMatches.length - 1) {
|
|
186
|
+
const lastArtifact = artifacts[lineArtifactIndexes.at(-1)];
|
|
187
|
+
listContinuation = { kind: lastArtifact.kind, required: lastArtifact.required };
|
|
188
|
+
if (candidates[0]?.continuedFromPriorLine) {
|
|
189
|
+
for (const artifactIndex of lineArtifactIndexes)
|
|
190
|
+
listArtifactIndexes.push(artifactIndex);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
listArtifactIndexes = lineArtifactIndexes;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
listArtifactIndexes = [];
|
|
198
|
+
listContinuation = undefined;
|
|
55
199
|
}
|
|
56
200
|
}
|
|
57
201
|
return artifacts;
|