pi-agent-browser-native 0.5.0 → 0.6.6

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 (64) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +75 -42
  3. package/dist/extensions/agent-browser/index.js +13 -83
  4. package/dist/extensions/agent-browser/lib/argv-grammar.js +8 -2
  5. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +1 -1
  6. package/dist/extensions/agent-browser/lib/command-policy.js +4 -7
  7. package/dist/extensions/agent-browser/lib/command-taxonomy.js +19 -11
  8. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  9. package/dist/extensions/agent-browser/lib/config.js +1 -1
  10. package/dist/extensions/agent-browser/lib/input-modes/job.js +0 -9
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  12. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +18 -4
  13. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -138
  14. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -81
  15. package/dist/extensions/agent-browser/lib/managed-session-storage.js +4 -1
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +31 -26
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +60 -8
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +1 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +31 -40
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +76 -70
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +6 -8
  24. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +5 -10
  25. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  26. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  27. package/dist/extensions/agent-browser/lib/playbook.js +13 -12
  28. package/dist/extensions/agent-browser/lib/process-identity.js +11 -10
  29. package/dist/extensions/agent-browser/lib/process.js +18 -82
  30. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -78
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +1 -1
  32. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +27 -14
  33. package/dist/extensions/agent-browser/lib/results/presentation/common.js +20 -2
  34. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +11 -16
  35. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +12 -9
  36. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +2 -2
  37. package/dist/extensions/agent-browser/lib/results/presentation.js +31 -4
  38. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +1 -1
  39. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  40. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +3 -3
  41. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  42. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  43. package/dist/extensions/agent-browser/lib/runtime.js +73 -72
  44. package/dist/extensions/agent-browser/lib/session-page-state.js +12 -3
  45. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  46. package/dist/extensions/agent-browser/lib/upstream-version.js +5 -5
  47. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  48. package/dist/scripts/agent-browser-target.mjs +19 -1
  49. package/docs/ARCHITECTURE.md +24 -20
  50. package/docs/COMMAND_REFERENCE.md +181 -49
  51. package/docs/ELECTRON.md +2 -2
  52. package/docs/RELEASE.md +10 -8
  53. package/docs/REQUIREMENTS.md +8 -7
  54. package/docs/SUPPORT_MATRIX.md +31 -26
  55. package/docs/TOOL_CONTRACT.md +89 -56
  56. package/package.json +1 -1
  57. package/scripts/agent-browser-capability-baseline.mjs +65 -5
  58. package/scripts/agent-browser-target.mjs +19 -1
  59. package/scripts/config.mjs +1 -0
  60. package/scripts/doctor.mjs +15 -9
  61. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  62. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -601
  63. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  64. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,601 +0,0 @@
