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.
- package/CHANGELOG.md +13 -0
- package/README.md +11 -8
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/COMMAND_REFERENCE.md +12 -8
- package/docs/RELEASE.md +11 -11
- package/docs/REQUIREMENTS.md +4 -3
- package/docs/SUPPORT_MATRIX.md +13 -5
- package/docs/TOOL_CONTRACT.md +30 -20
- package/extensions/agent-browser/index.ts +145 -33
- package/extensions/agent-browser/lib/playbook.ts +10 -10
- package/extensions/agent-browser/lib/results/presentation.ts +154 -2
- package/extensions/agent-browser/lib/results/shared.ts +7 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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