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.
Files changed (118) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +69 -35
  3. package/dist/extensions/agent-browser/index.js +911 -121
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
  10. package/dist/extensions/agent-browser/lib/config.js +0 -6
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
  15. package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
  16. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
  17. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
  18. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
  19. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
  21. package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
  22. package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
  23. package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
  24. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
  25. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
  26. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
  27. package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
  28. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
  29. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
  30. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
  31. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
  32. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
  49. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  50. package/dist/extensions/agent-browser/lib/parsing.js +0 -7
  51. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
  52. package/dist/extensions/agent-browser/lib/playbook.js +20 -24
  53. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  54. package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
  55. package/dist/extensions/agent-browser/lib/process.js +131 -48
  56. package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
  57. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  58. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
  59. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
  60. package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
  61. package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
  62. package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
  63. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
  64. package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
  65. package/dist/extensions/agent-browser/lib/results/network.js +0 -7
  66. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
  67. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
  68. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  69. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
  70. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
  71. package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
  72. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
  73. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  74. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
  75. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
  76. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
  77. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  78. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
  79. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
  80. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
  81. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
  82. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
  83. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
  84. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
  85. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
  86. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
  87. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
  88. package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
  89. package/dist/extensions/agent-browser/lib/results/text.js +0 -7
  90. package/dist/extensions/agent-browser/lib/runtime.js +119 -52
  91. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
  92. package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
  93. package/dist/extensions/agent-browser/lib/temp.js +0 -7
  94. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  95. package/dist/extensions/agent-browser/lib/web-search.js +0 -5
  96. package/dist/extensions/agent-browser/script-worker.js +169 -0
  97. package/dist/scripts/agent-browser-target.mjs +3 -0
  98. package/docs/ARCHITECTURE.md +50 -31
  99. package/docs/COMMAND_REFERENCE.md +97 -41
  100. package/docs/ELECTRON.md +3 -19
  101. package/docs/RELEASE.md +8 -20
  102. package/docs/REQUIREMENTS.md +8 -6
  103. package/docs/SUPPORT_MATRIX.md +32 -25
  104. package/docs/TOOL_CONTRACT.md +107 -64
  105. package/docs/platform-smoke.md +1 -1
  106. package/package.json +7 -6
  107. package/platform-smoke.config.mjs +2 -2
  108. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  109. package/scripts/agent-browser-target.mjs +3 -0
  110. package/scripts/build.mjs +41 -0
  111. package/scripts/doctor.mjs +8 -7
  112. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  113. package/scripts/platform-smoke/targets.mjs +12 -6
  114. package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
  115. package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
  116. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
  117. package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
  118. 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
- return index === 0 ? args : [token, ...leadingGlobals, ...args.slice(index + 1)];
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 ["--args", browserArgs, "--allow-file-access", "false", ...filtered];
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 prefix = platform === "darwin" ? "/private/tmp/piab" : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
231
- return `${prefix}${typeof uid === "number" ? `-${uid}` : ""}`;
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.isSymbolicLink()) {
237
- if (metadata.uid !== 0)
238
- return false;
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 ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
284
- if (!isAbsolute(socketDir) || typeof uid !== "number")
285
- return false;
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 false;
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 false;
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() || metadata.isSymbolicLink() || metadata.uid !== uid || (metadata.mode & 0o777) !== 0o700)
298
- return false;
299
- return await hasTrustedSocketDirAncestry(socketDir, uid) && await socketDirEntriesAreOwned(socketDir, uid);
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 false;
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 socketDirIsSecure = requestedSocketDir.length > 0 && await ensureAgentBrowserSocketDir(requestedSocketDir);
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
- if (!socketDirIsSecure) {
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("Agent-browser socket storage must be an absolute, non-symlink directory owned by the current user with mode 0700."),
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(processEnv, effectiveEnv);
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"'`),;]+)+|[^\s"'`),;:\\/]+)\.(?:png|jpe?g|webp|gif|webm|mp4|har|pdf|trace|json))(?:[\s"'`),;.]|$)/gi;
21
- function inferPromptArtifactKind(line, path) {
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 Set();
37
- for (const line of prompt.split(/\r?\n/)) {
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 path = match[1]?.trim();
41
- if (!path)
42
- continue;
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 key = `${kind}:${path}`;
47
- if (seen.has(key))
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
- seen.add(key);
50
- artifacts.push({
51
- kind,
52
- path,
53
- required: kind === "screenshot" || !/\b(?:if|when)\s+(?:recording\s+)?(?:is\s+)?available\b/i.test(line),
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;