pi-agent-browser-native 0.2.30 → 0.2.31

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.
@@ -96,6 +96,10 @@ const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
96
96
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
97
97
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
98
98
  const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
99
+ const NETWORK_NEXT_ACTION_LIMIT = 4;
100
+ const NETWORK_FILTER_MAX_CHARS = 160;
101
+ const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = ["apikey", "api-key", "api_key", "authentication", "authorization", "bearer", "credential", "credentials", "jwt", "passwd", "password", "reset", "secret", "session", "token"] as const;
102
+ const NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN = /^(?:[A-Fa-f0-9]{16,}|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{16,})$/;
99
103
  const NETWORK_PREVIEW_FIELD_CANDIDATES = {
100
104
  request: ["postData"] as const,
101
105
  response: ["responseBody"] as const,
@@ -641,6 +645,145 @@ function formatNetworkRequestText(data: Record<string, unknown>): string | undef
641
645
  return formatNetworkRequestLine(data, 0).join("\n");
642
646
  }
643
647
 
648
+ interface NetworkRequestActionCandidate {
649
+ filter?: string;
650
+ item: Record<string, unknown>;
651
+ kind: "actionable" | "api" | "benign" | "request";
652
+ requestId: string;
653
+ }
654
+
655
+ function getSafeNetworkActionValue(value: string | undefined): string | undefined {
656
+ if (!value) return undefined;
657
+ const trimmed = value.trim();
658
+ if (trimmed.length === 0 || redactSensitiveText(trimmed) !== trimmed) return undefined;
659
+ return trimmed;
660
+ }
661
+
662
+ function getNetworkRequestId(item: Record<string, unknown>): string | undefined {
663
+ return getSafeNetworkActionValue(getStringField(item, "requestId") ?? getStringField(item, "id"));
664
+ }
665
+
666
+ function isSensitiveNetworkPathSegment(segment: string): boolean {
667
+ const normalized = segment.toLowerCase();
668
+ return normalized === "auth" || NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS.some((term) => normalized.includes(term));
669
+ }
670
+
671
+ function pathFilterMayExposeSensitiveSegment(filter: string): boolean {
672
+ const decoded = (() => {
673
+ try {
674
+ return decodeURIComponent(filter);
675
+ } catch {
676
+ return filter;
677
+ }
678
+ })();
679
+ return decoded.split("/").some((segment) => isSensitiveNetworkPathSegment(segment) || NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN.test(segment));
680
+ }
681
+
682
+ function getNetworkRequestPathFilter(item: Record<string, unknown>): string | undefined {
683
+ const url = getStringField(item, "url");
684
+ if (!url) return undefined;
685
+ let filter: string | undefined;
686
+ try {
687
+ filter = new URL(url).pathname;
688
+ } catch {
689
+ filter = url.split(/[?#]/, 1)[0];
690
+ }
691
+ filter = filter?.trim();
692
+ if (!filter || filter === "/" || filter.length > NETWORK_FILTER_MAX_CHARS || pathFilterMayExposeSensitiveSegment(filter)) return undefined;
693
+ return getSafeNetworkActionValue(filter);
694
+ }
695
+
696
+ function isApiLikeNetworkRequest(item: Record<string, unknown>): boolean {
697
+ const method = (getStringField(item, "method") ?? "GET").toUpperCase();
698
+ const resourceType = (getStringField(item, "resourceType") ?? "").toLowerCase();
699
+ const mimeType = (getStringField(item, "mimeType") ?? "").toLowerCase();
700
+ const filter = getNetworkRequestPathFilter(item) ?? "";
701
+ return resourceType === "fetch" || resourceType === "xhr" || mimeType.includes("json") || /\/(?:api|graphql|rpc)(?:\/|$)/i.test(filter) || !["GET", "HEAD"].includes(method);
702
+ }
703
+
704
+ function getNetworkRequestActionCandidate(item: Record<string, unknown>): NetworkRequestActionCandidate | undefined {
705
+ const requestId = getNetworkRequestId(item);
706
+ if (!requestId) return undefined;
707
+ const classification = classifyNetworkRequestFailure(item);
708
+ const kind: NetworkRequestActionCandidate["kind"] = classification?.impact === "actionable"
709
+ ? "actionable"
710
+ : classification?.impact === "benign"
711
+ ? "benign"
712
+ : isApiLikeNetworkRequest(item)
713
+ ? "api"
714
+ : "request";
715
+ return { filter: getNetworkRequestPathFilter(item), item, kind, requestId };
716
+ }
717
+
718
+ function chooseNetworkRequestActionCandidate(candidates: NetworkRequestActionCandidate[]): NetworkRequestActionCandidate | undefined {
719
+ return candidates.find((candidate) => candidate.kind === "actionable")
720
+ ?? candidates.find((candidate) => candidate.kind === "api")
721
+ ?? candidates.find((candidate) => candidate.kind === "benign")
722
+ ?? candidates[0];
723
+ }
724
+
725
+ function formatNetworkRequestActionDescriptor(candidate: NetworkRequestActionCandidate): string {
726
+ const method = getStringField(candidate.item, "method") ?? "GET";
727
+ const status = typeof candidate.item.status === "number" ? String(candidate.item.status) : "pending";
728
+ const target = candidate.filter ? ` ${candidate.filter}` : "";
729
+ return `${status} ${method}${target} [${candidate.requestId}]`;
730
+ }
731
+
732
+ function getNetworkRequestDetailActionId(candidate: NetworkRequestActionCandidate): string {
733
+ if (candidate.kind === "actionable") return "inspect-actionable-network-request";
734
+ if (candidate.kind === "benign") return "inspect-benign-network-request";
735
+ return "inspect-network-request";
736
+ }
737
+
738
+ function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
739
+ if (!isRecord(data)) return undefined;
740
+ const requests = getArrayField(data, "requests");
741
+ if (!requests) return undefined;
742
+ const candidates = requests.flatMap((item) => {
743
+ if (!isRecord(item)) return [];
744
+ const candidate = getNetworkRequestActionCandidate(item);
745
+ return candidate ? [candidate] : [];
746
+ });
747
+ const selected = chooseNetworkRequestActionCandidate(candidates);
748
+ if (!selected) return undefined;
749
+ const descriptor = formatNetworkRequestActionDescriptor(selected);
750
+ const actions: AgentBrowserNextAction[] = [
751
+ {
752
+ id: getNetworkRequestDetailActionId(selected),
753
+ params: { args: withSessionPrefix(sessionName, ["network", "request", selected.requestId]) },
754
+ reason: `Inspect full request details for ${descriptor}.`,
755
+ safety: "Read-only network diagnostic; request inspection must not replace the active page/ref context.",
756
+ tool: "agent_browser",
757
+ },
758
+ ];
759
+ if (selected.kind === "actionable") {
760
+ actions.push({
761
+ id: "trace-actionable-network-source",
762
+ params: { networkSourceLookup: { requestId: selected.requestId, ...(sessionName ? { session: sessionName } : {}) } },
763
+ reason: `Look for local source candidates related to ${descriptor}.`,
764
+ safety: "Read-only experimental helper; it reports bounded candidates and may miss bundled or dynamic call sites.",
765
+ tool: "agent_browser",
766
+ });
767
+ }
768
+ if (selected.filter) {
769
+ actions.push({
770
+ id: "filter-network-requests-by-path",
771
+ params: { args: withSessionPrefix(sessionName, ["network", "requests", "--filter", selected.filter]) },
772
+ reason: `List captured requests matching ${selected.filter}.`,
773
+ safety: "Read-only request-list filter; absence from a compact preview is not proof the request did not happen.",
774
+ tool: "agent_browser",
775
+ });
776
+ }
777
+ actions.push({
778
+ id: "start-network-har-capture",
779
+ params: { args: withSessionPrefix(sessionName, ["network", "har", "start"]) },
780
+ reason: "Start HAR capture before reproducing the network behavior again.",
781
+ safety: "HARs can contain URLs and headers; stop to an explicit path, inspect metadata, and avoid sharing sensitive captures.",
782
+ tool: "agent_browser",
783
+ });
784
+ return actions.slice(0, NETWORK_NEXT_ACTION_LIMIT);
785
+ }
786
+
644
787
  function formatConsoleText(data: Record<string, unknown>): string | undefined {
645
788
  const messages = getArrayField(data, "messages");
646
789
  if (!messages) return undefined;
@@ -1694,7 +1837,7 @@ async function buildBatchStepPresentation(options: {
1694
1837
  secondaryPaths: presentation.imagePaths,
1695
1838
  });
1696
1839
  const text = getPresentationText(presentation) || presentation.summary;
1697
- const nextActions = buildAgentBrowserNextActions({
1840
+ const nextActions = presentation.nextActions ?? buildAgentBrowserNextActions({
1698
1841
  artifacts: presentation.artifacts,
1699
1842
  args: command,
1700
1843
  command: command?.[0],
@@ -2129,6 +2272,11 @@ function buildSpillArtifactEntries(options: {
2129
2272
  ];
2130
2273
  }
2131
2274
 
2275
+ function mergeNextActions(...groups: Array<AgentBrowserNextAction[] | undefined>): AgentBrowserNextAction[] | undefined {
2276
+ const merged = groups.flatMap((group) => group ?? []);
2277
+ return merged.length > 0 ? merged : undefined;
2278
+ }
2279
+
2132
2280
  async function compactLargePresentationOutput(options: {
2133
2281
  artifactManifest?: SessionArtifactManifest;
2134
2282
  commandInfo: CommandInfo;
@@ -2321,7 +2469,7 @@ export async function buildToolPresentation(options: {
2321
2469
  savedFile: presentationWithManifest.savedFile,
2322
2470
  });
2323
2471
  }
2324
- presentationWithManifest.nextActions = presentationWithManifest.nextActions ?? buildAgentBrowserNextActions({
2472
+ const genericNextActions = presentationWithManifest.nextActions ? undefined : buildAgentBrowserNextActions({
2325
2473
  artifacts: presentationWithManifest.artifacts,
2326
2474
  args,
2327
2475
  command: commandInfo.command,
@@ -2331,6 +2479,10 @@ export async function buildToolPresentation(options: {
2331
2479
  savedFilePath: presentationWithManifest.savedFilePath,
2332
2480
  successCategory: presentationWithManifest.successCategory,
2333
2481
  });
2482
+ const networkNextActions = commandInfo.command === "network" && commandInfo.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
2483
+ ? buildNetworkRequestsNextActions(data, sessionName)
2484
+ : undefined;
2485
+ presentationWithManifest.nextActions = mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions);
2334
2486
  presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
2335
2487
  artifacts: presentationWithManifest.artifacts,
2336
2488
  commandInfo,
@@ -59,7 +59,13 @@ export interface AgentBrowserNextAction {
59
59
  artifactPath?: string;
60
60
  id: string;
61
61
  params?: {
62
- args: string[];
62
+ args?: string[];
63
+ networkSourceLookup?: {
64
+ filter?: string;
65
+ requestId?: string;
66
+ session?: string;
67
+ url?: string;
68
+ };
63
69
  sessionMode?: "auto" | "fresh";
64
70
  stdin?: string;
65
71
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",