pi-agent-browser-native 0.6.9 → 0.6.11

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 (54) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +62 -19
  3. package/dist/extensions/agent-browser/index.js +424 -451
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +7 -1
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
  7. package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
  10. package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
  11. package/dist/extensions/agent-browser/lib/managed-session-restore.js +42 -12
  12. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +3 -5
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +6 -14
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -25
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +12 -6
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +38 -31
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +60 -20
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
  23. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
  24. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
  25. package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
  26. package/dist/extensions/agent-browser/lib/playbook.js +13 -12
  27. package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
  28. package/dist/extensions/agent-browser/lib/process.js +8 -5
  29. package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
  30. package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
  32. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
  33. package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
  34. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +76 -57
  35. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
  36. package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -25
  37. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
  38. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
  39. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
  40. package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
  41. package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
  42. package/dist/extensions/agent-browser/lib/runtime.js +72 -20
  43. package/dist/extensions/agent-browser/lib/session-page-state.js +24 -8
  44. package/dist/extensions/agent-browser/lib/temp.js +4 -0
  45. package/dist/scripts/agent-browser-target.mjs +1 -1
  46. package/docs/ARCHITECTURE.md +29 -12
  47. package/docs/COMMAND_REFERENCE.md +67 -31
  48. package/docs/RELEASE.md +6 -4
  49. package/docs/SUPPORT_MATRIX.md +24 -16
  50. package/docs/TOOL_CONTRACT.md +82 -28
  51. package/package.json +1 -1
  52. package/scripts/agent-browser-capability-baseline.mjs +10 -3
  53. package/scripts/agent-browser-target.mjs +1 -1
  54. package/scripts/prepare.mjs +2 -4
@@ -1,9 +1,9 @@
1
1
  import { isRecord } from "../../parsing.js";
2
- import { isSensitiveFieldName, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
2
+ import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
4
4
  import { withOptionalSessionArgs } from "../next-actions.js";
5
5
  import { stringifyUnknown, truncateText } from "../text.js";
6
- import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
6
+ import { firstLine, formatCount, getArrayField, getStringField, redactModelFacingText, stringifyModelFacing, } from "./common.js";
7
7
  const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
8
8
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
9
9
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
@@ -185,11 +185,11 @@ export function formatDiagnosticSummary(commandInfo, data) {
185
185
  }
186
186
  if (commandInfo.subcommand === "route") {
187
187
  const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
188
- return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
188
+ return routed ? `Network route: ${redactSensitiveText(routed)}` : "Network route configured";
189
189
  }
190
190
  if (commandInfo.subcommand === "unroute") {
191
191
  const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
192
- return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
192
+ return unrouted ? `Network unroute: ${redactSensitiveText(unrouted)}` : "Network route removed";
193
193
  }
194
194
  if (commandInfo.subcommand === "har") {
195
195
  const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
@@ -273,14 +273,33 @@ function formatSessionText(data) {
273
273
  const metadata = [
274
274
  `active=${active ? "true" : "false"}`,
275
275
  label ? `label=${redactModelFacingText(label)}` : undefined,
276
- title ? `title=${redactModelFacingTextIfSensitive(title)}` : undefined,
277
- url ? `url=${redactModelFacingTextIfSensitive(url)}` : undefined,
276
+ title ? `title=${redactSensitiveText(title)}` : undefined,
277
+ url ? `url=${redactSensitiveText(url)}` : undefined,
278
278
  tabCount,
279
279
  ].filter(Boolean).join("; ");
280
280
  return `${index + 1}. name=${name}${active ? " *active*" : ""}; ${metadata}`;
281
281
  })
282
282
  .join("\n");
283
283
  }
