pi-agent-browser-native 0.2.42 → 0.2.43

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 (31) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +8 -8
  3. package/docs/COMMAND_REFERENCE.md +9 -10
  4. package/docs/SUPPORT_MATRIX.md +6 -5
  5. package/docs/TOOL_CONTRACT.md +27 -24
  6. package/extensions/agent-browser/index.ts +71 -2
  7. package/extensions/agent-browser/lib/input-modes/params.ts +1 -1
  8. package/extensions/agent-browser/lib/input-modes/types.ts +1 -1
  9. package/extensions/agent-browser/lib/navigation-policy.ts +95 -0
  10. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +2 -7
  11. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +1 -0
  12. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +2 -2
  13. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +103 -12
  14. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +20 -3
  15. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +6 -1
  16. package/extensions/agent-browser/lib/playbook.ts +4 -4
  17. package/extensions/agent-browser/lib/results/action-recommendations.ts +15 -0
  18. package/extensions/agent-browser/lib/results/contracts.ts +17 -0
  19. package/extensions/agent-browser/lib/results/network-routes.ts +80 -0
  20. package/extensions/agent-browser/lib/results/network.ts +10 -2
  21. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +14 -0
  22. package/extensions/agent-browser/lib/results/presentation/batch.ts +36 -13
  23. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +154 -16
  24. package/extensions/agent-browser/lib/results/presentation/errors.ts +62 -2
  25. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +2 -4
  26. package/extensions/agent-browser/lib/results/presentation.ts +31 -1
  27. package/extensions/agent-browser/lib/results/selector-recovery.ts +11 -3
  28. package/extensions/agent-browser/lib/results/shared.ts +1 -0
  29. package/extensions/agent-browser/lib/results.ts +3 -0
  30. package/extensions/agent-browser/lib/runtime.ts +6 -0
  31. package/package.json +1 -1
@@ -11,15 +11,18 @@ import type {
11
11
  AgentBrowserNextAction,
12
12
  BatchFailurePresentationDetails,
13
13
  BatchStepPresentationDetails,
14
+ NetworkRouteDiagnostic,
15
+ NetworkRouteRecord,
14
16
  SessionArtifactManifest,
15
17
  ToolPresentation,
16
18
  } from "../contracts.js";
19
+ import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
17
20
  import { withOptionalSessionArgs } from "../next-actions.js";
18
21
  import { stringifyModelFacing } from "./common.js";
19
22
  import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries, type ArtifactRequestContext } from "./artifacts.js";
20
23
  import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
21
24
  import { buildPageChangeSummary } from "./navigation.js";
22
- import { appendSelectorRecoveryHint } from "./errors.js";
25
+ import { appendSelectorRecoveryHint, getClipboardWritePayloadCandidates, redactClipboardPermissionErrorValue } from "./errors.js";
23
26
 
24
27
  export interface BuildNestedToolPresentationOptions {
25
28
  artifactManifest?: SessionArtifactManifest;
@@ -28,6 +31,7 @@ export interface BuildNestedToolPresentationOptions {
28
31
  commandInfo: CommandInfo;
29
32
  cwd: string;
30
33
  envelope?: AgentBrowserEnvelope;
34
+ networkRouteDiagnostics?: NetworkRouteDiagnostic[];
31
35
  persistentArtifactStore?: PersistentSessionArtifactStore;
32
36
  sessionName?: string;
33
37
  }
@@ -120,16 +124,19 @@ async function buildBatchStepPresentation(options: {
120
124
  cwd: string;
121
125
  index: number;
122
126
  item: AgentBrowserBatchResult;
127
+ networkRoutes?: NetworkRouteRecord[];
123
128
  persistentArtifactStore?: PersistentSessionArtifactStore;
124
129
  sessionName?: string;
125
130
  }): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
126
- const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, persistentArtifactStore, sessionName } = options;
131
+ const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, networkRoutes, persistentArtifactStore, sessionName } = options;
127
132
  const command = isStringArray(item.command) ? item.command : undefined;
128
133
  const redactedCommand = command ? redactInvocationArgs(command) : undefined;
129
134
  const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
130
135
 
