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
@@ -197,7 +197,7 @@ export function formatPresentationSummary(commandInfo, data, compiledSemanticAct
197
197
  return semanticSummary;
198
198
  }
199
199
  const navigationSummary = getNavigationSummary(data);
200
- if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command)) {
200
+ if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command, presentationCommandInfo.subcommand)) {
201
201
  const navigationText = formatNavigationSummary(navigationSummary);
202
202
  if (navigationText) {
203
203
  return `${presentationCommandInfo.command ?? "navigation"} → ${navigationText.split("\n", 1)[0] ?? navigationText}`;
@@ -242,7 +242,7 @@ export function formatPresentationContentText(commandInfo, data, compiledSemanti
242
242
  }
243
243
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfo, compiledSemanticAction);
244
244
  const navigationSummary = getNavigationSummary(data);
245
- if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command)) {
245
+ if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command, presentationCommandInfo.subcommand)) {
246
246
  const navigationText = formatNavigationSummary(navigationSummary);
247
247
  if (navigationText) {
248
248
  const actionText = formatNavigationActionResult(data);
@@ -1,13 +1,13 @@
1
1
  import { isRecord } from "../parsing.js";
2
- import { extractUpstreamCommandTokens } from "../runtime.js";
2
+ import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
3
3
  import { buildAgentBrowserNextActions } from "./action-recommendations.js";
4
4
  import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
5
5
  import { detectConfirmationRequired } from "./confirmation.js";
6
6
  import { buildSnapshotPresentation } from "./snapshot.js";
7
7
  import { parseJsonPreviewString, redactModelFacingText, stringifyModelFacing } from "./presentation/common.js";
8
8
  import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts, classifyPresentationSuccessCategory, extractFileArtifacts, extractImagePath, formatArtifactMetadataLines, formatArtifactSummary, formatMissingArtifactFailureText, getSavedFileDetails, hasMissingFileArtifact, isManifestFileArtifact, } from "./presentation/artifacts.js";
9
- import { buildBatchPresentation, isAgentBrowserBatchResultArray } from "./presentation/batch.js";
10
- import { getPresentationPaths } from "./presentation/content.js";
9
+ import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
10
+ import { getPresentationPaths, isStringArray } from "./presentation/content.js";
11
11
  import { buildNetworkRequestsNextActions, buildStreamNextActions, enrichStreamStatusData, formatNetworkRouteDiagnosticsText, redactPresentationData, } from "./presentation/diagnostics.js";
12
12
  import { buildErrorPresentation } from "./presentation/errors.js";
13
13
  import { compactLargePresentationOutput } from "./presentation/large-output.js";
@@ -31,6 +31,23 @@ function mergeNextActions(...groups) {
31
31
  function shouldAddAnnotatedScreenshotGuidance(commandInfo, args) {
32
32
  return commandInfo.command === "screenshot" && (args?.includes("--annotate") ?? false);
33
33
  }
34
+ function getKeyboardInsertTextWarning(commandInfo) {
35
+ if (commandInfo.command !== "keyboard" || commandInfo.subcommand !== "inserttext")
36
+ return undefined;
37
+ return "Input dispatch warning: keyboard inserttext skips key events. A DOM value change does not prove a framework-controlled editor accepted it; verify application state before saving, or use keyboard type when real key events are required.";
38
+ }
39
+ function redactBatchSpillData(data) {
40
+ return data.map((row) => {
41
+ const command = isStringArray(row.command) ? row.command : undefined;
42
+ const commandInfo = parseCommandInfo(command ?? []);
43
+ return {
44
+ ...row,
45
+ command: command ? redactInvocationArgs(command) : row.command,
46
+ error: row.error === undefined ? undefined : redactBatchStepErrorData(command, row.error),
47
+ result: row.result === undefined ? undefined : redactPresentationData(commandInfo, row.result),
48
+ };
49
+ });
50
+ }
34
51
  export async function buildToolPresentation(options) {
35
52
  const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
36
53
  const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
@@ -39,7 +56,9 @@ export async function buildToolPresentation(options) {
39
56
  return buildErrorPresentation({ args, commandInfo, errorText, sessionName });
40
57
  }
41
58
  const data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
42
- const presentationData = redactPresentationData(commandInfoWithTokens, data);
59
+ const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
60
+ ? redactBatchSpillData(data)
61
+ : redactPresentationData(commandInfoWithTokens, data);
43
62
  const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, sessionName });
44
63
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
45
64
  const artifactSummary = formatArtifactSummary(artifacts);
@@ -95,6 +114,10 @@ export async function buildToolPresentation(options) {
95
114
  const guidance = "Annotated screenshot note: dense pages can produce overlapping labels. If the labels are noisy, capture a scoped element screenshot, take a non-annotated screenshot, or use snapshot -i high-value refs as the machine-readable map.";
96
115
  presentation.content[0] = { ...presentation.content[0], text: `${presentation.content[0].text}\n\n${guidance}` };
97
116
  }
117
+ const keyboardInsertTextWarning = getKeyboardInsertTextWarning(commandInfoWithTokens);
118
+ if (keyboardInsertTextWarning && presentation.content[0]?.type === "text") {
119
+ presentation.content[0] = { ...presentation.content[0], text: `${presentation.content[0].text}\n\n${keyboardInsertTextWarning}` };
120
+ }
98
121
  const imagePath = artifactRequest?.absolutePath ?? extractImagePath(commandInfo, cwd, data);
99
122
  const presentationWithImage = imagePath ? await attachInlineImage(presentation, imagePath) : presentation;
100
123
  const compactedPresentation = await compactLargePresentationOutput({
@@ -159,6 +182,7 @@ export async function buildToolPresentation(options) {
159
182
  resultCategory: presentationWithManifest.resultCategory ?? "success",
160
183
  savedFilePath: presentationWithManifest.savedFilePath,
161
184
  sessionName,
185
+ subcommand: presentationCommandInfo.subcommand,
162
186
  successCategory: presentationWithManifest.successCategory,
163
187
  });
164
188
  const networkNextActions = commandInfoWithTokens.command === "network" && commandInfoWithTokens.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
@@ -174,5 +198,8 @@ export async function buildToolPresentation(options) {
174
198
  savedFilePath: presentationWithManifest.savedFilePath,
175
199
  summary: presentationWithManifest.summary,
176
200
  });
201
+ if (presentationWithManifest.pageChangeSummary?.observed === false && presentationCommandInfo.command !== "batch" && presentationWithManifest.content[0]?.type === "text") {
202
+ presentationWithManifest.content[0] = { ...presentationWithManifest.content[0], text: `${presentationWithManifest.content[0].text}\n\nAction dispatched; application change unverified. Verify the expected URL, text, state, or external receipt before relying on it.` };
203
+ }
177
204
  return sanitizeModelFacingPresentation(presentationWithManifest);
178
205
  }