1
- import { realpathSync } from "node:fs";
2
- import { basename, dirname, isAbsolute, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { parseArgvDescriptor } from "./argv-descriptor.js";
5
- import { needsManagedSession } from "./command-policy.js";
6
- import { getScreenshotPathTokenIndex } from "./orchestration/browser-run/artifact-paths.js";
7
- import { getUpstreamEffectiveBatchSteps, parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
8
- import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
9
- import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, extractExplicitSessionName, isUpstreamEnvFlagEnabled, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
10
- import { extractManagedSessionRestoreKeys, isWrapperManagedSessionName } from "./managed-session-capabilities.js";
11
- import { agentBrowserExplicitConfigIsPresent } from "./managed-session-restore.js";
12
- import { hasManagedSessionRestoreProjectIdentity } from "./managed-session-storage.js";
13
- import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
14
- const BLOCKED_GLOBAL_STATE_MESSAGE = "This operation could read or modify wrapper-owned browser state outside the current checkout. Use a caller-owned state name or path instead.";
15
- const BLOCKED_MANAGED_BROWSER_FILE_MESSAGE = "Browser access to local .agent-browser storage is blocked because state files can contain authenticated cookies and storage. Use guarded state commands instead.";
16
- const BLOCKED_MANAGED_SESSION_MESSAGE = "This session name is reserved for a browser managed by this extension instance. Use the current managed session or a caller-owned session name instead.";
17
- const FILE_ACCESS_FLAG_MESSAGE = "Browser file-access enablement is blocked because a local page could read and exfiltrate authenticated .agent-browser state.";
18
- const UPSTREAM_CONFIG_MESSAGE = "Explicit upstream agent-browser config from --config or AGENT_BROWSER_CONFIG is blocked for browser-backed native calls because it can load protected state, profiles, extensions, or file-access settings. Remove that override and pass safe settings explicitly. Passive agent-browser.json files are ignored through the wrapper's protected empty config.";
19
- const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly to a safe URL before page-content inspection.";
20
- const BATCH_UNVERIFIED_PAGE_MESSAGE = `${UNVERIFIED_PAGE_MESSAGE} In a batch, put get url after the transition before later content steps, or split the batch at that boundary.`;
21
- const UNSAFE_BATCH_ARGUMENT_MESSAGE = "Batch command arguments could not be safely inspected. Use batch stdin JSON command arrays instead.";
22
- const NESTED_BATCH_ARGUMENT_MESSAGE = "Nested batch commands are blocked by the wrapper's page-state safety policy. Flatten the batch steps instead.";
23
- const NON_BAIL_BATCH_NAVIGATION_MESSAGE = "Batches that navigate before page-content access must use exact batch --bail so a failed navigation cannot expose the prior page.";
24
- const MAX_NON_BAIL_BATCH_PAGE_STATES = 64;
25
- const EXPLICIT_NAVIGATION_COMMANDS = new Set(["a11y", "goto", "navigate", "open", "pushstate", "visit", "vitals", "web-vitals"]);
26
- const FILE_PATH_GLOBAL_FLAGS = ["--action-policy", "--config", "--download-path", "--executable-path", "--extension", "--init-script", "--profile", "--screenshot-dir", "--state"];
27
- const FILE_PATH_ENV_VARIABLES = [
28
- "AGENT_BROWSER_ACTION_POLICY",
29
- "AGENT_BROWSER_CONFIG",
30
- "AGENT_BROWSER_DOWNLOAD_PATH",
31
- "AGENT_BROWSER_EXECUTABLE_PATH",
32
- "AGENT_BROWSER_PROFILE",
33
- "AGENT_BROWSER_SCREENSHOT_DIR",
34
- "AGENT_BROWSER_SKILLS_DIR",
35
- "AGENT_BROWSER_SOCKET_DIR",
36
- "AGENT_BROWSER_STATE",
37
- ];
38
- const FILE_PATH_LIST_ENV_VARIABLES = ["AGENT_BROWSER_EXTENSIONS", "AGENT_BROWSER_INIT_SCRIPTS"];
39
- const POSITIONAL_VALUE_FLAGS = new Set([...VALUE_FLAGS, "--llms"]);
40
- function decodeUrlComponent(value) {
41
- let decoded = value;
42
- for (let attempt = 0; attempt < 8; attempt += 1) {
43
- try {
44
- const next = decodeURIComponent(decoded);
45
- if (next === decoded)
46
- break;
47
- decoded = next;
48
- }
49
- catch {
50
- break;
51
- }
52
- }
53
- return decoded;
54
- }
55
- function hasAgentBrowserPathComponent(value) {
56
- return decodeUrlComponent(value).replaceAll("\\", "/").toLowerCase().split("/").some((component) => {
57
- const windowsCanonicalComponent = component.replace(/^[a-z]:/i, "").split(":", 1)[0]?.replace(/[. ]+$/g, "");
58
- return windowsCanonicalComponent === ".agent-browser";
59
- });
60
- }
61
- function getFileUrlPath(value) {
62
- if (!value)
63
- return undefined;
64
- const trimmedValue = value.replaceAll(/[\t\n\r]/g, "").trim();
65
- const nestedFileUrl = /^(?:(?:blob|filesystem|view-source):)*(file:.*)$/i.exec(trimmedValue)?.[1];
66
- if (!nestedFileUrl)
67
- return undefined;
68
- try {
69
- return fileURLToPath(new URL(nestedFileUrl));
70
- }
71
- catch {
72
- try {
73
- return decodeUrlComponent(new URL(nestedFileUrl).pathname);
74
- }
75
- catch {
76
- return trimmedValue;
77
- }
78
- }
79
- }
80
- export function isFileUrl(value) {
81
- return getFileUrlPath(value) !== undefined;
82
- }
83
- function resolveThroughExistingAncestor(path) {
84
- const suffix = [];
85
- let current = path;
86
- for (;;) {
87
- try {
88
- return resolve(realpathSync(current), ...suffix.reverse());
89
- }
90
- catch {
91
- const parent = dirname(current);
92
- if (parent === current)
93
- return undefined;
94
- suffix.push(basename(current));
95
- current = parent;
96
- }
97
- }
98
- }
99
- export function isProtectedAgentBrowserFileTarget(value, cwd) {
100
- if (!value)
101
- return false;
102
- const filePath = getFileUrlPath(value);
103
- const windowsDrivePath = /^[a-z]:/i.test(value);
104
- const windowsAbsolutePath = /^[a-z]:[\\/]/i.test(value);
105
- const hasScheme = /^[a-z][a-z\d+.-]*:/i.test(value) && !windowsDrivePath;
106
- if (hasScheme && filePath === undefined)
107
- return false;
108
- const pathValue = filePath ?? (isAbsolute(value) || windowsAbsolutePath ? value : resolve(cwd, value));
109
- if (hasAgentBrowserPathComponent(pathValue))
110
- return true;
111
- const resolvedPath = resolveThroughExistingAncestor(pathValue);
112
- return resolvedPath !== undefined && hasAgentBrowserPathComponent(resolvedPath);
113
- }
114
- export function getAgentBrowserStoragePathValidationError(value, cwd) {
115
- return isProtectedAgentBrowserFileTarget(value, cwd) ? BLOCKED_MANAGED_BROWSER_FILE_MESSAGE : undefined;
116
- }
117
- export function getObservedBrowserPageValidationError(args, observedUrl, cwd) {
118
- if (!isFileUrl(observedUrl))
119
- return undefined;
120
- if (isProtectedAgentBrowserFileTarget(observedUrl, cwd))
121
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
122
- const descriptor = parseArgvDescriptor(args);
123
- if (descriptor.commandInfo.command === "get" && descriptor.commandInfo.subcommand === "url")
124
- return undefined;
125
- const explicitTarget = getExplicitNavigationTarget(args);
126
- return explicitTarget !== undefined && isFileUrl(explicitTarget) && !isProtectedAgentBrowserFileTarget(explicitTarget, cwd)
127
- ? undefined
128
- : BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
129
- }
130
- function getPositionalOperands(commandTokens) {
131
- const values = [];
132
- for (let index = 1; index < commandTokens.length; index += 1) {
133
- const token = commandTokens[index];
134
- if (!token)
135
- continue;
136
- if (token.includes("=") && token.startsWith("-"))
137
- continue;
138
- if (POSITIONAL_VALUE_FLAGS.has(token)) {
139
- index += 1;
140
- continue;
141
- }
142
- if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(token)) {
143
- if (["true", "false"].includes(commandTokens[index + 1] ?? ""))
144
- index += 1;
145
- continue;
146
- }
147
- if (!token.startsWith("-") || token.includes("/") || token.includes("\\"))
148
- values.push(token);
149
- }
150
- return values;
151
- }
152
- function getExplicitNavigationTarget(args) {
153
- const descriptor = parseArgvDescriptor(args);
154
- const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
155
- if (EXPLICIT_NAVIGATION_COMMANDS.has(descriptor.commandInfo.command ?? ""))
156
- return positionals[0];
157
- if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
158
- return positionals[1];
159
- return undefined;
160
- }
161
- function getResultingExplicitNavigationTarget(args, currentPageUrl) {
162
- const target = getExplicitNavigationTarget(args);
163
- if (target === undefined)
164
- return undefined;
165
- const descriptor = parseArgvDescriptor(args);
166
- if (descriptor.commandInfo.command !== "pushstate")
167
- return target;
168
- try {
169
- return new URL(target).href;
170
- }
171
- catch {
172
- if (!currentPageUrl)
173
- return undefined;
174
- try {
175
- return new URL(target, currentPageUrl).href;
176
- }
177
- catch {
178
- return undefined;
179
- }
180
- }
181
- }
182
- function getBrowserContentUrlOperands(args) {
183
- const descriptor = parseArgvDescriptor(args);
184
- const command = descriptor.commandInfo.command;
185
- const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
186
- if (["a11y", "read", "vitals", "web-vitals"].includes(command ?? ""))
187
- return positionals.slice(0, 1);
188
- if (command === "auth" && descriptor.commandInfo.subcommand === "login")
189
- return getFlagValues(descriptor.upstreamCommandTokens, "--url", true);
190
- if (command === "diff" && descriptor.commandInfo.subcommand === "url")
191
- return positionals.slice(1, 3);
192
- if (command === "record" && ["start", "restart"].includes(descriptor.commandInfo.subcommand ?? ""))
193
- return positionals.slice(2, 3);
194
- return [];
195
- }
196
- function trimUnicodeWhitespace(value) {
197
- return value.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
198
- }
199
- function getRawBrowserArgsValues(args, env) {
200
- const values = [env.AGENT_BROWSER_ARGS];
201
- for (let index = 0; index < args.length; index += 1) {
202
- const token = args[index];
203
- if (token.startsWith("--args="))
204
- values.push(token.slice("--args=".length));
205
- else if (token === "--args")
206
- values.push(args[++index]);
207
- }
208
- return values;
209
- }
210
- function getRawBrowserFileOperands(args, env) {
211
- return getRawBrowserArgsValues(args, env).flatMap((value) => value?.split(/[,\r\n]/).flatMap((rawToken) => {
212
- const token = trimUnicodeWhitespace(rawToken);
213
- const equalsIndex = token.indexOf("=");
214
- return equalsIndex < 0 ? [token] : [token, token.slice(equalsIndex + 1)];
215
- }) ?? []);
216
- }
217
- function getBrowserFileEnvOperands(env) {
218
- return [
219
- ...FILE_PATH_ENV_VARIABLES.map((name) => env[name] === undefined ? undefined : trimUnicodeWhitespace(env[name])),
220
- ...FILE_PATH_LIST_ENV_VARIABLES.flatMap((name) => env[name]?.split(/[,\r\n]/).map(trimUnicodeWhitespace) ?? []),
221
- ];
222
- }
223
- function getBrowserFileOperands(args, env) {
224
- const descriptor = parseArgvDescriptor(args);
225
- const command = descriptor.commandInfo.command;
226
- const subcommand = descriptor.commandInfo.subcommand;
227
- const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
228
- const values = [
229
- getExplicitNavigationTarget(args),
230
- ...getBrowserContentUrlOperands(args),
231
- ...FILE_PATH_GLOBAL_FLAGS.flatMap((flag) => getFlagValues(args, flag, true)),
232
- ...getBrowserFileEnvOperands(env),
233
- ...getRawBrowserFileOperands(args, env),
234
- ];
235
- if (command === "read")
236
- values.push(positionals[0]);
237
- if (command === "upload")
238
- values.push(...positionals.slice(1));
239
- if (command === "download")
240
- values.push(...positionals.slice(1));
241
- if (command === "pdf")
242
- values.push(positionals[0]);
243
- if (command === "screenshot") {
244
- const pathIndex = getScreenshotPathTokenIndex(descriptor.upstreamCommandTokens);
245
- values.push(pathIndex === undefined ? undefined : descriptor.upstreamCommandTokens[pathIndex]);
246
- }
247
- if (["profiler", "trace"].includes(command ?? "") && subcommand === "stop")
248
- values.push(...positionals.slice(1));
249
- if (command === "network" && subcommand === "har")
250
- values.push(...positionals.slice(2));
251
- if (command === "wait")
252
- values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--download", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-d", true));
253
- if (command === "cookies" && subcommand === "set")
254
- values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--curl", true));
255
- if (command === "state" && ["load", "rename", "save", "show"].includes(subcommand ?? ""))
256
- values.push(...positionals.slice(1));
257
- if (command === "diff" && subcommand === "url")
258
- values.push(...positionals.slice(1));
259
- if (command === "diff")
260
- values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--baseline", true), ...getFlagValues(descriptor.upstreamCommandTokens, "--output", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-o", true));
261
- if (command === "record" && ["start", "restart"].includes(subcommand ?? ""))
262
- values.push(...positionals.slice(1));
263
- return values.filter((value) => typeof value === "string" && value.length > 0);
264
- }
265
- function rawBrowserArgsEnableFileAccess(args, env) {
266
- return getRawBrowserArgsValues(args, env).some((value) => typeof value === "string" && /(?:^|[,\p{White_Space}])--(?:allow-file-access(?:-from-files)?|disable-web-security)(?=$|[=,\p{White_Space}])/iu.test(value));
267
- }
268
- function fileAccessFlagIsEnabled(args, env) {
269
- let enabled = isUpstreamEnvFlagEnabled(env.AGENT_BROWSER_ALLOW_FILE_ACCESS);
270
- for (let index = 0; index < args.length; index += 1) {
271
- if (args[index]?.startsWith("--allow-file-access=") && !args[index]?.endsWith("=false")) {
272
- enabled = true;
273
- continue;
274
- }
275
- if (args[index] !== "--allow-file-access")
276
- continue;
277
- const value = args[index + 1];
278
- enabled = value !== "false";
279
- if (value === "true" || value === "false")
280
- index += 1;
281
- }
282
- return enabled;
283
- }
284
- function getManagedBrowserFileAccessValidationError(options) {
285
- const descriptor = parseArgvDescriptor(options.args);
286
- const command = descriptor.commandInfo.command;
287
- const subcommand = descriptor.commandInfo.subcommand;
288
- const explicitTarget = getResultingExplicitNavigationTarget(options.args, options.currentPageUrl);
289
- const closesPage = ["close", "exit", "quit"].includes(command ?? "") || (command === "tab" && subcommand === "close");
290
- const safelyInspectsPageTarget = (command === "tab" && subcommand === "list") || (command === "get" && subcommand === "url");
291
- const safelySelectsUnverifiedTab = command === "tab" && subcommand !== undefined && !["close", "list", "new"].includes(subcommand);
292
- const safelyTransitionsUnverifiedPage = options.allowUnverifiedPageTransitions === true
293
- && command !== "eval"
294
- && isUnverifiedPageTransitionCommand(command, subcommand);
295
- const safelyLeavesPage = explicitTarget !== undefined && !isFileUrl(explicitTarget) && !isProtectedAgentBrowserFileTarget(explicitTarget, options.cwd);
296
- if (fileAccessFlagIsEnabled(options.args, options.env) || rawBrowserArgsEnableFileAccess(options.args, options.env))
297
- return FILE_ACCESS_FLAG_MESSAGE;
298
- if (getBrowserContentUrlOperands(options.args).some(isFileUrl))
299
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
300
- if (getBrowserFileOperands(options.args, options.env).some((value) => isProtectedAgentBrowserFileTarget(value, options.cwd)))
301
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
302
- if (command === "eval" && options.stdin !== undefined && /(?:\.agent-browser|%2eagent-browser)/i.test(options.stdin))
303
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
304
- if (options.pageUrlUnknown && !closesPage && !safelyInspectsPageTarget && !safelySelectsUnverifiedTab && !safelyTransitionsUnverifiedPage && !safelyLeavesPage && !(options.trustedBatchTabSelection && command === "tab"))
305
- return UNVERIFIED_PAGE_MESSAGE;
306
- if (isProtectedAgentBrowserFileTarget(options.currentPageUrl, options.cwd) && !closesPage && !safelyInspectsPageTarget && !safelyLeavesPage)
307
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
308
- if (isFileUrl(options.currentPageUrl) && !closesPage && !safelyInspectsPageTarget && !safelyLeavesPage)
309
- return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
310
- return undefined;
311
- }
312
- // Deliberately unions raw argv steps AND parseable stdin steps even though
313
- // upstream executes argv steps exclusively when any exist: this pre-spawn
314
- // validator must stay a superset of anything upstream could run, so it fails
315
- // closed on unsafe content in either source. Stdin parse failures are fatal
316
- // only when upstream would actually read stdin (no argv steps); ignored
317
- // unparseable stdin cannot execute and must not reject a valid raw-argv call.
318
- // The upstream-exact selection lives in getUpstreamEffectiveBatchSteps
319
- // (batch-stdin.ts) for guard/folding paths.
320
- function getBatchCommandSteps(args, stdin) {
321
- const descriptor = parseArgvDescriptor(args);
322
- if (descriptor.commandInfo.command !== "batch")
323
- return { steps: [] };
324
- const steps = [];
325
- for (const command of descriptor.upstreamCommandTokens.slice(1)) {
326
- if (command === "--bail")
327
- continue;
328
- if (decodeUrlComponent(command).toLowerCase().includes(".agent-browser"))
329
- return { error: BLOCKED_MANAGED_BROWSER_FILE_MESSAGE, steps: [] };
330
- const parsed = parseBatchCommandArgument(command);
331
- if (parsed.error || parsed.step === undefined)
332
- return { error: parsed.error ?? UNSAFE_BATCH_ARGUMENT_MESSAGE, steps: [] };
333
- if (parseArgvDescriptor(parsed.step).commandInfo.command === "batch")
334
- return { error: NESTED_BATCH_ARGUMENT_MESSAGE, steps: [] };
335
- steps.push(parsed.step);
336
- }
337
- const parsedStdin = parseUserBatchStdin(stdin);
338
- if (parsedStdin.error && steps.length === 0)
339
- return { error: parsedStdin.error, steps: [] };
340
- for (const step of parsedStdin.steps ?? []) {
341
- if (parseArgvDescriptor(step).commandInfo.command === "batch")
342
- return { error: NESTED_BATCH_ARGUMENT_MESSAGE, steps: [] };
343
- steps.push(step);
344
- }
345
- return { steps };
346
- }
347
- export function invocationMayNavigateToLocalFile(args, stdin) {
348
- if (isFileUrl(getResultingExplicitNavigationTarget(args)))
349
- return true;
350
- const descriptor = parseArgvDescriptor(args);
351
- return getUpstreamEffectiveBatchSteps(descriptor.upstreamCommandTokens, stdin)
352
- .some((step) => isFileUrl(getResultingExplicitNavigationTarget(step)));
353
- }
354
- function batchBailsOnFirstError(args) {
355
- const descriptor = parseArgvDescriptor(args);
356
- return descriptor.commandInfo.command === "batch" && descriptor.upstreamCommandTokens.slice(1).includes("--bail");
357
- }
358
- function commandMayChangePageTarget(args, trustedBatchTabSelection) {
359
- const descriptor = parseArgvDescriptor(args);
360
- return getExplicitNavigationTarget(args) !== undefined
361
- || (isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand)
362
- && !(trustedBatchTabSelection && descriptor.commandInfo.command === "tab"));
363
- }
364
- function deduplicatePossibleBatchPageStates(states) {
365
- const deduplicated = new Map();
366
- for (const state of states) {
367
- const key = `${state.pageUrlUnknown ? "unknown" : "known"}\0${state.currentPageUrl ?? ""}`;
368
- const existing = deduplicated.get(key);
369
- if (!existing)
370
- deduplicated.set(key, state);
371
- else
372
- existing.retainedAfterFailedNavigation ||= state.retainedAfterFailedNavigation;
373
- }
374
- return [...deduplicated.values()];
375
- }
376
- export function managedSessionCommandRequiresLivePageVerification(args, stdin) {
377
- const descriptor = parseArgvDescriptor(args);
378
- if (descriptor.commandInfo.command === "eval")
379
- return true;
380
- if (descriptor.commandInfo.command !== "batch")
381
- return false;
382
- const batch = getBatchCommandSteps(args, stdin);
383
- return batch.error === undefined && batch.steps.some((step) => managedSessionCommandRequiresLivePageVerification(step));
384
- }
385
- function getResultingPageState(options) {
386
- const descriptor = parseArgvDescriptor(options.args);
387
- const rawExplicitTarget = getExplicitNavigationTarget(options.args);
388
- const explicitTarget = getResultingExplicitNavigationTarget(options.args, options.currentPageUrl);
389
- if (explicitTarget !== undefined)
390
- return { currentPageUrl: explicitTarget, pageUrlUnknown: false };
391
- if (rawExplicitTarget !== undefined)
392
- return { pageUrlUnknown: true };
393
- if (isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand)) {
394
- return options.trustedBatchTabSelection && descriptor.commandInfo.command === "tab"
395
- ? { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown }
396
- : { pageUrlUnknown: true };
397
- }
398
- return { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown };
399
- }
400
- export function getManagedSessionResultingPageState(options) {
401
- const descriptor = parseArgvDescriptor(options.args);
402
- let state = { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown ?? false };
403
- let pageTargetMayHaveChanged = false;
404
- if (descriptor.commandInfo.command !== "batch") {
405
- pageTargetMayHaveChanged = getExplicitNavigationTarget(options.args) !== undefined
406
- || isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand);
407
- return { ...getResultingPageState({ ...state, args: options.args, trustedBatchTabSelection: false }), pageTargetMayHaveChanged };
408
- }
409
- const batch = getBatchCommandSteps(options.args, options.stdin);
410
- if (batch.error)
411
- return { pageTargetMayHaveChanged: true, pageUrlUnknown: true };
412
- for (let index = 0; index < batch.steps.length; index += 1) {
413
- const step = batch.steps[index];
414
- const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
415
- const stepDescriptor = parseArgvDescriptor(step);
416
- pageTargetMayHaveChanged ||= getExplicitNavigationTarget(step) !== undefined
417
- || (isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand)
418
- && !(trustedBatchTabSelection && stepDescriptor.commandInfo.command === "tab"));
419
- state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection });
420
- }
421
- return { ...state, pageTargetMayHaveChanged };
422
- }
423
- export function getCallerOwnedSessionLivePageVerificationRequirement(options) {
424
- const descriptor = parseArgvDescriptor(options.args);
425
- if (!needsManagedSession(descriptor))
426
- return undefined;
427
- if (descriptor.commandInfo.command !== "eval" && isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand))
428
- return undefined;
429
- const validationError = getManagedSessionStateAccessValidationError({
430
- args: options.args,
431
- cwd: options.cwd,
432
- env: {},
433
- pageUrlUnknown: true,
434
- parentEnv: {},
435
- stdin: options.stdin,
436
- allowUnverifiedPageTransitions: true,
437
- trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
438
- trustedPinnedEmptyConfig: true,
439
- });
440
- return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
441
- ? UNVERIFIED_PAGE_MESSAGE
442
- : undefined;
443
- }
444
- export function getManagedSessionTargetAccessValidationError(args, ownedManagedSession, env = getAgentBrowserProcessEnvironment()) {
445
- const sessionName = extractExplicitSessionName(args) ?? env.AGENT_BROWSER_SESSION;
446
- return sessionName && isWrapperManagedSessionName(sessionName) && !ownedManagedSession
447
- ? BLOCKED_MANAGED_SESSION_MESSAGE
448
- : undefined;
449
- }
450
- function getFlagValues(args, flag, consumeDashValue = false) {
451
- const values = [];
452
- for (let index = 0; index < args.length; index += 1) {
453
- const token = args[index];
454
- if (token.startsWith(`${flag}=`)) {
455
- values.push(token.slice(flag.length + 1));
456
- continue;
457
- }
458
- if (token !== flag)
459
- continue;
460
- const value = args[index + 1];
461
- if (flag === "--restore" && !optionalGlobalValueFlagConsumesNext(flag, value))
462
- continue;
463
- if (value !== undefined && (consumeDashValue || !value.startsWith("-")))
464
- values.push(value);
465
- }
466
- return values;
467
- }
468
- function resolveExistingPath(cwd, value) {
469
- try {
470
- return realpathSync(resolve(cwd, value));
471
- }
472
- catch {
473
- return undefined;
474
- }
475
- }
476
- function getReferencedValues(args, cwd, env) {
477
- const descriptor = parseArgvDescriptor(args);
478
- const values = [
479
- ...args,
480
- ...getFlagValues(args, "--restore"),
481
- ...getFlagValues(args, "--state", true),
482
- env.AGENT_BROWSER_RESTORE,
483
- env.AGENT_BROWSER_STATE,
484
- ].filter((value) => typeof value === "string" && value.length > 0);
485
- if (descriptor.commandInfo.command === "state" && ["clear", "load", "rename", "save", "show"].includes(descriptor.commandInfo.subcommand ?? "")) {
486
- values.push(...descriptor.upstreamCommandTokens.slice(2));
487
- }
488
- for (const value of [...values]) {
489
- const realPath = resolveExistingPath(cwd, value);
490
- if (realPath)
491
- values.push(realPath);
492
- }
493
- return values;
494
- }
495
- export function getManagedSessionStateAccessValidationError(options) {
496
- const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
497
- const descriptor = parseArgvDescriptor(options.args);
498
- const command = descriptor.commandInfo.command;
499
- const subcommand = descriptor.commandInfo.subcommand;
500
- if (["close", "exit", "quit"].includes(command ?? ""))
501
- return undefined;
502
- if (!options.trustedPinnedEmptyConfig && needsManagedSession(descriptor) && agentBrowserExplicitConfigIsPresent(effectiveEnv, options.args))
503
- return UPSTREAM_CONFIG_MESSAGE;
504
- if (command === "batch") {
505
- const batch = getBatchCommandSteps(options.args, options.stdin);
506
- if (batch.error) {
507
- if (batch.error === BLOCKED_MANAGED_BROWSER_FILE_MESSAGE || batch.error === NESTED_BATCH_ARGUMENT_MESSAGE || batch.error.startsWith("agent_browser batch stdin"))
508
- return batch.error;
509
- return UNSAFE_BATCH_ARGUMENT_MESSAGE;
510
- }
511
- const bailOnFirstError = batchBailsOnFirstError(options.args);
512
- let possibleStates = [{
513
- currentPageUrl: options.currentPageUrl,
514
- pageUrlUnknown: options.pageUrlUnknown ?? false,
515
- retainedAfterFailedNavigation: false,
516
- }];
517
- for (let index = 0; index < batch.steps.length; index += 1) {
518
- const step = batch.steps[index];
519
- const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
520
- let directError;
521
- let failedNavigationHazard = false;
522
- for (const state of possibleStates) {
523
- const error = getManagedSessionStateAccessValidationError({
524
- ...options,
525
- args: step,
526
- currentPageUrl: state.currentPageUrl,
527
- pageUrlUnknown: state.pageUrlUnknown,
528
- stdin: undefined,
529
- trustedFirstBatchTabSelection: trustedBatchTabSelection,
530
- });
531
- if (!error)
532
- continue;
533
- if (state.retainedAfterFailedNavigation && [BLOCKED_MANAGED_BROWSER_FILE_MESSAGE, UNVERIFIED_PAGE_MESSAGE].includes(error)) {
534
- failedNavigationHazard = true;
535
- }
536
- else {
537
- directError ??= error;
538
- }
539
- }
540
- if (directError)
541
- return directError === UNVERIFIED_PAGE_MESSAGE ? BATCH_UNVERIFIED_PAGE_MESSAGE : directError;
542
- if (failedNavigationHazard)
543
- return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
544
- const mayChangePageTarget = commandMayChangePageTarget(step, trustedBatchTabSelection);
545
- const nextStates = [];
546
- for (const state of possibleStates) {
547
- const successState = getResultingPageState({
548
- args: step,
549
- currentPageUrl: state.currentPageUrl,
550
- pageUrlUnknown: state.pageUrlUnknown,
551
- trustedBatchTabSelection,
552
- });
553
- if (nextStates.length >= MAX_NON_BAIL_BATCH_PAGE_STATES)
554
- return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
555
- nextStates.push({
556
- ...successState,
557
- retainedAfterFailedNavigation: mayChangePageTarget ? false : state.retainedAfterFailedNavigation,
558
- });
559
- if (!bailOnFirstError && mayChangePageTarget) {
560
- if (nextStates.length >= MAX_NON_BAIL_BATCH_PAGE_STATES)
561
- return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
562
- nextStates.push({ ...state, retainedAfterFailedNavigation: true });
563
- }
564
- }
565
- possibleStates = deduplicatePossibleBatchPageStates(nextStates);
566
- }
567
- }
568
- const browserFileError = getManagedBrowserFileAccessValidationError({
569
- allowUnverifiedPageTransitions: options.allowUnverifiedPageTransitions,
570
- args: options.args,
571
- currentPageUrl: command === "batch" ? undefined : options.currentPageUrl,
572
- cwd: options.cwd,
573
- env: effectiveEnv,
574
- pageUrlUnknown: command === "batch" ? false : options.pageUrlUnknown,
575
- stdin: options.stdin,
576
- trustedBatchTabSelection: options.trustedFirstBatchTabSelection,
577
- });
578
- if (browserFileError)
579
- return browserFileError;
580
- if (command === "state" && subcommand === "clean")
581
- return BLOCKED_GLOBAL_STATE_MESSAGE;
582
- if (command === "state" && subcommand === "clear") {
583
- const target = descriptor.upstreamCommandTokens.slice(2).find((token) => !token.startsWith("-"));
584
- if (!target || descriptor.upstreamCommandTokens.includes("--all") || descriptor.upstreamCommandTokens.includes("-a") || isWrapperManagedSessionName(target)) {
585
- return BLOCKED_GLOBAL_STATE_MESSAGE;
586
- }
587
- }
588
- const referencedValues = [
589
- ...getReferencedValues(options.args, options.cwd, effectiveEnv),
590
- ...(options.stdin ? [options.stdin] : []),
591
- ];
592
- const referencedKeys = [...new Set(referencedValues.flatMap(extractManagedSessionRestoreKeys))];
593
- if (referencedKeys.length === 0)
594
- return undefined;
595
- if (command === "state" && ["rename", "save"].includes(subcommand ?? ""))
596
- return BLOCKED_GLOBAL_STATE_MESSAGE;
597
- if (!hasManagedSessionRestoreProjectIdentity(options.cwd) || !options.managedSessionRestoreKey)
598
- return BLOCKED_GLOBAL_STATE_MESSAGE;
599
- const currentKey = options.managedSessionRestoreKey.toLowerCase();
600
- return referencedKeys.every((key) => key === currentKey) ? undefined : BLOCKED_GLOBAL_STATE_MESSAGE;
601
- }
@@ -1,78 +0,0 @@
1
- function normalizeDomainEntry(value) {
2
- let candidate = value.trim().toLowerCase();
3
- if (!candidate)
4
- return undefined;
5
- try {
6
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(candidate)) {
7
- candidate = new URL(candidate).hostname;
8
- }
9
- }
10
- catch {
11
- return undefined;
12
- }
13
- candidate = candidate.replace(/^\*\./, "").replace(/\.$/, "");
14
- if (candidate.includes("/"))
15
- candidate = candidate.split("/")[0] ?? "";
16
- if (candidate.includes(":"))
17
- candidate = candidate.split(":")[0] ?? "";
18
- return candidate.length > 0 ? candidate : undefined;
19
- }
20
- function splitAllowedDomainsValue(value) {
21
- return value.split(/[,\s]+/).map((entry) => entry.trim()).filter(Boolean);
22
- }
23
- export function parseAllowedDomainsPolicyFromArgs(args) {
24
- const domains = [];
25
- for (let index = 0; index < args.length; index += 1) {
26
- const arg = args[index];
27
- if (arg === "--allowed-domains") {
28
- const value = args[index + 1];
29
- if (value && !value.startsWith("-")) {
30
- domains.push(...splitAllowedDomainsValue(value));
31
- index += 1;
32
- }
33
- continue;
34
- }
35
- if (arg?.startsWith("--allowed-domains=")) {
36
- domains.push(...splitAllowedDomainsValue(arg.slice("--allowed-domains=".length)));
37
- }
38
- }
39
- const allowedDomains = [...new Set(domains.flatMap((domain) => {
40
- const normalized = normalizeDomainEntry(domain);
41
- return normalized ? [normalized] : [];
42
- }))];
43
- if (allowedDomains.length === 0)
44
- return undefined;
45
- return { allowedDomains, display: allowedDomains.join(", ") };
46
- }
47
- function normalizeObservedHost(url) {
48
- try {
49
- const parsed = new URL(url);
50
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
51
- return undefined;
52
- return parsed.hostname.toLowerCase().replace(/\.$/, "");
53
- }
54
- catch {
55
- return undefined;
56
- }
57
- }
58
- export function isHostAllowedByDomains(host, allowedDomains) {
59
- const normalizedHost = host.toLowerCase().replace(/\.$/, "");
60
- return allowedDomains.some((domain) => normalizedHost === domain || normalizedHost.endsWith(`.${domain}`));
61
- }
62
- export function getAllowedDomainsViolation(options) {
63
- if (!options.policy || !options.url)
64
- return undefined;
65
- const observedHost = normalizeObservedHost(options.url);
66
- if (!observedHost)
67
- return undefined;
68
- if (isHostAllowedByDomains(observedHost, options.policy.allowedDomains))
69
- return undefined;
70
- const summary = `Navigation policy blocked: --allowed-domains ${options.policy.display} does not allow ${observedHost} (${options.url}).`;
71
- return {
72
- allowedDomains: options.policy.allowedDomains,
73
- allowedDisplay: options.policy.display,
74
- observedHost,
75
- observedUrl: options.url,
76
- summary,
77
- };
78
- }