284
+ if (typeof data.active === "boolean") {
285
+ const runtime = data.runtime;
286
+ const browser = isRecord(runtime) && isRecord(runtime.browser) ? runtime.browser : undefined;
287
+ const identity = [
288
+ `Daemon: ${data.active ? "active" : "inactive"}; PID: ${data.pid ?? "unknown"}`,
289
+ `Browser: ${browser?.status ?? "unknown"}; alive: ${browser?.alive ?? "unknown"}; Chrome PID: ${browser?.pid ?? "unknown"}`,
290
+ `Exact profile: ${browser?.userDataDir ?? "unknown"}`,
291
+ `Native browser ownership: ${browser?.ownership ?? "unknown"}; Pi cleanup ownership: ${data.piCleanupOwnership ?? "unknown"}`,
292
+ ].join("\n");
293
+ return `${redactModelFacingText(identity)}\n\n${stringifyModelFacing({
294
+ ...Object.fromEntries(["session", "namespace", "socketDir", "active", "pid", "version", "runtimeError"].map((key) => [key, data[key]])),
295
+ // Native runtime also includes restore check URLs, text and code; show status/identity only.
296
+ runtime: isRecord(runtime) ? Object.fromEntries([
297
+ "session", "namespace", "socketDir", "backgroundPid", "browserLaunched", "browser", "recording", "capabilities", "pageCount", "engine", "launchHash",
298
+ "compatibilityStatus", "restoreKey", "restoreStatus", "restoreLoadedPath", "restoreValidationPending",
299
+ "restoreSave", "saveStatus", "restoreSavedPath",
300
+ ].map((key) => [key, runtime[key]])) : runtime,
301
+ })}`;
302
+ }
284
303
  const session = getStringField(data, "session");
285
304
  return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
286
305
  }
@@ -315,8 +334,7 @@ function getPreviewCandidate(item, keys) {
315
334
  function formatNetworkPreviewValue(value, maxChars) {
316
335
  if (value === undefined || value === null)
317
336
  return undefined;
318
- const previewValue = typeof value === "string" ? parseJsonPreviewString(value) : value;
319
- const redacted = redactSensitiveValue(previewValue);
337
+ const redacted = redactSensitiveValue(value);
320
338
  const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
321
339
  const normalized = raw.replace(/\s+/g, " ").trim();
322
340
  if (normalized.length === 0)
@@ -797,7 +815,7 @@ function valueContainsStorageSecret(value) {
797
815
  return true;
798
816
  }
799
817
  catch { }
800
- if (redactSensitiveText(trimmed) !== trimmed || redactModelFacingTextIfSensitive(trimmed) !== trimmed)
818
+ if (redactSensitiveText(trimmed) !== trimmed)
801
819
  return true;
802
820
  try {
803
821
  return valueContainsStorageSecret(JSON.parse(trimmed));
@@ -830,13 +848,13 @@ function formatStorageValue(key, value) {
830
848
  }
831
849
  function redactStorageEntryValue(item) {
832
850
  if (!Object.hasOwn(item, "value"))
833
- return redactStructuredPresentationValue(item);
851
+ return redactSensitiveValue(item);
834
852
  const key = getStringField(item, "key") ?? getStringField(item, "name");
835
853
  const value = item.value;
836
854
  if (shouldRevealStorageValue(key, value))
837
- return redactStructuredPresentationValue(item);
855
+ return redactSensitiveValue(item);
838
856
  return {
839
- ...redactStructuredPresentationValue({ ...item, value: undefined }),
857
+ ...redactSensitiveValue({ ...item, value: undefined }),
840
858
  value: "[REDACTED]",
841
859
  valueRedacted: true,
842
860
  valueRedactionReason: key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value",
@@ -846,15 +864,15 @@ function redactStorageData(value) {
846
864
  if (Array.isArray(value))
847
865
  return value.map((item) => redactStorageData(item));
848
866
  if (!isRecord(value))
849
- return redactStructuredPresentationValue(value);
867
+ return redactSensitiveValue(value);
850
868
  const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
851
869
  if ((key === "entries" || key === "items") && Array.isArray(entryValue))
852
- return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactStructuredPresentationValue(item))];
870
+ return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactSensitiveValue(item))];
853
871
  if (key === "value") {
854
872
  const itemKey = getStringField(value, "key") ?? getStringField(value, "name");
855
- return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactStructuredPresentationValue(entryValue) : "[REDACTED]"];
873
+ return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactSensitiveValue(entryValue) : "[REDACTED]"];
856
874
  }
857
- return [key, redactStructuredPresentationValue(entryValue)];
875
+ return [key, redactSensitiveValue(entryValue)];
858
876
  }));