@@ -60,7 +60,7 @@ export function buildRecoveryNextActions(recovery) {
60
60
  args: sessionArgs(["get", "url"]),
61
61
  id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionGetUrl,
62
62
  reason: "Verify the attached page URL before any page-content inspection.",
63
- safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target is a local file page or the URL cannot be verified.",
63
+ safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target URL cannot be verified.",
64
64
  }),
65
65
  buildNextToolAction({
66
66
  args: sessionArgs(["tab", "list"]),
@@ -17,6 +17,15 @@ export function buildNoActivePageNextActions(sessionName) {
17
17
  resultCategory: "failure",
18
18
  }) ?? [];
19
19
  }
20
+ export function buildPendingWebMcpNextActions(sessionName) {
21
+ return [{
22
+ id: "verify-page-target-after-pending-webmcp",
23
+ params: { args: withOptionalSessionArgs(sessionName, ["get", "url"]) },
24
+ reason: "Verify the current page target before taking a fresh snapshot; use webmcp result or cancel to settle the detached invocation.",
25
+ safety: "Read-only URL inspection. It does not settle the pending page tool, which may still mutate or navigate later.",
26
+ tool: "agent_browser",
27
+ }];
28
+ }
20
29
  export function buildSessionTabRecoveryNextActions(options) {
21
30
  const resultCategory = options.resultCategory ?? "success";
22
31
  return buildAgentBrowserNextActions({
@@ -9,7 +9,7 @@ const SELECTOR_RECOVERY_ACTION_NAMES = new Set(["check", "click", "fill", "selec
9
9
  const VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT = 3;
10
10
  const EDITABLE_CONTROL_ROLES = new Set(["combobox", "searchbox", "textbox"]);
11
11
  const RICH_INPUT_RECOVERY_EDITABLE_ROLES = new Set(["searchbox", "textbox"]);
12
- const RICH_INPUT_RECOVERY_HINT = "After the editable ref is focused, use keyboard inserttext or keyboard type with the intended text in a separate call, and do not press Enter or otherwise submit unless the user flow explicitly calls for it.";
12
+ const RICH_INPUT_RECOVERY_HINT = "After the editable ref is focused, use keyboard type when a framework-controlled editor requires real key events. keyboard inserttext is paste-like and needs separate application-state verification. Do not press Enter or otherwise submit unless the user flow explicitly calls for it.";
13
13
  function isSelectorRecoveryActionName(action) {
14
14
  return SELECTOR_RECOVERY_ACTION_NAMES.has(action);
15
15
  }
@@ -225,7 +225,7 @@ export function buildRichInputRecoveryNextActions(options) {
225
225
  const clickId = getAgentBrowserRichInputRecoveryNextActionId("click", index, candidateCount);
226
226
  const safety = ambiguous
227
227
  ? `Several editable refs share the same exact name. Inspect the current snapshot and use only the ${candidate.ref} ${candidate.role} if it is clearly the intended input. No fill text or submit key is included.`
228
- : "Does not include fill text or submit the form. After focus/click succeeds, use keyboard inserttext or keyboard type with the intended text only if this is the right input.";
228
+ : "Does not include fill text or submit the form. After focus/click succeeds, use keyboard type for framework-controlled editors; use keyboard inserttext only with separate application-state verification.";
229
229
  return [
230
230
  {
231
231
  id: focusId,
@@ -251,7 +251,7 @@ export function formatRichInputRecoveryText(diagnostic) {
251
251
  "Rich input recovery:",
252
252
  ...diagnostic.candidates.map((candidate, index) => {
253
253
  const [focusId, clickId] = diagnostic.nextActionIds.slice(index * 2, index * 2 + 2);
254
- return `- ${candidate.ref} ${candidate.role} ${JSON.stringify(candidate.name)}: use ${focusId} or ${clickId}; then use keyboard inserttext/type with the intended text.`;
254
+ return `- ${candidate.ref} ${candidate.role} ${JSON.stringify(candidate.name)}: use ${focusId} or ${clickId}; then use keyboard type for framework-controlled editors, or paste-like keyboard inserttext only with separate application-state verification.`;
255
255
  }),
256
256
  `- ${diagnostic.inputMethodHint}`,
257
257
  ].join("\n");
@@ -1,9 +1,10 @@
1
+ import { redactSensitiveValue } from "../runtime.js";
1
2
  import { writePersistentSessionArtifactFile, writeSecureTempFile } from "../temp.js";
2
3
  import { buildEvictedSessionArtifactEntries, formatSessionArtifactRetentionSummary, mergeSessionArtifactManifest, } from "./artifact-manifest.js";
3
4
  const SNAPSHOT_SPILL_FILE_PREFIX = "pi-agent-browser-snapshot";
4
5
  export async function writeSnapshotSpillFile(data, persistentArtifactStore) {
5
6
  const options = {
6
- content: JSON.stringify(data, null, 2),
7
+ content: JSON.stringify(redactSensitiveValue(data), null, 2),
7
8
  prefix: SNAPSHOT_SPILL_FILE_PREFIX,
8
9
  suffix: ".json",
9
10
  };
@@ -162,12 +162,12 @@ export async function buildSnapshotPresentation(data, persistentArtifactStore =
162
162
  ...(roleCountsText ? [`Top roles: ${roleCountsText}`] : []),
163
163
  "",
164
164
  "Compact snapshot view.",
165
- "Viewport note: compact snapshots are DOM/signal-prioritized, not guaranteed to start with the currently scrolled viewport; use the full raw snapshot, a screenshot, or listed high-value refs when viewport context matters.",
165
+ "Viewport note: compact snapshots are DOM/signal-prioritized, not guaranteed to start with the currently scrolled viewport; use the full redacted snapshot, a screenshot, or listed high-value refs when viewport context matters.",
166
166
  ];
167
167
  if (fallbackPreview) {
168
168
  lines.push("", "Compact outline:", ...(fallbackPreview.lines.length > 0 ? fallbackPreview.lines : ["(no interactive elements)"]));
169
169
  if (fallbackPreview.omittedCount > 0) {
170
- lines.push(`- ... (${fallbackPreview.omittedCount} additional snapshot lines omitted; ${fullOutputPath ? `full output path: ${fullOutputPath}` : "the full raw snapshot was omitted"})`);
170
+ lines.push(`- ... (${fallbackPreview.omittedCount} additional snapshot lines omitted; ${fullOutputPath ? `full output path: ${fullOutputPath}` : "the full redacted snapshot was omitted"})`);
171
171
  }
172
172
  }
173
173
  else {
@@ -204,8 +204,8 @@ export async function buildSnapshotPresentation(data, persistentArtifactStore =
204
204
  }
205
205
  }
206
206
  lines.push("", fullOutputPath
207
- ? `Full raw snapshot path: ${fullOutputPath}`
208
- : `Full raw snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`);
207
+ ? `Full redacted snapshot path: ${fullOutputPath}`
208
+ : `Full redacted snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`);
209
209
  const manifestFields = applySnapshotArtifactManifest({
210
210
  baseManifest: artifactManifest,
211
211
  command: "snapshot",
@@ -2,9 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename } from "node:path";
3
3
  import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
4
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
5
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
5
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
6
6
  import { needsManagedSession } from "./command-policy.js";
7
- import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
8
7
  import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
9
8
  import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
10
9
  import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
@@ -21,7 +20,9 @@ const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
21
20
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
22
21
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
23
22
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
24
- const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
23
+ const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|relay(?:_|-)?state|saml(?:_|-)?request|saml(?:_|-)?response|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
24
+ const AUTH_STATE_QUERY_PARAM_PATTERN = /^(?:nonce|state)$/i;
25
+ const AUTH_URL_CONTEXT_PATTERN = /(?:^|[./_-])(?:auth|authorize|callback|login|oauth2?|oidc|saml|sso)(?:[./?#_-]|$)/i;
25
26
  const SENSITIVE_FIELD_NAME_PATTERN = /^(?:[A-Za-z0-9_-]*(?:api[_-]?key|access[_-]?key|private[_-]?key|secret(?:[_-]?(?:key|access[_-]?key))?|token|password|passwd|credentials?|database[_-]?url|db[_-]?url|connection[_-]?string|mongo(?:db)?[_-]?uri|redis[_-]?url)|[A-Za-z0-9]*(?:apiKey|ApiKey|apikey|privateKey|PrivateKey|databaseUrl|DatabaseUrl|dbUrl|DbUrl|connectionString|ConnectionString|mongoUri|MongoUri|mongodbUri|MongodbUri|mongoDbUri|MongoDbUri|redisUrl|RedisUrl|Token|Secret|Password|Credential|Credentials)|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
26
27
  const ENV_SECRET_ASSIGNMENT_PATTERN = /\b((?:export\s+)?([A-Za-z_][A-Za-z0-9_-]*)(\s*[:=]\s*))(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)/g;
27
28
  const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM = {
@@ -49,48 +50,57 @@ function redactUrlToken(token) {
49
50
  catch {
50
51
  return token;
51
52
  }
52
- let mutated = false;
53
- if (parsed.username.length > 0) {
53
+ if (parsed.username.length > 0)
54
54
  parsed.username = "[REDACTED]";
55
- mutated = true;
56
- }
57
- if (parsed.password.length > 0) {
55
+ if (parsed.password.length > 0)
58
56
  parsed.password = "[REDACTED]";
59
- mutated = true;
60
- }
57
+ const hashText = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
58
+ const hashParams = hashText.includes("=") ? new URLSearchParams(hashText) : undefined;
59
+ const authContext = AUTH_URL_CONTEXT_PATTERN.test(`${parsed.hostname}${parsed.pathname}`)
60
+ || [...parsed.searchParams.keys(), ...(hashParams?.keys() ?? [])].some(shouldRedactQueryParam);
61
61
  for (const [name] of parsed.searchParams) {
62
- if (shouldRedactQueryParam(name)) {
62
+ if (shouldRedactQueryParam(name) || (authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name))) {
63
63
  parsed.searchParams.set(name, "[REDACTED]");
64
- mutated = true;
65
64
  }
66
65
  }
67
- const hashText = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
68
- if (hashText.includes("=")) {
69
- const hashParams = new URLSearchParams(hashText);
66
+ if (hashParams) {
67
+ let hashMutated = false;
70
68
  for (const [name] of hashParams) {
71
- if (shouldRedactQueryParam(name)) {
69
+ if (shouldRedactQueryParam(name) || (authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name))) {
72
70
  hashParams.set(name, "[REDACTED]");
73
- mutated = true;
71
+ hashMutated = true;
74
72
  }
75
73
  }
76
- if (mutated) {
74
+ if (hashMutated)
77
75
  parsed.hash = `#${hashParams.toString()}`;
78
- }
79
76
  }
80
77
  return parsed.toString();
81
78
  }
82
79
  function redactLooseUrlParameterText(text) {
83
- return text.replace(/([?#&])([^=&#\s]+)=([^&#\s]*)/g, (match, separator, rawName) => {
84
- let name = rawName;
85
- try {
86
- name = decodeURIComponent(rawName.replace(/\+/g, " "));
87
- }
88
- catch {
89
- // Keep the raw name when percent decoding fails.
90
- }
91
- if (!shouldRedactQueryParam(name))
92
- return match;
93
- return `${separator}${rawName}=[REDACTED]`;
80
+ return text.replace(/(?<![^\s"'`<>\])}])[^\s"'`<>\])}]*[?#&][^\s"'`<>\])}]*/g, (token) => {
81
+ const queryNames = [...token.matchAll(/[?#&]([^=&#\s"'`<>\])}]+)=/g)].map((match) => {
82
+ try {
83
+ return decodeURIComponent((match[1] ?? "").replace(/\+/g, " "));
84
+ }
85
+ catch {
86
+ return match[1] ?? "";
87
+ }
88
+ });
89
+ const authContext = AUTH_URL_CONTEXT_PATTERN.test(token) || queryNames.some(shouldRedactQueryParam);
90
+ return token.replace(/([?#&])([^=&#\s"'`<>\])}]+)=([^&#\s"'`<>\])}]*)/g, (match, separator, rawName, rawValue) => {
91
+ if (rawValue === "[REDACTED" || rawValue === "[REDACTED]" || /%5Bredacted%5D/i.test(rawValue))
92
+ return match;
93
+ let name = rawName;
94
+ try {
95
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
96
+ }
97
+ catch {
98
+ // Keep the raw name when percent decoding fails.
99
+ }
100
+ if (!shouldRedactQueryParam(name) && !(authContext && AUTH_STATE_QUERY_PARAM_PATTERN.test(name)))
101
+ return match;
102
+ return `${separator}${rawName}=[REDACTED]`;
103
+ });
94
104
  });
95
105
  }
96
106
  function redactLooseUrlUserinfo(text) {
@@ -226,7 +236,7 @@ function redactEnvSecretAssignments(text) {
226
236
  });
227
237
  }
228
238
  export function redactSensitiveText(text) {
229
- return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(redactManagedSessionRestoreKeys(text))))
239
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlParameterText(redactLooseUrlUserinfo(redactLooseUrlMatches(text))))
230
240
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
231
241
  .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
232
242
  }
@@ -323,20 +333,6 @@ export function getImplicitSessionIdleTimeoutMs(env = getAgentBrowserProcessEnvi
323
333
  function countExplicitGlobalFlags(args, targetFlag) {
324
334
  return scanUpstreamGlobalFlagOccurrences(args, targetFlag).length;
325
335
  }
326
- function getUnsupportedLeadingIdentityAssignment(args) {
327
- const commandStartIndex = findCommandStartIndex(args) ?? args.length;
328
- for (let index = 0; index < commandStartIndex; index += 1) {
329
- const token = args[index];
330
- if (token.startsWith("--session="))
331
- return "--session";
332
- if (token.startsWith("--namespace="))
333
- return "--namespace";
334
- const flag = token.split("=", 1)[0] ?? token;
335
- if (!token.includes("=") && PREVALIDATED_VALUE_FLAGS.has(flag))
336
- index += 1;
337
- }
338
- return undefined;
339
- }
340
336
  export function getImplicitSessionCloseTimeoutMs(env = getAgentBrowserProcessEnvironment()) {
341
337
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
342
338
  }
@@ -611,7 +607,7 @@ function getUnsupportedInlineWaitDownloadError(args) {
611
607
  return undefined;
612
608
  return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`wait --download=<path>\`. Pass the optional path as a separate argument: \`wait --download <path>\` (or \`wait -d <path>\`).`;
613
609
  }
614
- export function validateToolArgs(args) {
610
+ export function validateToolArgs(args, options = {}) {
615
611
  if (args.length === 0) {
616
612
  return "`args` must contain at least one agent-browser command token.";
617
613
  }
@@ -623,27 +619,30 @@ export function validateToolArgs(args) {
623
619
  if (sessionModeArg) {
624
620
  return "Do not pass `--session-mode` in args. Use the top-level agent_browser `sessionMode` field instead, for example { args: [\"--profile\", \"Default\", \"open\", \"https://example.com\"], sessionMode: \"fresh\" }.";
625
621
  }
622
+ const inspection = !options.batchStep && isPlainTextInspectionArgs(args);
623
+ const invalidValueFlag = inspection ? undefined : getInvalidValueFlagDetails(args, !options.batchStep);
624
+ if (invalidValueFlag?.reason === "unsupported-assignment")
625
+ return formatInvalidValueFlagError(invalidValueFlag, options.batchStep);
626
626
  return getBareMcpValidationError(args) ?? getSingleKeyCommandValidationError(args) ?? getUnsupportedInlineWaitDownloadError(args);
627
627
  }
628
- function getInvalidValueFlagDetails(args) {
628
+ function getInvalidValueFlagDetails(args, allowRestoreAssignment = true) {
629
629
  for (let index = 0; index < args.length; index += 1) {
630
630
  const token = args[index];
631
631
  if (!token.startsWith("-")) {
632
632
  continue;
633
633
  }
634
634
  const normalizedToken = token.split("=", 1)[0] ?? token;
635
- if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
636
- continue;
635
+ if (token.includes("=")
636
+ && (PREVALIDATED_VALUE_FLAGS.has(normalizedToken)
637
+ || GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken)
638
+ || (!allowRestoreAssignment && normalizedToken === "--restore"))) {
639
+ return {
640
+ flag: normalizedToken,
641
+ index,
642
+ reason: "unsupported-assignment",
643
+ };
637
644
  }
638
- if (token.includes("=")) {
639
- const value = token.slice(token.indexOf("=") + 1).trim();
640
- if (value.length === 0) {
641
- return {
642
- flag: normalizedToken,
643
- index,
644
- reason: "missing-value",
645
- };
646
- }
645
+ if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
647
646
  continue;
648
647
  }
649
648
  const receivedToken = args[index + 1];
@@ -666,7 +665,15 @@ function getInvalidValueFlagDetails(args) {
666
665
  }
667
666
  return undefined;
668
667
  }
669
- function formatInvalidValueFlagError(details) {
668
+ function formatInvalidValueFlagError(details, batchStep = false) {
669
+ if (details.reason === "unsupported-assignment") {
670
+ if (batchStep) {
671
+ return details.flag === "--restore"
672
+ ? "Global `--restore=<key>` belongs before `batch` in top-level args, not inside a batch step."
673
+ : `Global \`${details.flag}=<value>\` is not supported inside a batch step. Move \`${details.flag}\` and its value before \`batch\` as separate top-level args.`;
674
+ }
675
+ return `agent-browser ${TARGET_AGENT_BROWSER_VERSION} does not support \`${details.flag}=<value>\`. Pass \`${details.flag}\` and its value as separate arguments.`;
676
+ }
670
677
  if (details.reason === "unexpected-flag" && details.receivedToken) {
671
678
  return `Flag \`${details.flag}\` requires a value, but received \`${details.receivedToken}\` instead. Pass a non-flag value immediately after \`${details.flag}\`.`;
672
679
  }
@@ -779,7 +786,6 @@ export function getStartupScopedFlags(args) {
779
786
  }
780
787
  export function buildExecutionPlan(args, options) {
781
788
  const invalidValueFlag = getInvalidValueFlagDetails(args);
782
- const unsupportedIdentityAssignment = getUnsupportedLeadingIdentityAssignment(args);
783
789
  const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
784
790
  const explicitNamespace = extractExplicitNamespace(args);
785
791
  const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
@@ -811,16 +817,6 @@ export function buildExecutionPlan(args, options) {
811
817
  validationError: formatInvalidValueFlagError(invalidValueFlag),
812
818
  };
813
819
  }
814
- if (unsupportedIdentityAssignment) {
815
- return {
816
- commandInfo: {},
817
- effectiveArgs,
818
- plainTextInspection: false,
819
- startupScopedFlags: [],
820
- usedImplicitSession: false,
821
- validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser ${TARGET_AGENT_BROWSER_VERSION}. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
822
- };
823
- }
824
820
  for (const flag of ["--session", "--namespace"]) {
825
821
  if (countExplicitGlobalFlags(args, flag) <= 1)
826
822
  continue;
@@ -834,8 +830,13 @@ export function buildExecutionPlan(args, options) {
834
830
  };
835
831
  }
836
832
  const explicitSessionName = extractExplicitSessionName(args);
837
- if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
838
- namespace = resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
833
+ if (explicitSessionName && !explicitNamespacePresent) {
834
+ const targetsCurrentManagedSession = options.managedSessionActive
835
+ && getAgentBrowserSessionIdentityKey(explicitSessionName, managedSessionNamespace)
836
+ === getAgentBrowserSessionIdentityKey(options.managedSessionName, managedSessionNamespace);
837
+ namespace = targetsCurrentManagedSession
838
+ ? managedSessionNamespace
839
+ : resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
839
840
  }
840
841
  const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
841
842
  let argsToAppend = args;
@@ -1,7 +1,7 @@
1
1
  import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
3
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
- import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
4
+ import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand } from "./command-taxonomy.js";
5
5
  import { isRecord } from "./parsing.js";
6
6
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
7
7
  import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
@@ -210,6 +210,14 @@ export function buildPageTransitionRefSnapshotInvalidation(summary) {
210
210
  summary: summary ?? "A recording command (record start, or record restart with a URL) replaced or navigated the active page and invalidated the prior snapshot. Run snapshot -i before using page-scoped refs.",
211
211
  };
212
212
  }
213
+ export function getCommandRefSnapshotInvalidation(commandTokens) {
214
+ if (isRecordPageTransitionCommand(commandTokens))
215
+ return buildPageTransitionRefSnapshotInvalidation();
216
+ if (isWebMcpPageMutationCommand(commandTokens)) {
217
+ return buildPageTransitionRefSnapshotInvalidation("A WebMCP invoke, result, or cancel command can mutate, rerender, or navigate the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.");
218
+ }
219
+ return undefined;
220
+ }
213
221
  export function isNoActivePageSnapshotFailure(command, text) {
214
222
  return command === "snapshot" && /\bno active page\b/i.test(text ?? "");
215
223
  }
@@ -226,8 +234,9 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
226
234
  latestState = undefined;
227
235
  continue;
228
236
  }
229
- if (isRecordPageTransitionCommand(commandTokens)) {
230
- latestState = { invalidation: buildPageTransitionRefSnapshotInvalidation() };
237
+ const transitionInvalidation = getCommandRefSnapshotInvalidation(commandTokens);
238
+ if (transitionInvalidation) {
239
+ latestState = { invalidation: transitionInvalidation };
231
240
  continue;
232
241
  }
233
242
  if (name !== "snapshot")
@@ -8,7 +8,6 @@ import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process
8
8
  const TEMP_ROOT_PREFIX = "pi-agent-browser-";
9
9
  const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
10
10
  const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
11
- const TEMP_ROOT_LEGACY_MARKER_VERSION = 1;
12
11
  const TEMP_ROOT_MARKER_VERSION = 2;
13
12
  const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
14
13
  const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
@@ -39,7 +38,7 @@ function isProtectedTempChildName(value) {
39
38
  function isTempRootOwnershipRecord(value) {
40
39
  if (!isRecord(value))
41
40
  return false;
42
- if (value.kind !== TEMP_ROOT_MARKER_KIND || ![TEMP_ROOT_LEGACY_MARKER_VERSION, TEMP_ROOT_MARKER_VERSION].includes(value.version))
41
+ if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version !== TEMP_ROOT_MARKER_VERSION)
43
42
  return false;
44
43
  if (!isPositiveFiniteNumber(value.createdAtMs))
45
44
  return false;
@@ -1,14 +1,14 @@
1
- import { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL } from "../../../scripts/agent-browser-target.mjs";
2
- export { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL };
1
+ import { MINIMUM_AGENT_BROWSER_VERSION, MINIMUM_AGENT_BROWSER_VERSION_LABEL, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, isSupportedAgentBrowserVersion, } from "../../../scripts/agent-browser-target.mjs";
2
+ export { MINIMUM_AGENT_BROWSER_VERSION, MINIMUM_AGENT_BROWSER_VERSION_LABEL, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, isSupportedAgentBrowserVersion, };
3
3
  export function parseAgentBrowserVersionOutput(stdout) {
4
4
  const match = stdout.trim().match(/^agent-browser\s+(\S+)$/);
5
5
  return match?.[1];
6
6
  }
7
7
  export function getAgentBrowserVersionValidationError(stdout) {
8
8
  const observed = parseAgentBrowserVersionOutput(stdout);
9
- if (observed === TARGET_AGENT_BROWSER_VERSION)
9
+ if (observed && isSupportedAgentBrowserVersion(observed))
10
10
  return undefined;
11
11
  return observed
12
- ? `Installed agent-browser ${observed} does not match this extension's exact ${TARGET_AGENT_BROWSER_VERSION} capability baseline. Install ${TARGET_AGENT_BROWSER_VERSION_LABEL}, run pi-agent-browser-doctor, then reload Pi.`
13
- : `agent-browser --version returned an unrecognized value; expected exactly ${JSON.stringify(TARGET_AGENT_BROWSER_VERSION_LABEL)}. Run pi-agent-browser-doctor and install the exact supported upstream version.`;
12
+ ? `Installed agent-browser ${observed} is unsupported; stable versions must be at least ${MINIMUM_AGENT_BROWSER_VERSION_LABEL}. Install ${TARGET_AGENT_BROWSER_VERSION_LABEL} (recommended), run pi-agent-browser-doctor, then reload Pi.`
13
+ : `agent-browser --version returned an unrecognized value; expected ${SUPPORTED_AGENT_BROWSER_VERSION_LABEL}. Run pi-agent-browser-doctor and install a supported upstream version.`;
14
14
  }