131
136
  if (item.success === false) {
132
- const redactedErrorData = redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
137
+ const redactedErrorData = command?.[0] === "clipboard"
138
+ ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
139
+ : redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
133
140
  const errorText = formatBatchStepError(redactedErrorData);
134
141
  const failureCategory = classifyAgentBrowserFailureCategory({
135
142
  args: command,
@@ -173,13 +180,18 @@ async function buildBatchStepPresentation(options: {
173
180
  };
174
181
  }
175
182
 
183
+ const commandInfo = parseCommandInfo(command ?? []);
184
+ const networkRouteDiagnostics = commandInfo.command === "network" && commandInfo.subcommand === "requests"
185
+ ? buildNetworkRouteDiagnostics(item.result, networkRoutes)
186
+ : undefined;
176
187
  const presentation = await buildNestedToolPresentation({
177
188
  artifactManifest,
178
189
  artifactRequest,
179
- commandInfo: parseCommandInfo(command ?? []),
190
+ commandInfo,
180
191
  cwd,
181
192
  args: command,
182
193
  envelope: { data: item.result, success: true },
194
+ networkRouteDiagnostics,
183
195
  persistentArtifactStore,
184
196
  sessionName,
185
197
  });
@@ -192,17 +204,19 @@ async function buildBatchStepPresentation(options: {
192
204
  secondaryPaths: presentation.imagePaths,
193
205
  });
194
206
  const text = getPresentationText(presentation) || presentation.summary;
207
+ const stepSucceeded = presentation.resultCategory !== "failure";
195
208
  const nextActions = presentation.nextActions ?? buildAgentBrowserNextActions({
196
209
  artifacts: presentation.artifacts,
197
210
  args: command,
198
211
  command: command?.[0],
199
- resultCategory: "success",
212
+ failureCategory: presentation.failureCategory,
213
+ resultCategory: stepSucceeded ? "success" : "failure",
200
214
  savedFilePath: presentation.savedFilePath,
201
215
  successCategory: presentation.successCategory,
202
216
  });
203
217
  const pageChangeSummary = buildPageChangeSummary({
204
218
  artifacts: presentation.artifacts,
205
- commandInfo: parseCommandInfo(command ?? []),
219
+ commandInfo,
206
220
  data: presentation.data,
207
221
  nextActions,
208
222
  savedFilePath: presentation.savedFilePath,
@@ -216,18 +230,20 @@ async function buildBatchStepPresentation(options: {
216
230
  command: redactedCommand,
217
231
  commandText,
218
232
  data: presentation.data,
233
+ failureCategory: stepSucceeded ? undefined : presentation.failureCategory,
219
234
  fullOutputPath: fullOutputPaths[0],
220
235
  fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
221
236
  imagePath: imagePaths[0],
222
237
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
223
238
  index,
239
+ networkRouteDiagnostics: presentation.networkRouteDiagnostics,
224
240
  nextActions,
225
241
  pageChangeSummary,
226
- resultCategory: "success",
242
+ resultCategory: stepSucceeded ? "success" : "failure",
227
243
  savedFile: presentation.savedFile,
228
244
  savedFilePath: presentation.savedFilePath,
229
- success: true,
230
- successCategory: classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }),
245
+ success: stepSucceeded,
246
+ successCategory: stepSucceeded ? classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }) : undefined,
231
247
  summary: presentation.summary,
232
248
  text,
233
249
  },
@@ -241,14 +257,16 @@ export async function buildBatchPresentation(options: {
241
257
  buildNestedToolPresentation: BuildNestedToolPresentation;
242
258
  cwd: string;
243
259
  data: AgentBrowserBatchResult[];
260
+ networkRoutes?: NetworkRouteRecord[];
244
261
  persistentArtifactStore?: PersistentSessionArtifactStore;
245
262
  sessionName?: string;
246
263
  summary: string;
247
264
  }): Promise<ToolPresentation> {
248
- const { artifactRequests, buildNestedToolPresentation, cwd, data, persistentArtifactStore, sessionName, summary } = options;
265
+ const { artifactRequests, buildNestedToolPresentation, cwd, data, networkRoutes, persistentArtifactStore, sessionName, summary } = options;
249
266
  const steps: Array<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> = [];
250
267
  const protectedPersistentPaths: string[] = [];
251
268
  let currentArtifactManifest = options.artifactManifest;
269
+ let currentNetworkRoutes = networkRoutes;
252
270
  for (const [index, item] of data.entries()) {
253
271
  const step = await buildBatchStepPresentation({
254
272
  artifactManifest: currentArtifactManifest,
@@ -257,11 +275,13 @@ export async function buildBatchPresentation(options: {
257
275
  cwd,
258
276
  index,
259
277
  item,
278
+ networkRoutes: currentNetworkRoutes,
260
279
  persistentArtifactStore: persistentArtifactStore ? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths } : undefined,
261
280
  sessionName,
262
281
  });
263
282
  steps.push(step);
264
283
  currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
284
+ currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
265
285
  protectedPersistentPaths.push(
266
286
  ...getPresentationPaths({
267
287
  primaryPath: step.presentation.fullOutputPath,
@@ -299,10 +319,13 @@ export async function buildBatchPresentation(options: {
299
319
  return lines.join("\n");
300
320
  })
301
321
  .join("\n\n");
322
+ const batchSummary = batchFailure === undefined
323
+ ? summary
324
+ : `Batch failed: ${batchFailure.successCount}/${batchFailure.totalCount} succeeded`;
302
325
  const failureHeader = batchFailure === undefined
303
326
  ? undefined
304
327
  : [
305
- summary,
328
+ batchSummary,
306
329
  `First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
307
330
  batchFailure.failureCount > 1 ? `${batchFailure.failureCount} steps failed. See the per-step results below.` : "See the per-step results below.",
308
331
  ].join("\n");
@@ -321,7 +344,7 @@ export async function buildBatchPresentation(options: {
321
344
  commandInfo: { command: "batch" },
322
345
  data,
323
346
  nextActions,
324
- summary,
347
+ summary: batchSummary,
325
348
  })
326
349
  : changedSteps.length > 0
327
350
  ? {
@@ -350,6 +373,6 @@ export async function buildBatchPresentation(options: {
350
373
  pageChangeSummary,
351
374
  resultCategory: batchFailure ? "failure" : "success",
352
375
  successCategory: batchFailure ? undefined : classifyPresentationSuccessCategory({ artifactVerification, artifacts }),
353
- summary,
376
+ summary: batchSummary,
354
377
  };
355
378
  }
@@ -6,8 +6,8 @@
6
6
 
7
7
  import { isRecord } from "../../parsing.js";
8
8
  import { redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../../runtime.js";
9
- import type { AgentBrowserNextAction } from "../contracts.js";
10
- import { classifyNetworkRequestFailure, summarizeNetworkFailures } from "../network.js";
9
+ import type { AgentBrowserNextAction, NetworkRouteDiagnostic } from "../contracts.js";
10
+ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, summarizeNetworkFailures } from "../network.js";
11
11
  import { withOptionalSessionArgs } from "../next-actions.js";
12
12
  import { stringifyUnknown, truncateText } from "../text.js";
13
13
  import {
@@ -29,10 +29,24 @@ const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
29
29
 
30
30
  const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
31
31
 
32
- const NETWORK_NEXT_ACTION_LIMIT = 4;
32
+ const NETWORK_NEXT_ACTION_LIMIT = 6;
33
33
 
34
34
  const NETWORK_FILTER_MAX_CHARS = 160;
35
35
 
36
+ const STORAGE_VALUE_PREVIEW_MAX_CHARS = 160;
37
+
38
+ const STORAGE_SECRET_KEY_PATTERN = /(?:access(?:_|-)?token|account|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|credential|csrf|email|id(?:_|-)?token|jwt|pass(?:word)?|private(?:_|-)?key|profile|refresh(?:_|-)?token|secret|session|sid|sig(?:nature)?|token|user(?:name)?|x(?:_|-)?api(?:_|-)?key|xsrf)/i;
39
+
40
+ const STORAGE_BENIGN_KEY_PATTERN = /^(?:color(?:scheme)?|debug|dev|experiment|feature(?:flag)?|flag|language|layout|locale|mode|onboarding|sort|tab|theme|timezone|tour|variant|view)$/i;
41
+
42
+ const STORAGE_TOKEN_VALUE_PATTERN = /(?:\bBearer\s+[A-Za-z0-9._~-]+|\bBasic\s+[A-Za-z0-9+/=]+|^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_~+/=-]{32,})/;
43
+
44
+ const STORAGE_SECRET_VALUE_WORD_PATTERN = /(?:secret|token|password|passwd|bearer|credential|authorization|cookie|session[-_ ]?id)/i;
45
+
46
+ const STORAGE_EMAIL_VALUE_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
47
+
48
+ const STORAGE_IDENTITY_VALUE_PATTERN = /(?:^|[\s:=/_-])(?:account|profile|session|sid|user(?:id|name)?)(?:[\s:=/_-]|$)/i;
49
+
36
50
  const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = [
37
51
  "apikey",
38
52
  "api-key",
@@ -86,6 +100,15 @@ export function getTabSummary(data: Record<string, unknown>): string | undefined
86
100
  }
87
101
 
88
102
  export function getStreamSummary(data: Record<string, unknown>): string | undefined {
103
+ if (data.alreadyEnabled === true) {
104
+ const lines = ["Stream already enabled (idempotent no-op)."];
105
+ if (typeof data.port === "number") {
106
+ lines.push(`Port: ${data.port}`);
107
+ lines.push(`WebSocket URL: ${getStreamWebSocketUrl(data.port)}`);
108
+ }
109
+ lines.push("Run stream status for current connection details or stream disable when streaming is no longer needed.");
110
+ return lines.join("\n");
111
+ }
89
112
  if (typeof data.enabled !== "boolean" || typeof data.connected !== "boolean") {
90
113
  return undefined;
91
114
  }
@@ -211,6 +234,7 @@ export function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<s
211
234
 
212
235
  if (commandInfo.command === "stream") {
213
236
  if (commandInfo.subcommand === "enable") {
237
+ if (data.alreadyEnabled === true) return "Stream already enabled";
214
238
  const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
215
239
  return `Stream enabled${port}`;
216
240
  }
@@ -416,14 +440,6 @@ function getNetworkRequestPathFilter(item: Record<string, unknown>): string | un
416
440
  return getSafeNetworkActionValue(filter);
417
441
  }
418
442
 
419
- function isApiLikeNetworkRequest(item: Record<string, unknown>): boolean {
420
- const method = (getStringField(item, "method") ?? "GET").toUpperCase();
421
- const resourceType = (getStringField(item, "resourceType") ?? "").toLowerCase();
422
- const mimeType = (getStringField(item, "mimeType") ?? "").toLowerCase();
423
- const filter = getNetworkRequestPathFilter(item) ?? "";
424
- return resourceType === "fetch" || resourceType === "xhr" || mimeType.includes("json") || /\/(?:api|graphql|rpc)(?:\/|$)/i.test(filter) || !["GET", "HEAD"].includes(method);
425
- }
426
-
427
443
  function getNetworkRequestActionCandidate(item: Record<string, unknown>): NetworkRequestActionCandidate | undefined {
428
444
  const requestId = getNetworkRequestId(item);
429
445
  if (!requestId) return undefined;
@@ -458,7 +474,41 @@ function getNetworkRequestDetailActionId(candidate: NetworkRequestActionCandidat
458
474
  return "inspect-network-request";
459
475
  }
460
476
 
461
- export function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
477
+ export function formatNetworkRouteDiagnosticsText(diagnostics: NetworkRouteDiagnostic[] | undefined): string | undefined {
478
+ if (!diagnostics || diagnostics.length === 0) return undefined;
479
+ const lines = ["Network route diagnostics:"];
480
+ for (const diagnostic of diagnostics) {
481
+ const target = diagnostic.requestId ? `[${diagnostic.requestId}] ${diagnostic.requestUrl ?? "request"}` : diagnostic.requestUrl ?? "request";
482
+ lines.push(`- ${diagnostic.reason}: ${target} matched route ${diagnostic.routePattern} (${diagnostic.mode}).`);
483
+ }
484
+ lines.push("If this route is intended as a mock, verify the page origin/CORS headers and inspect the request before assuming the mock fulfilled normally.");
485
+ return lines.join("\n");
486
+ }
487
+
488
+ export function buildNetworkRouteDiagnosticsNextActions(diagnostics: NetworkRouteDiagnostic[] | undefined, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
489
+ const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
490
+ if (!diagnostic) return undefined;
491
+ const actions: AgentBrowserNextAction[] = [];
492
+ if (diagnostic.requestId) {
493
+ actions.push({
494
+ id: "inspect-pending-routed-network-request",
495
+ params: { args: withOptionalSessionArgs(sessionName, ["network", "request", diagnostic.requestId]) },
496
+ reason: `Inspect the routed request ${diagnostic.requestId} before assuming the route mock fulfilled normally.`,
497
+ safety: "Read-only request diagnostic; look for pending state, CORS/preflight errors, response status, and headers.",
498
+ tool: "agent_browser",
499
+ });
500
+ }
501
+ actions.push({
502
+ id: "start-network-har-capture-for-route-mock",
503
+ params: { args: withOptionalSessionArgs(sessionName, ["network", "har", "start"]) },
504
+ reason: "Capture a HAR before reproducing the route mock so pending/CORS behavior has request and response headers.",
505
+ safety: "HARs can contain URLs and headers; stop to an explicit path and avoid sharing sensitive captures.",
506
+ tool: "agent_browser",
507
+ });
508
+ return actions;
509
+ }
510
+
511
+ export function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined, routeDiagnostics?: NetworkRouteDiagnostic[]): AgentBrowserNextAction[] | undefined {
462
512
  if (!isRecord(data)) return undefined;
463
513
  const requests = getArrayField(data, "requests");
464
514
  if (!requests) return undefined;
@@ -504,7 +554,27 @@ export function buildNetworkRequestsNextActions(data: unknown, sessionName: stri
504
554
  safety: "HARs can contain URLs and headers; stop to an explicit path, inspect metadata, and avoid sharing sensitive captures.",
505
555
  tool: "agent_browser",
506
556
  });
507
- return actions.slice(0, NETWORK_NEXT_ACTION_LIMIT);
557
+ return [...(buildNetworkRouteDiagnosticsNextActions(routeDiagnostics, sessionName) ?? []), ...actions].slice(0, NETWORK_NEXT_ACTION_LIMIT);
558
+ }
559
+
560
+ export function buildStreamNextActions(commandInfo: CommandInfo, data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
561
+ if (commandInfo.command !== "stream" || commandInfo.subcommand !== "enable" || !isRecord(data) || data.alreadyEnabled !== true) return undefined;
562
+ return [
563
+ {
564
+ id: "check-stream-status-after-noop",
565
+ params: { args: withOptionalSessionArgs(sessionName, ["stream", "status"]) },
566
+ reason: "Read current stream port and connection details after the idempotent enable no-op.",
567
+ safety: "Read-only stream diagnostic.",
568
+ tool: "agent_browser",
569
+ },
570
+ {
571
+ id: "disable-existing-stream-when-done",
572
+ params: { args: withOptionalSessionArgs(sessionName, ["stream", "disable"]) },
573
+ reason: "Disable the existing stream when it is no longer needed.",
574
+ safety: "Only run when no other workflow is relying on the current stream.",
575
+ tool: "agent_browser",
576
+ },
577
+ ];
508
578
  }
509
579
 
510
580
  function formatConsoleText(data: Record<string, unknown>): string | undefined {
@@ -602,6 +672,74 @@ function formatCookiesText(data: Record<string, unknown>): string | undefined {
602
672
  return undefined;
603
673
  }
604
674
 
675
+ function valueContainsStorageSecret(value: unknown): boolean {
676
+ if (typeof value === "string") {
677
+ const trimmed = value.trim();
678
+ if (trimmed.length === 0) return false;
679
+ if (STORAGE_TOKEN_VALUE_PATTERN.test(trimmed) || STORAGE_SECRET_VALUE_WORD_PATTERN.test(trimmed) || STORAGE_EMAIL_VALUE_PATTERN.test(trimmed) || STORAGE_IDENTITY_VALUE_PATTERN.test(trimmed)) return true;
680
+ try {
681
+ const url = new URL(trimmed);
682
+ if (url.protocol === "http:" || url.protocol === "https:" || url.username || url.password || url.search) return true;
683
+ } catch {}
684
+ if (redactSensitiveText(trimmed) !== trimmed || redactModelFacingTextIfSensitive(trimmed) !== trimmed) return true;
685
+ try {
686
+ return valueContainsStorageSecret(JSON.parse(trimmed));
687
+ } catch {
688
+ return false;
689
+ }
690
+ }
691
+ if (Array.isArray(value)) return value.some((item) => valueContainsStorageSecret(item));
692
+ if (!isRecord(value)) return false;
693
+ return Object.entries(value).some(([key, entryValue]) => STORAGE_SECRET_KEY_PATTERN.test(key) || valueContainsStorageSecret(entryValue));
694
+ }
695
+
696
+ function shouldRevealStorageValue(key: string | undefined, value: unknown): boolean {
697
+ if (!key || STORAGE_SECRET_KEY_PATTERN.test(key) || !STORAGE_BENIGN_KEY_PATTERN.test(key)) return false;
698
+ if (valueContainsStorageSecret(value)) return false;
699
+ if (typeof value === "string") return value.length <= STORAGE_VALUE_PREVIEW_MAX_CHARS;
700
+ return value === null || typeof value === "number" || typeof value === "boolean";
701
+ }
702
+
703
+ function formatStorageValue(key: string | undefined, value: unknown): string {
704
+ if (!shouldRevealStorageValue(key, value)) return "[REDACTED]";
705
+ if (typeof value === "string") return redactModelFacingText(value);
706
+ return stringifyModelFacing(value);
707
+ }
708
+
709
+ function redactStorageEntryValue(item: Record<string, unknown>): Record<string, unknown> {
710
+ if (!Object.hasOwn(item, "value")) return redactStructuredPresentationValue(item) as Record<string, unknown>;
711
+ const key = getStringField(item, "key") ?? getStringField(item, "name");
712
+ const value = item.value;
713
+ if (shouldRevealStorageValue(key, value)) return redactStructuredPresentationValue(item) as Record<string, unknown>;
714
+ return {
715
+ ...redactStructuredPresentationValue({ ...item, value: undefined }) as Record<string, unknown>,
716
+ value: "[REDACTED]",
717
+ valueRedacted: true,
718
+ valueRedactionReason: key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value",
719
+ };
720
+ }
721
+
722
+ function redactStorageData(value: unknown): unknown {
723
+ if (Array.isArray(value)) return value.map((item) => redactStorageData(item));
724
+ if (!isRecord(value)) return redactStructuredPresentationValue(value);
725
+ const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
726
+ if ((key === "entries" || key === "items") && Array.isArray(entryValue)) return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactStructuredPresentationValue(item))];
727
+ if (key === "value") {
728
+ const itemKey = getStringField(value, "key") ?? getStringField(value, "name");
729
+ return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactStructuredPresentationValue(entryValue) : "[REDACTED]"];
730
+ }
731
+ return [key, redactStructuredPresentationValue(entryValue)];
732
+ }));
733
+ if (Object.hasOwn(value, "value")) {
734
+ const key = getStringField(value, "key") ?? getStringField(value, "name");
735
+ if (!shouldRevealStorageValue(key, value.value)) {
736
+ entries.valueRedacted = true;
737
+ entries.valueRedactionReason = key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value";
738
+ }
739
+ }
740
+ return entries;
741
+ }
742
+
605
743
  function formatStorageText(data: Record<string, unknown>): string | undefined {
606
744
  const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
607
745
  const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
@@ -612,12 +750,12 @@ function formatStorageText(data: Record<string, unknown>): string | undefined {
612
750
  if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
613
751
  const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
614
752
  const key = redactModelFacingText(rawKey);
615
- return Object.hasOwn(item, "value") ? `${key}: [REDACTED]` : key;
753
+ return Object.hasOwn(item, "value") ? `${key}: ${formatStorageValue(rawKey, item.value)}` : key;
616
754
  })
617
755
  .join("\n");
618
756
  }
619
757
  const key = getStringField(data, "key");
620
- if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: [REDACTED]`;
758
+ if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: ${formatStorageValue(key, data.value)}`;
621
759
  if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
622
760
  if (data.cleared === true || data.clear === true) return `${type} cleared.`;
623
761
  return undefined;
@@ -690,7 +828,7 @@ function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unkno
690
828
 
691
829
  export function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
692
830
  if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
693
- if (commandInfo.command === "storage") return redactStatefulValues(data, new Set(["value"]));
831
+ if (commandInfo.command === "storage") return redactStorageData(data);
694
832
  return redactStructuredPresentationValue(data);
695
833
  }
696
834
 
@@ -1,5 +1,5 @@
1
1
  import { isOpenNavigationCommand } from "../../command-taxonomy.js";
2
- import type { CommandInfo } from "../../runtime.js";
2
+ import { redactSensitiveText, type CommandInfo } from "../../runtime.js";
3
3
  import { buildBrowserProfileConfigRecovery } from "./browser-profile-recovery.js";
4
4
  import { redactModelFacingText } from "./common.js";
5
5
  import { buildAgentBrowserNextActions } from "../action-recommendations.js";
@@ -17,6 +17,17 @@ const SELECTOR_DIALECT_ERROR_HINT = [
17
17
  "Prefer refs from `snapshot -i`, or use supported `find role|text|label|placeholder|alt|title|testid ...` locators; use `scrollintoview` before interacting with off-screen elements.",
18
18
  ].join(" ");
19
19
 
20
+ const CLIPBOARD_PERMISSION_ERROR_HINT = [
21
+ "Agent-browser clipboard hint: Clipboard read/write access is environment-dependent and often fails in headless, managed, remote-profile, or file:// sessions.",
22
+ "If you see `NotAllowedError` or `permission denied`, treat it as a browser/OS permission limitation rather than proof that page state changed.",
23
+ "When possible, prefer page-native reads (`snapshot -i`, `get text`, `eval --stdin`) or direct input (`keyboard inserttext` / `keyboard type`) instead of relying on OS clipboard access.",
24
+ "If true clipboard access is required, retry in a browser/profile/session with explicit clipboard permission on a normal http(s) page.",
25
+ ].join(" ");
26
+
27
+ function isRecord(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === "object" && value !== null;
29
+ }
30
+
20
31
  function getSelectorRecoveryHint(errorText: string): string | undefined {
21
32
  const normalized = errorText.trim();
22
33
  if (normalized.length === 0) return undefined;
@@ -42,6 +53,51 @@ function getSelectorRecoveryHint(errorText: string): string | undefined {
42
53
  return undefined;
43
54
  }
44
55
 
56
+ function getClipboardPermissionHint(commandInfo: CommandInfo, errorText: string): string | undefined {
57
+ if (commandInfo.command !== "clipboard") return undefined;
58
+ if (!/\bNotAllowedError\b|\bclipboard\b.*\bpermission denied\b|\bpermission denied\b.*\bclipboard\b/i.test(errorText)) {
59
+ return undefined;
60
+ }
61
+ return CLIPBOARD_PERMISSION_ERROR_HINT;
62
+ }
63
+
64
+ export function redactClipboardPermissionEcho(commandInfo: CommandInfo, errorText: string): string {
65
+ if (commandInfo.command !== "clipboard") return errorText;
66
+ return errorText
67
+ .replace(/(\b(?:read|write)\s+permission denied\b(?:\s+for)?\s+)([\s\S]+)$/gi, "$1[REDACTED]")
68
+ .replace(/(\bFailed to execute '[^']+' on 'Clipboard':\s*)([\s\S]+)$/gi, (match, prefix: string, suffix: string) => {
69
+ if (!/\bpermission denied\b/i.test(suffix)) return match;
70
+ return `${prefix}${suffix.replace(/(\bpermission denied\b(?:\s+for)?\s+)([\s\S]+)$/i, "$1[REDACTED]")}`;
71
+ });
72
+ }
73
+
74
+ export function getClipboardWritePayloadCandidates(commandTokens: readonly string[]): string[] {
75
+ if (commandTokens[0] !== "clipboard" || commandTokens[1] !== "write") return [];
76
+ const payloadTokens = commandTokens.slice(2).filter((value) => value.length > 0);
77
+ return [...new Set([...payloadTokens, payloadTokens.join(" ")].filter((value) => value.length > 0))];
78
+ }
79
+
80
+ function shouldRedactClipboardPayloadField(key: string, value: string, payloadCandidates: readonly string[]): boolean {
81
+ return payloadCandidates.some((candidate) => {
82
+ if (value === candidate) return true;
83
+ if (candidate.length < 8 || !/payload|clipboard|argument/i.test(key)) return false;
84
+ return value.includes(candidate);
85
+ });
86
+ }
87
+
88
+ export function redactClipboardPermissionErrorValue(commandInfo: CommandInfo, value: unknown, payloadCandidates: readonly string[] = []): unknown {
89
+ if (commandInfo.command !== "clipboard") return value;
90
+ if (typeof value === "string") return payloadCandidates.includes(value) ? "[REDACTED]" : redactClipboardPermissionEcho(commandInfo, value);
91
+ if (Array.isArray(value)) return value.map((item) => redactClipboardPermissionErrorValue(commandInfo, item, payloadCandidates));
92
+ if (!isRecord(value)) return value;
93
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
94
+ key,
95
+ typeof entryValue === "string" && shouldRedactClipboardPayloadField(key, entryValue, payloadCandidates)
96
+ ? "[REDACTED]"
97
+ : redactClipboardPermissionErrorValue(commandInfo, entryValue, payloadCandidates),
98
+ ]));
99
+ }
100
+
45
101
  interface CommandSuggestion {
46
102
  args?: string[];
47
103
  description: string;
@@ -116,17 +172,21 @@ export function buildErrorPresentation(options: {
116
172
  sessionName?: string;
117
173
  }): ToolPresentation {
118
174
  const { args, commandInfo, errorText, sessionName } = options;
119
- const safeErrorText = redactModelFacingText(errorText);
175
+ const safeErrorText = redactModelFacingText(
176
+ redactSensitiveText(redactClipboardPermissionEcho(commandInfo, errorText)),
177
+ );
120
178
  const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
121
179
  const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
122
180
  const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
123
181
  const browserProfileConfigRecovery = buildBrowserProfileConfigRecovery({ args, commandInfo, errorText: safeErrorText });
124
182
  const localhostNavigationHint = getLocalhostNavigationHint(commandInfo, safeErrorText);
183
+ const clipboardPermissionHint = getClipboardPermissionHint(commandInfo, safeErrorText);
125
184
  const hintedErrorParts = [
126
185
  selectorHintedErrorText,
127
186
  unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:") ? unknownCommandSuggestionText : undefined,
128
187
  browserProfileConfigRecovery?.hint,
129
188
  localhostNavigationHint,
189
+ clipboardPermissionHint,
130
190
  ].filter((part): part is string => Boolean(part));
131
191
  const hintedErrorText = hintedErrorParts.join("\n\n");
132
192
  const categoryDetails = buildAgentBrowserResultCategoryDetails({
@@ -18,9 +18,9 @@ import {
18
18
  } from "./navigation.js";
19
19
  import { redactModelFacingText } from "./common.js";
20
20
 
21
- const SEMANTIC_NAVIGATION_PROBE_ACTIONS = new Set(["check", "click", "uncheck"]);
21
+ const SEMANTIC_NAVIGATION_PROBE_ACTIONS = new Set(["check", "click"]);
22
22
 
23
- const SEMANTIC_PRESENTATION_ACTIONS = new Set(["check", "click", "fill", "select", "uncheck"]);
23
+ const SEMANTIC_PRESENTATION_ACTIONS = new Set(["check", "click", "fill", "select"]);
24
24
 
25
25
  function getPageSummary(data: Record<string, unknown>): string | undefined {
26
26
  const title = typeof data.title === "string" ? data.title : undefined;
@@ -55,8 +55,6 @@ export function formatSemanticActionCompactLine(compiled: CompiledAgentBrowserSe
55
55
  return `Filled: ${target}`;
56
56
  case "check":
57
57
  return `Checked: ${target}`;
58
- case "uncheck":
59
- return `Unchecked: ${target}`;
60
58
  case "select":
61
59
  return `Selected: ${target}`;
62
60
  default:
@@ -14,6 +14,7 @@ import { detectConfirmationRequired } from "./confirmation.js";
14
14
  import type {
15
15
  AgentBrowserEnvelope,
16
16
  AgentBrowserNextAction,
17
+ NetworkRouteDiagnostic,
17
18
  SessionArtifactManifest,
18
19
  ToolPresentation,
19
20
  } from "./contracts.js";
@@ -29,7 +30,9 @@ import {
29
30
  extractImagePath,
30
31
  formatArtifactMetadataLines,
31
32
  formatArtifactSummary,
33
+ formatMissingArtifactFailureText,
32
34
  getSavedFileDetails,
35
+ hasMissingFileArtifact,
33
36
  isManifestFileArtifact,
34
37
  type ArtifactRequestContext,
35
38
  } from "./presentation/artifacts.js";
@@ -37,7 +40,9 @@ import { buildBatchPresentation, isAgentBrowserBatchResultArray } from "./presen
37
40
  import { getPresentationPaths } from "./presentation/content.js";
38
41
  import {
39
42
  buildNetworkRequestsNextActions,
43
+ buildStreamNextActions,
40
44
  enrichStreamStatusData,
45
+ formatNetworkRouteDiagnosticsText,
41
46
  redactPresentationData,
42
47
  } from "./presentation/diagnostics.js";
43
48
  import { buildErrorPresentation } from "./presentation/errors.js";
@@ -71,6 +76,8 @@ export async function buildToolPresentation(options: {
71
76
  cwd: string;
72
77
  envelope?: AgentBrowserEnvelope;
73
78
  errorText?: string;
79
+ networkRouteDiagnostics?: NetworkRouteDiagnostic[];
80
+ networkRoutes?: import("./contracts.js").NetworkRouteRecord[];
74
81
  persistentArtifactStore?: PersistentSessionArtifactStore;
75
82
  sessionName?: string;
76
83
  }): Promise<ToolPresentation> {
@@ -83,6 +90,8 @@ export async function buildToolPresentation(options: {
83
90
  cwd,
84
91
  envelope,
85
92
  errorText,
93
+ networkRouteDiagnostics,
94
+ networkRoutes,
86
95
  persistentArtifactStore,
87
96
  sessionName,
88
97
  } = options;
@@ -108,6 +117,7 @@ export async function buildToolPresentation(options: {
108
117
  buildNestedToolPresentation: buildToolPresentation,
109
118
  cwd,
110
119
  data,
120
+ networkRoutes,
111
121
  persistentArtifactStore,
112
122
  sessionName,
113
123
  summary,
@@ -124,6 +134,11 @@ export async function buildToolPresentation(options: {
124
134
  };
125
135
  }
126
136
 
137
+ if (networkRouteDiagnostics && networkRouteDiagnostics.length > 0 && presentation.content[0]?.type === "text") {
138
+ const diagnosticText = formatNetworkRouteDiagnosticsText(networkRouteDiagnostics);
139
+ if (diagnosticText) presentation.content[0] = { ...presentation.content[0], text: `${diagnosticText}\n\n${presentation.content[0].text}` };
140
+ presentation.networkRouteDiagnostics = networkRouteDiagnostics;
141
+ }
127
142
  if (artifacts.length > 0 && !presentation.artifacts) {
128
143
  presentation.artifacts = artifacts;
129
144
  }
@@ -161,6 +176,19 @@ export async function buildToolPresentation(options: {
161
176
  ) ?? presentationWithManifest.artifactVerification;
162
177
 
163
178
  const confirmationRequired = detectConfirmationRequired(data);
179
+ const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
180
+ if (missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
181
+ presentationWithManifest.resultCategory = "failure";
182
+ presentationWithManifest.failureCategory = "artifact-missing";
183
+ presentationWithManifest.successCategory = undefined;
184
+ presentationWithManifest.summary = missingArtifactFailureText;
185
+ if (presentationWithManifest.content[0]?.type === "text") {
186
+ presentationWithManifest.content[0] = { ...presentationWithManifest.content[0], text: `${missingArtifactFailureText}\n\n${presentationWithManifest.content[0].text}` };
187
+ } else {
188
+ presentationWithManifest.content.unshift({ type: "text", text: missingArtifactFailureText });
189
+ }
190
+ }
191
+
164
192
  if (!presentationWithManifest.resultCategory) {
165
193
  const categoryDetails = buildAgentBrowserResultCategoryDetails({
166
194
  artifacts: presentationWithManifest.artifacts,
@@ -199,12 +227,14 @@ export async function buildToolPresentation(options: {
199
227
  successCategory: presentationWithManifest.successCategory,
200
228
  });
201
229
  const networkNextActions = commandInfo.command === "network" && commandInfo.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
202
- ? buildNetworkRequestsNextActions(data, sessionName)
230
+ ? buildNetworkRequestsNextActions(data, sessionName, presentationWithManifest.networkRouteDiagnostics)
203
231
  : undefined;
232
+ const streamNextActions = presentationWithManifest.resultCategory === "success" ? buildStreamNextActions(commandInfo, data, sessionName) : undefined;
204
233
  presentationWithManifest.nextActions = mergeNextActions(
205
234
  presentationWithManifest.nextActions,
206
235
  genericNextActions,
207
236
  networkNextActions,
237
+ streamNextActions,
208
238
  );
209
239
  presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
210
240
  artifacts: presentationWithManifest.artifacts,