859
877
  if (Object.hasOwn(value, "value")) {
860
878
  const key = getStringField(value, "key") ?? getStringField(value, "name");
@@ -899,7 +917,7 @@ function formatDialogText(data) {
899
917
  lines.push(`Type: ${redactModelFacingText(type)}`);
900
918
  const message = getStringField(data, "message");
901
919
  if (message)
902
- lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
920
+ lines.push(`Message: ${redactModelFacingText(message)}`);
903
921
  if (data.accepted === true)
904
922
  lines.push("Accepted.");
905
923
  if (data.dismissed === true)
@@ -910,7 +928,7 @@ function formatFrameText(data) {
910
928
  const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
911
929
  const url = getStringField(data, "url");
912
930
  const title = getStringField(data, "title");
913
- const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
931
+ const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactSensitiveText(url)}` : undefined].filter(Boolean);
914
932
  return lines.length > 0 ? lines.join("\n") : undefined;
915
933
  }
916
934
  function formatStateText(data, subcommand) {
@@ -933,10 +951,10 @@ function formatStateText(data, subcommand) {
933
951
  return states
934
952
  .map((item, index) => {
935
953
  if (!isRecord(item))
936
- return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
954
+ return `${index + 1}. ${stringifyModelFacing(item)}`;
937
955
  const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
938
956
  const url = getStringField(item, "url");
939
- return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
957
+ return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactSensitiveText(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
940
958
  })
941
959
  .join("\n");
942
960
  }
@@ -946,23 +964,11 @@ function formatStateText(data, subcommand) {
946
964
  return "State cleared.";
947
965
  return undefined;
948
966
  }
949
- function redactStructuredPresentationValue(value) {
950
- if (typeof value === "string")
951
- return redactModelFacingTextIfSensitive(value);
952
- if (Array.isArray(value))
953
- return value.map((item) => redactStructuredPresentationValue(item));
954
- if (!isRecord(value))
955
- return value;
956
- return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
957
- key,
958
- isSensitiveFieldName(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
959
- ]));
960
- }
961
967
  function redactStatefulValues(value, sensitiveKeys) {
962
968
  if (Array.isArray(value))
963
969
  return value.map((item) => redactStatefulValues(item, sensitiveKeys));
964
970
  if (!isRecord(value))
965
- return redactStructuredPresentationValue(value);
971
+ return redactSensitiveValue(value);
966
972
  return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
967
973
  key,
968
974
  sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
@@ -973,13 +979,9 @@ export function redactPresentationData(commandInfo, data) {
973
979
  return redactStatefulValues(data, new Set(["value"]));
974
980
  if (commandInfo.command === "storage")
975
981
  return redactStorageData(data);
976
- if (commandInfo.command === "session" && commandInfo.subcommand === "list")
977
- return redactStructuredPresentationValue(data);
978
- if (commandInfo.command === "state" && commandInfo.subcommand === "list")
979
- return redactStructuredPresentationValue(data);
980
982
  if (commandInfo.command === "state" && commandInfo.subcommand === "show")
981
983
  return redactStatefulValues(data, new Set(["value"]));
982
- return redactStructuredPresentationValue(data);
984
+ return redactSensitiveValue(data);
983
985
  }
984
986
  export function formatDiagnosticText(commandInfo, data) {
985
987
  if (commandInfo.command === "session")
@@ -203,6 +203,7 @@ export function buildErrorPresentation(options) {
203
203
  overlayBlockedClick: isOverlayBlockedClickError(presentationCommand ?? commandInfo.command, safeErrorText, args ?? commandInfo.commandTokens),
204
204
  resultCategory: "failure",
205
205
  sessionName,
206
+ subcommand: commandInfo.subcommand,
206
207
  }) ?? []),
207
208
  ];
208
209
  return {
@@ -1,7 +1,7 @@
1
1
  import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { detectConfirmationRequired } from "../confirmation.js";
4
- import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
4
+ import { firstLine, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
5
5
  const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
6
6
  const GET_RESULT_FIELDS = {
7
7
  attr: "value",
@@ -25,7 +25,7 @@ function getScalarExtractionResult(commandInfo, data) {
25
25
  if (result === null || result === undefined)
26
26
  return "null";
27
27
  if (typeof result === "object")
28
- return JSON.stringify(result);
28
+ return JSON.stringify(result, null, 2);
29
29
  return undefined;
30
30
  }
31
31
  function getExtractionOrigin(data) {
@@ -52,7 +52,7 @@ export function formatExtractionSummary(commandInfo, data) {
52
52
  return undefined;
53
53
  }
54
54
  const safeScalarResult = redactModelFacingText(scalarResult);
55
- const firstResultLine = safeScalarResult.split("\n", 1)[0] ?? safeScalarResult;
55
+ const firstResultLine = firstLine(safeScalarResult);
56
56
  if (commandInfo.command === "get") {
57
57
  return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
58
58
  }
@@ -1,10 +1,11 @@
1
1
  import { isRecord } from "../parsing.js";
2
+ import { buildReadConfirmationNextActions, nextReadConfirmation } from "../read-confirmation.js";
2
3
  import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
3
4
  import { buildAgentBrowserNextActions } from "./action-recommendations.js";
4
5
  import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
5
6
  import { detectConfirmationRequired } from "./confirmation.js";
6
7
  import { buildSnapshotPresentation } from "./snapshot.js";
7
- import { parseJsonPreviewString, redactModelFacingText, stringifyModelFacing } from "./presentation/common.js";
8
+ import { redactModelFacingText } from "./presentation/common.js";
8
9
  import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts, classifyPresentationSuccessCategory, extractFileArtifacts, extractImagePath, formatArtifactMetadataLines, formatArtifactSummary, formatMissingArtifactFailureText, getSavedFileDetails, hasMissingFileArtifact, isManifestFileArtifact, } from "./presentation/artifacts.js";
9
10
  import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
10
11
  import { getPresentationPaths, isStringArray } from "./presentation/content.js";
@@ -18,8 +19,7 @@ function sanitizeModelFacingPresentation(presentation) {
18
19
  presentation.content = presentation.content.map((item) => {
19
20
  if (item.type !== "text")
20
21
  return item;
21
- const parsed = parseJsonPreviewString(item.text);
22
- return parsed === item.text ? item : { ...item, text: stringifyModelFacing(parsed) };
22
+ return { ...item, text: redactModelFacingText(item.text) };
23
23
  });
24
24
  presentation.summary = redactModelFacingText(presentation.summary);
25
25
  return presentation;
@@ -52,7 +52,10 @@ export async function buildToolPresentation(options) {
52
52
  const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
53
53
  const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
54
54
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
55
- if (errorText) {
55
+ const recordingCommand = commandInfo.command === "record";
56
+ const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
57
+ && envelope.data.some((row) => row.command?.[0] === "record");
58
+ if (errorText && !recordingCommand && !recordingBatch) {
56
59
  return buildErrorPresentation({
57
60
  args,
58
61
  commandInfo,
@@ -61,11 +64,15 @@ export async function buildToolPresentation(options) {
61
64
  sessionName,
62
65
  });
63
66
  }
64
- const data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
67
+ let data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
68
+ if (commandInfo.command === "session" && commandInfo.subcommand === "info" && isRecord(data)) {
69
+ data = { ...data, piCleanupOwnership: options.piCleanupOwnership ?? "unknown" };
70
+ }
71
+ const readConfirmation = nextReadConfirmation({ commandTokens: commandInfoWithTokens.commandTokens ?? [], data, namespace, sessionName: sessionName ?? "default", succeeded: envelope?.success !== false });
65
72
  const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
66
73
  ? redactBatchSpillData(data)
67
74
  : redactPresentationData(commandInfoWithTokens, data);
68
- const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, sessionName });
75
+ const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, recordingOutcome: recordingCommand ? envelope?.success : undefined, recordingPending: options.recordingPending, sessionName });
69
76
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
70
77
  const artifactSummary = formatArtifactSummary(artifacts);
71
78
  const summary = artifactSummary ?? formatPresentationSummary(commandInfoWithTokens, data, compiledSemanticAction);
@@ -83,6 +90,7 @@ export async function buildToolPresentation(options) {
83
90
  namespace,
84
91
  networkRoutes,
85
92
  persistentArtifactStore,
93
+ piCleanupOwnership: options.piCleanupOwnership,
86
94
  sessionName,
87
95
  summary,
88
96
  });
@@ -99,6 +107,12 @@ export async function buildToolPresentation(options) {
99
107
  summary,
100
108
  };
101
109
  }
110
+ if (errorText && (recordingCommand || recordingBatch)) {
111
+ const errorPresentation = buildErrorPresentation({ args, commandInfo, errorText, presentationCommand: presentationCommandInfo.command, sessionName });
112
+ presentation = { ...presentation, resultCategory: "failure", failureCategory: errorPresentation.failureCategory, summary: errorPresentation.summary,
113
+ content: [{ type: "text", text: `${errorPresentation.content[0]?.type === "text" ? errorPresentation.content[0].text : errorText}\n\n${presentation.content[0]?.type === "text" ? presentation.content[0].text : ""}` }],
114
+ };
115
+ }
102
116
  if (networkRouteDiagnostics && networkRouteDiagnostics.length > 0 && presentation.content[0]?.type === "text") {
103
117
  const diagnosticText = formatNetworkRouteDiagnosticsText(networkRouteDiagnostics);
104
118
  if (diagnosticText)
@@ -141,7 +155,7 @@ export async function buildToolPresentation(options) {
141
155
  presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(artifacts, presentationWithManifest.artifactManifest, currentSpillPaths) ?? presentationWithManifest.artifactVerification;
142
156
  const confirmationRequired = detectConfirmationRequired(data);
143
157
  const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
144
- if (missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
158
+ if (!errorText && missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
145
159
  presentationWithManifest.resultCategory = "failure";
146
160
  presentationWithManifest.failureCategory = "artifact-missing";
147
161
  presentationWithManifest.successCategory = undefined;
@@ -153,6 +167,21 @@ export async function buildToolPresentation(options) {
153
167
  presentationWithManifest.content.unshift({ type: "text", text: missingArtifactFailureText });
154
168
  }
155
169
  }
170
+ const failedRecording = presentationWithManifest.artifacts?.find((artifact) => artifact.recording?.success === false || artifact.recording?.output.encoderSucceeded === false);
171
+ if (failedRecording && !errorText) {
172
+ const failure = `Recording failed: ${failedRecording.recording?.error ?? "native capture/encoder failure"}`;
173
+ presentationWithManifest.resultCategory = "failure";
174
+ presentationWithManifest.failureCategory = "upstream-error";
175
+ presentationWithManifest.successCategory = undefined;
176
+ presentationWithManifest.summary = failure;
177
+ presentationWithManifest.content.unshift({ type: "text", text: failure });
178
+ }
179
+ if (readConfirmation?.state === "pending") {
180
+ presentationWithManifest.readConfirmation = readConfirmation;
181
+ presentationWithManifest.resultCategory = "failure";
182
+ presentationWithManifest.failureCategory = "confirmation-required";
183
+ presentationWithManifest.successCategory = undefined;
184
+ }
156
185
  if (!presentationWithManifest.resultCategory) {
157
186
  const categoryDetails = buildAgentBrowserResultCategoryDetails({
158
187
  artifacts: presentationWithManifest.artifacts,
@@ -160,7 +189,7 @@ export async function buildToolPresentation(options) {
160
189
  confirmationRequired: confirmationRequired !== undefined,
161
190
  errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
162
191
  savedFile: presentationWithManifest.savedFile,
163
- succeeded: envelope?.success !== false,
192
+ succeeded: envelope?.success !== false && confirmationRequired === undefined,
164
193
  });
165
194
  presentationWithManifest.resultCategory = categoryDetails.resultCategory;
166
195
  presentationWithManifest.successCategory = categoryDetails.resultCategory === "success"
@@ -196,7 +225,7 @@ export async function buildToolPresentation(options) {
196
225
  ? buildNetworkRequestsNextActions(data, sessionName, presentationWithManifest.networkRouteDiagnostics)
197
226
  : undefined;
198
227
  const streamNextActions = presentationWithManifest.resultCategory === "success" ? buildStreamNextActions(commandInfoWithTokens, data, sessionName) : undefined;
199
- presentationWithManifest.nextActions = mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
228
+ presentationWithManifest.nextActions = readConfirmation ? buildReadConfirmationNextActions(readConfirmation, true) : mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
200
229
  presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
201
230
  artifacts: presentationWithManifest.artifacts,
202
231
  commandInfo: presentationCommandInfo,
@@ -0,0 +1,50 @@
1
+ import { isRecord } from "../parsing.js";
2
+ const RECORDING_QUALITY_WARNING = "Capture quality warning: repaint-driven capture, held/repeated or static frames, and late/final-state-only frames cannot establish UI smoothness. Output FPS is not captured-frame rate; inspect the recording and capture window before judging motion.";
3
+ function number(value) {
4
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
5
+ }
6
+ function text(value) {
7
+ return typeof value === "string" && value.length > 0 ? value : null;
8
+ }
9
+ export function getRecordingReceipt(value, outcome) {
10
+ if (!isRecord(value) || typeof value.path !== "string" || !value.path)
11
+ return undefined;
12
+ const capture = isRecord(value.capture) ? value.capture : {};
13
+ const output = isRecord(value.output) ? value.output : {};
14
+ const file = isRecord(value.file) ? value.file : {};
15
+ return {
16
+ warning: RECORDING_QUALITY_WARNING,
17
+ recordingId: text(value.recordingId), path: value.path,
18
+ success: typeof value.success === "boolean" ? value.success : value.success === null ? null : outcome ?? null,
19
+ error: text(value.error), frames: number(value.frames), capturedFrames: number(value.capturedFrames), fps: number(value.fps),
20
+ capture: {
21
+ startedAt: text(capture.startedAt), endedAt: text(capture.endedAt), durationMs: number(capture.durationMs),
22
+ firstFrameAt: text(capture.firstFrameAt), lastFrameAt: text(capture.lastFrameAt), firstFrameAfterMs: number(capture.firstFrameAfterMs), lastFrameAfterMs: number(capture.lastFrameAfterMs),
23
+ averageFps: number(capture.averageFps), maxFrameGapMs: number(capture.maxFrameGapMs), timestampSource: text(capture.timestampSource),
24
+ },
25
+ output: {
26
+ frames: number(output.frames ?? value.frames), fps: number(output.fps ?? value.fps), encodedFrames: number(output.encodedFrames),
27
+ durationMs: number(output.durationMs), durationSource: text(output.durationSource), heldFrames: number(output.heldFrames), droppedFrames: number(output.droppedFrames), skippedFrames: number(output.skippedFrames),
28
+ encoderSucceeded: typeof output.encoderSucceeded === "boolean" ? output.encoderSucceeded : null,
29
+ },
30
+ file: { exists: typeof file.exists === "boolean" ? file.exists : null, sizeBytes: number(file.sizeBytes) },
31
+ };
32
+ }
33
+ export function formatRecordingReceipt(receipt) {
34
+ const metric = (value, unit = "") => value === null ? "unknown" : `${value}${unit}`;
35
+ return [
36
+ `Recording ID: ${metric(receipt.recordingId)}`,
37
+ `Native recording outcome: ${receipt.success === true ? "succeeded" : receipt.success === false ? "failed" : "pending/unknown"}${receipt.error ? ` — ${receipt.error}` : ""}`,
38
+ `Capture started: ${metric(receipt.capture.startedAt)}; ended: ${metric(receipt.capture.endedAt)}`,
39
+ `Wall-clock capture duration: ${metric(receipt.capture.durationMs, " ms")}`,
40
+ `Captured frames (received, not pixel-unique): ${metric(receipt.capturedFrames)}`,
41
+ `Captured-frame rate: ${metric(receipt.capture.averageFps, " fps")}`,
42
+ `First frame: ${metric(receipt.capture.firstFrameAt)}; after start: ${metric(receipt.capture.firstFrameAfterMs, " ms")}`,
43
+ `Last frame: ${metric(receipt.capture.lastFrameAt)}; after start: ${metric(receipt.capture.lastFrameAfterMs, " ms")}`,
44
+ `Maximum frame gap: ${metric(receipt.capture.maxFrameGapMs, " ms")}; timestamp source: ${metric(receipt.capture.timestampSource)}`,
45
+ `Written frames: ${metric(receipt.output.frames)}; Encoded frames: ${metric(receipt.output.encodedFrames)}`,
46
+ `Held frames: ${metric(receipt.output.heldFrames)}; dropped frames: ${metric(receipt.output.droppedFrames)}; skipped frames: ${metric(receipt.output.skippedFrames)}`,
47
+ `Nominal/output FPS: ${metric(receipt.output.fps)}; output duration (encoded frames/fps): ${metric(receipt.output.durationMs, " ms")}`,
48
+ receipt.warning,
49
+ ].join("\n");
50
+ }
@@ -20,7 +20,7 @@ const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
20
20
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
21
21
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
22
22
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
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;
23
+ const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|authorization(?:_|-)?session(?:_|-)?id|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
24
  const AUTH_STATE_QUERY_PARAM_PATTERN = /^(?:nonce|state)$/i;
25
25
  const AUTH_URL_CONTEXT_PATTERN = /(?:^|[./_-])(?:auth|authorize|callback|login|oauth2?|oidc|saml|sso)(?:[./?#_-]|$)/i;
26
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;
@@ -50,6 +50,7 @@ function redactUrlToken(token) {
50
50
  catch {
51
51
  return token;
52
52
  }
53
+ const originalHref = parsed.href;
53
54
  if (parsed.username.length > 0)
54
55
  parsed.username = "[REDACTED]";
55
56
  if (parsed.password.length > 0)
@@ -74,7 +75,7 @@ function redactUrlToken(token) {
74
75
  if (hashMutated)
75
76
  parsed.hash = `#${hashParams.toString()}`;
76
77
  }
77
- return parsed.toString();
78
+ return parsed.href === originalHref ? token : parsed.href;
78
79
  }
79
80
  function redactLooseUrlParameterText(text) {
80
81
  return text.replace(/(?<![^\s"'`<>\])}])[^\s"'`<>\])}]*[?#&][^\s"'`<>\])}]*/g, (token) => {
@@ -160,6 +161,52 @@ function findBalancedJsonEnd(text, startIndex) {
160
161
  }
161
162
  return undefined;
162
163
  }
164
+ function redactSerializedJson(text) {
165
+ // Validate grammar only; rebuilding parsed values loses duplicates and numeric spelling.
166
+ try {
167
+ JSON.parse(text);
168
+ }
169
+ catch {
170
+ return undefined;
171
+ }
172
+ let output = "";
173
+ let cursor = 0;
174
+ const strings = /"(?:\\.|[^"\\])*"/g;
175
+ let match;
176
+ while ((match = strings.exec(text)) !== null) {
177
+ const end = strings.lastIndex;
178
+ const value = JSON.parse(match[0]);
179
+ const redacted = redactSensitiveText(value);
180
+ if (redacted !== value) {
181
+ output += text.slice(cursor, match.index) + JSON.stringify(redacted);
182
+ cursor = end;
183
+ }
184
+ const separator = /^\s*:\s*/.exec(text.slice(end));
185
+ if (!separator || !isSensitiveFieldName(value))
186
+ continue;
187
+ const valueStart = end + separator[0].length;
188
+ let valueEnd = findBalancedJsonEnd(text, valueStart);
189
+ if (valueEnd !== undefined) {
190
+ valueEnd += 1;
191
+ }
192
+ else if (text[valueStart] === '"') {
193
+ strings.lastIndex = valueStart;
194
+ const fieldValue = strings.exec(text);
195
+ valueEnd = strings.lastIndex;
196
+ if (JSON.parse(fieldValue[0]) === "[REDACTED]")
197
+ continue;
198
+ }
199
+ else {
200
+ valueEnd = valueStart;
201
+ while (valueEnd < text.length && !/[\s,\]}]/.test(text[valueEnd]))
202
+ valueEnd += 1;
203
+ }
204
+ output += text.slice(cursor, valueStart) + '"[REDACTED]"';
205
+ cursor = valueEnd;
206
+ strings.lastIndex = valueEnd;
207
+ }
208
+ return output + text.slice(cursor);
209
+ }
163
210
  function redactEmbeddedStructuredText(text) {
164
211
  let output = "";
165
212
  let cursor = 0;
@@ -177,15 +224,7 @@ function redactEmbeddedStructuredText(text) {
177
224
  continue;
178
225
  }
179
226
  const candidate = text.slice(cursor, endIndex + 1);
180
- try {
181
- const parsed = JSON.parse(candidate);
182
- const redacted = typeof parsed === "string" ? redactSensitiveText(parsed) : JSON.stringify(redactSensitiveValue(parsed));
183
- const original = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
184
- output += redacted === original ? candidate : redacted;
185
- }
186
- catch {
187
- output += candidate;
188
- }
227
+ output += redactSerializedJson(candidate) ?? candidate;
189
228
  cursor = endIndex + 1;
190
229
  }
191
230
  return output;
@@ -198,21 +237,22 @@ function redactStandaloneBasicCredential(text) {
198
237
  });
199
238
  }
200
239
  function credentialTrailingPunctuation(credential) {
201
- return credential.match(/^(.+?)([,.]+)$/)?.[2] ?? "";
202
- }
203
- function isBearerHelpPlaceholder(label, credential, trailing) {
204
- return label.toLowerCase() === "authorization bearer" && credential.toLowerCase() === "token" && trailing === ")";
240
+ return credential.match(/[,.:;!?]+$/)?.[0] ?? "";
205
241
  }
206
242
  function formatRedactedCredential(label, credential, trailing = "") {
207
243
  return `${label} [REDACTED]${credentialTrailingPunctuation(credential)}${trailing}`;
208
244
  }
209
245
  function redactBearerCredentials(text) {
210
246
  return text
211
- .replace(/\b(Authorization\s*:\s*Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (_match, label, credential, trailing) => {
247
+ .replace(/((?:\b([A-Za-z][A-Za-z0-9_-]*)\s*[:=]\s*|(?:^|\s)(?:-H\s*|--header(?:\s+|=)))["']?Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (match, label, field, credential, trailing) => {
248
+ if (field && !isSensitiveFieldName(field))
249
+ return match;
212
250
  return formatRedactedCredential(label, credential, trailing);
213
251
  })
214
- .replace(/\b((?:Authorization\s+)?Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
215
- if (isBearerHelpPlaceholder(label, credential, trailing))
252
+ .replace(/\b(Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
253
+ // Without a credential field/header, require a bearer-token shape, not prose, HTML or a URL.
254
+ const token = credential.slice(0, credential.length - credentialTrailingPunctuation(credential).length);
255
+ if (!/^[A-Za-z0-9._~+/-]+=*$/.test(token) || !/[0-9._~+/=-]/.test(token))
216
256
  return match;
217
257
  return formatRedactedCredential(label, credential, trailing);
218
258
  });
@@ -236,7 +276,13 @@ function redactEnvSecretAssignments(text) {
236
276
  });
237
277
  }
238
278
  export function redactSensitiveText(text) {
239
- return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlParameterText(redactLooseUrlUserinfo(redactLooseUrlMatches(text))))
279
+ // Redact JSON string literals before text heuristics can consume their escapes.
280
+ // Non-JSON stays whole so assignments and headers retain their credential context.
281
+ const serialized = redactSerializedJson(text);
282
+ if (serialized !== undefined)
283
+ return serialized;
284
+ const embeddedRedactedText = redactEmbeddedStructuredText(text);
285
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlParameterText(redactLooseUrlUserinfo(redactLooseUrlMatches(embeddedRedactedText))))
240
286
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
241
287
  .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
242
288
  }
@@ -744,6 +790,8 @@ export function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
744
790
  return DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM[platform] ?? FALLBACK_HEADLESS_COMPAT_USER_AGENT;
745
791
  }
746
792
  export function canUseHeadlessCompatibilityUserAgent(args, env = getAgentBrowserProcessEnvironment()) {
793
+ if (env.AGENT_BROWSER_SESSION !== undefined)
794
+ return false;
747
795
  if (hasFlagToken(args, "--user-agent") || hasFlagToken(args, "--args"))
748
796
  return false;
749
797
  if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p"))
@@ -798,6 +846,9 @@ export function getStartupScopedFlags(args) {
798
846
  .filter((flag) => hasLaunchScopedFlagToken(args, flag));
799
847
  }
800
848
  export function buildExecutionPlan(args, options) {
849
+ const nativeSession = getAgentBrowserProcessEnvironment().AGENT_BROWSER_SESSION;
850
+ if (nativeSession !== undefined && !isPlainTextInspectionArgs(args) && extractExplicitSessionName(args) === undefined)
851
+ args = ["--session", nativeSession, ...args];
801
852
  const invalidValueFlag = getInvalidValueFlagDetails(args);
802
853
  const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
803
854
  const explicitNamespace = extractExplicitNamespace(args);
@@ -806,7 +857,7 @@ export function buildExecutionPlan(args, options) {
806
857
  const plainTextInspection = isPlainTextInspectionArgs(args);
807
858
  const argvDescriptor = parseArgvDescriptor(args);
808
859
  const commandInfo = argvDescriptor.commandInfo;
809
- const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
860
+ const commandNeedsManagedSession = !plainTextInspection && !options.browserIndependentReadConfirmation && needsManagedSession(argvDescriptor, options.stdin);
810
861
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
811
862
  let namespace = explicitNamespacePresent ? explicitNamespace ?? "" : undefined;
812
863
  if (plainTextInspection) {
@@ -902,6 +953,7 @@ export function buildExecutionPlan(args, options) {
902
953
  namespace = resolveAgentBrowserNamespace(args, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
903
954
  }
904
955
  const targetsActiveManagedSession = options.managedSessionActive
956
+ && commandNeedsManagedSession
905
957
  && sessionName
906
958
  && getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace);
907
959
  if (targetsActiveManagedSession && startupScopedFlags.length > 0 && !isCloseCommand(commandInfo.command) && !validationError) {