pi-agent-browser-native 0.2.30 → 0.2.32
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 +31 -0
- package/README.md +51 -13
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +66 -15
- package/docs/ELECTRON.md +368 -0
- package/docs/RELEASE.md +40 -12
- package/docs/REQUIREMENTS.md +7 -4
- package/docs/SUPPORT_MATRIX.md +21 -10
- package/docs/TOOL_CONTRACT.md +200 -37
- package/extensions/agent-browser/index.ts +2305 -127
- package/extensions/agent-browser/lib/electron/cleanup.ts +287 -0
- package/extensions/agent-browser/lib/electron/discovery.ts +717 -0
- package/extensions/agent-browser/lib/electron/launch.ts +553 -0
- package/extensions/agent-browser/lib/playbook.ts +14 -13
- package/extensions/agent-browser/lib/results/presentation.ts +191 -9
- package/extensions/agent-browser/lib/results/shared.ts +95 -1
- package/extensions/agent-browser/lib/temp.ts +26 -0
- package/package.json +5 -4
|
@@ -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;
|
|
@@ -1561,6 +1704,33 @@ function buildUnknownCommandSuggestionActions(suggestions: CommandSuggestion[],
|
|
|
1561
1704
|
return actions.length > 0 ? actions : undefined;
|
|
1562
1705
|
}
|
|
1563
1706
|
|
|
1707
|
+
function isWaitTextAssertionCommand(command: string[] | undefined): boolean {
|
|
1708
|
+
return command?.[0] === "wait" && command.includes("--text");
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function buildWaitTextAssertionFailureNextAction(sessionName: string | undefined): AgentBrowserNextAction {
|
|
1712
|
+
return {
|
|
1713
|
+
id: "inspect-after-text-assertion-failure",
|
|
1714
|
+
params: { args: withSessionPrefix(sessionName, ["snapshot", "-i"]) },
|
|
1715
|
+
reason: "Inspect the current page after the text assertion failed before concluding the expected text is absent.",
|
|
1716
|
+
safety: "Read-only snapshot; use current refs or visible text from this page before retrying the assertion.",
|
|
1717
|
+
tool: "agent_browser",
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function mergePresentationNextActions(...groups: Array<AgentBrowserNextAction[] | undefined>): AgentBrowserNextAction[] | undefined {
|
|
1722
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
1723
|
+
const seen = new Set<string>();
|
|
1724
|
+
for (const group of groups) {
|
|
1725
|
+
for (const action of group ?? []) {
|
|
1726
|
+
if (seen.has(action.id)) continue;
|
|
1727
|
+
actions.push(action);
|
|
1728
|
+
seen.add(action.id);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return actions.length > 0 ? actions : undefined;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1564
1734
|
function appendSelectorRecoveryHint(errorText: string): string {
|
|
1565
1735
|
const hint = getSelectorRecoveryHint(errorText);
|
|
1566
1736
|
if (!hint || errorText.includes("Agent-browser hint:")) {
|
|
@@ -1642,13 +1812,16 @@ async function buildBatchStepPresentation(options: {
|
|
|
1642
1812
|
errorText,
|
|
1643
1813
|
});
|
|
1644
1814
|
const confirmationRequired = detectConfirmationRequired(item.error);
|
|
1645
|
-
const nextActions =
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1815
|
+
const nextActions = mergePresentationNextActions(
|
|
1816
|
+
buildAgentBrowserNextActions({
|
|
1817
|
+
args: command,
|
|
1818
|
+
command: command?.[0],
|
|
1819
|
+
confirmationId: confirmationRequired?.id,
|
|
1820
|
+
failureCategory,
|
|
1821
|
+
resultCategory: "failure",
|
|
1822
|
+
}),
|
|
1823
|
+
isWaitTextAssertionCommand(command) ? [buildWaitTextAssertionFailureNextAction(sessionName)] : undefined,
|
|
1824
|
+
);
|
|
1652
1825
|
const presentation: ToolPresentation = {
|
|
1653
1826
|
content: [{ type: "text", text: errorText }],
|
|
1654
1827
|
failureCategory,
|
|
@@ -1694,7 +1867,7 @@ async function buildBatchStepPresentation(options: {
|
|
|
1694
1867
|
secondaryPaths: presentation.imagePaths,
|
|
1695
1868
|
});
|
|
1696
1869
|
const text = getPresentationText(presentation) || presentation.summary;
|
|
1697
|
-
const nextActions = buildAgentBrowserNextActions({
|
|
1870
|
+
const nextActions = presentation.nextActions ?? buildAgentBrowserNextActions({
|
|
1698
1871
|
artifacts: presentation.artifacts,
|
|
1699
1872
|
args: command,
|
|
1700
1873
|
command: command?.[0],
|
|
@@ -2129,6 +2302,11 @@ function buildSpillArtifactEntries(options: {
|
|
|
2129
2302
|
];
|
|
2130
2303
|
}
|
|
2131
2304
|
|
|
2305
|
+
function mergeNextActions(...groups: Array<AgentBrowserNextAction[] | undefined>): AgentBrowserNextAction[] | undefined {
|
|
2306
|
+
const merged = groups.flatMap((group) => group ?? []);
|
|
2307
|
+
return merged.length > 0 ? merged : undefined;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2132
2310
|
async function compactLargePresentationOutput(options: {
|
|
2133
2311
|
artifactManifest?: SessionArtifactManifest;
|
|
2134
2312
|
commandInfo: CommandInfo;
|
|
@@ -2321,7 +2499,7 @@ export async function buildToolPresentation(options: {
|
|
|
2321
2499
|
savedFile: presentationWithManifest.savedFile,
|
|
2322
2500
|
});
|
|
2323
2501
|
}
|
|
2324
|
-
|
|
2502
|
+
const genericNextActions = presentationWithManifest.nextActions ? undefined : buildAgentBrowserNextActions({
|
|
2325
2503
|
artifacts: presentationWithManifest.artifacts,
|
|
2326
2504
|
args,
|
|
2327
2505
|
command: commandInfo.command,
|
|
@@ -2331,6 +2509,10 @@ export async function buildToolPresentation(options: {
|
|
|
2331
2509
|
savedFilePath: presentationWithManifest.savedFilePath,
|
|
2332
2510
|
successCategory: presentationWithManifest.successCategory,
|
|
2333
2511
|
});
|
|
2512
|
+
const networkNextActions = commandInfo.command === "network" && commandInfo.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
|
|
2513
|
+
? buildNetworkRequestsNextActions(data, sessionName)
|
|
2514
|
+
: undefined;
|
|
2515
|
+
presentationWithManifest.nextActions = mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions);
|
|
2334
2516
|
presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
|
|
2335
2517
|
artifacts: presentationWithManifest.artifacts,
|
|
2336
2518
|
commandInfo,
|
|
@@ -25,10 +25,12 @@ export type AgentBrowserSuccessCategory = "artifact-saved" | "artifact-unverifie
|
|
|
25
25
|
|
|
26
26
|
export type AgentBrowserFailureCategory =
|
|
27
27
|
| "aborted"
|
|
28
|
+
| "cleanup-failed"
|
|
28
29
|
| "confirmation-required"
|
|
29
30
|
| "download-not-verified"
|
|
30
31
|
| "missing-binary"
|
|
31
32
|
| "parse-failure"
|
|
33
|
+
| "policy-blocked"
|
|
32
34
|
| "qa-failure"
|
|
33
35
|
| "selector-not-found"
|
|
34
36
|
| "selector-unsupported"
|
|
@@ -59,7 +61,19 @@ export interface AgentBrowserNextAction {
|
|
|
59
61
|
artifactPath?: string;
|
|
60
62
|
id: string;
|
|
61
63
|
params?: {
|
|
62
|
-
args
|
|
64
|
+
args?: string[];
|
|
65
|
+
electron?: {
|
|
66
|
+
action: "cleanup" | "list" | "launch" | "probe" | "status";
|
|
67
|
+
all?: boolean;
|
|
68
|
+
handoff?: "connect" | "snapshot" | "tabs";
|
|
69
|
+
launchId?: string;
|
|
70
|
+
};
|
|
71
|
+
networkSourceLookup?: {
|
|
72
|
+
filter?: string;
|
|
73
|
+
requestId?: string;
|
|
74
|
+
session?: string;
|
|
75
|
+
url?: string;
|
|
76
|
+
};
|
|
63
77
|
sessionMode?: "auto" | "fresh";
|
|
64
78
|
stdin?: string;
|
|
65
79
|
};
|
|
@@ -353,6 +367,8 @@ export function classifyAgentBrowserFailureCategory(options: {
|
|
|
353
367
|
if (/ENOENT|not found on PATH|could not find.*agent-browser|agent-browser is required but was not found/i.test(text)) return "missing-binary";
|
|
354
368
|
if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text)) return "parse-failure";
|
|
355
369
|
if (/aborted/i.test(text)) return "aborted";
|
|
370
|
+
if (/policy[- ]blocked|blocked by caller policy|caller deny policy|caller allow policy/i.test(text)) return "policy-blocked";
|
|
371
|
+
if (/cleanup failed|cleanup.*partial|partial cleanup|remaining resources/i.test(text)) return "cleanup-failed";
|
|
356
372
|
if (options.tabDrift || /could not re-select the intended tab|about:blank|selected tab looks wrong|tab drift|tab.*wrong/i.test(text)) return "tab-drift";
|
|
357
373
|
if (/\bUnknown ref\b|\bstale ref\b|@ref may be stale|\bref\b.*\b(?:not found|missing|expired)\b/i.test(text)) return "stale-ref";
|
|
358
374
|
if (usedRef && /could not locate element|element not found|no element/i.test(text)) return "stale-ref";
|
|
@@ -418,6 +434,22 @@ function buildArtifactVerificationAction(artifact: FileArtifactMetadata): AgentB
|
|
|
418
434
|
};
|
|
419
435
|
}
|
|
420
436
|
|
|
437
|
+
function buildElectronToolAction(options: {
|
|
438
|
+
action: "cleanup" | "probe" | "status";
|
|
439
|
+
id: string;
|
|
440
|
+
launchId: string;
|
|
441
|
+
reason: string;
|
|
442
|
+
safety?: string;
|
|
443
|
+
}): AgentBrowserNextAction {
|
|
444
|
+
return {
|
|
445
|
+
id: options.id,
|
|
446
|
+
params: { electron: { action: options.action, launchId: options.launchId } },
|
|
447
|
+
reason: options.reason,
|
|
448
|
+
...(options.safety ? { safety: options.safety } : {}),
|
|
449
|
+
tool: "agent_browser",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
421
453
|
const MUTATING_COMMANDS = new Set([
|
|
422
454
|
"back",
|
|
423
455
|
"check",
|
|
@@ -459,12 +491,74 @@ export function buildAgentBrowserNextActions(options: {
|
|
|
459
491
|
args?: string[];
|
|
460
492
|
command?: string;
|
|
461
493
|
confirmationId?: string;
|
|
494
|
+
electron?: {
|
|
495
|
+
launchId?: string;
|
|
496
|
+
sessionName?: string;
|
|
497
|
+
status?: "active" | "cleaned" | "dead" | "failed" | "partial" | "succeeded";
|
|
498
|
+
};
|
|
462
499
|
failureCategory?: AgentBrowserFailureCategory;
|
|
463
500
|
resultCategory: AgentBrowserResultCategory;
|
|
464
501
|
savedFilePath?: string;
|
|
465
502
|
successCategory?: AgentBrowserSuccessCategory;
|
|
466
503
|
}): AgentBrowserNextAction[] | undefined {
|
|
467
504
|
const actions: AgentBrowserNextAction[] = [];
|
|
505
|
+
if (options.electron?.launchId) {
|
|
506
|
+
const { launchId, sessionName, status } = options.electron;
|
|
507
|
+
if (options.resultCategory === "success" && status !== "cleaned") {
|
|
508
|
+
actions.push(
|
|
509
|
+
buildElectronToolAction({
|
|
510
|
+
action: "status",
|
|
511
|
+
id: "status-electron-launch",
|
|
512
|
+
launchId,
|
|
513
|
+
reason: "Check the wrapper-tracked Electron launch liveness and current CDP targets without mutating the app.",
|
|
514
|
+
}),
|
|
515
|
+
buildElectronToolAction({
|
|
516
|
+
action: "probe",
|
|
517
|
+
id: "probe-electron-launch",
|
|
518
|
+
launchId,
|
|
519
|
+
reason: "Probe the attached Electron managed session and carry the wrapper launchId for follow-up diagnostics.",
|
|
520
|
+
}),
|
|
521
|
+
buildElectronToolAction({
|
|
522
|
+
action: "cleanup",
|
|
523
|
+
id: "cleanup-electron-launch",
|
|
524
|
+
launchId,
|
|
525
|
+
reason: "Clean the wrapper-owned Electron process and isolated userDataDir when the run is complete.",
|
|
526
|
+
safety: "Only operates on the launchId created by electron.launch; explicit artifacts and manually launched apps remain host-owned.",
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
if (sessionName) {
|
|
530
|
+
actions.push(
|
|
531
|
+
buildNextToolAction({
|
|
532
|
+
args: ["--session", sessionName, "tab", "list"],
|
|
533
|
+
id: "list-electron-tabs",
|
|
534
|
+
reason: "Inspect attached Electron page/webview targets before choosing the active tab.",
|
|
535
|
+
}),
|
|
536
|
+
buildNextToolAction({
|
|
537
|
+
args: ["--session", sessionName, "snapshot", "-i"],
|
|
538
|
+
id: "snapshot-electron-session",
|
|
539
|
+
reason: "Refresh interactive refs for the attached Electron session.",
|
|
540
|
+
safety: "Use current Electron refs only after a fresh snapshot for this session.",
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
} else if (options.resultCategory === "failure" && options.failureCategory === "cleanup-failed") {
|
|
545
|
+
actions.push(
|
|
546
|
+
buildElectronToolAction({
|
|
547
|
+
action: "status",
|
|
548
|
+
id: "status-electron-launch",
|
|
549
|
+
launchId,
|
|
550
|
+
reason: "Inspect which wrapper-tracked Electron resources remain after partial cleanup.",
|
|
551
|
+
}),
|
|
552
|
+
buildElectronToolAction({
|
|
553
|
+
action: "cleanup",
|
|
554
|
+
id: "retry-electron-cleanup",
|
|
555
|
+
launchId,
|
|
556
|
+
reason: "Retry cleanup for the same wrapper-owned Electron launch after reviewing remaining resources.",
|
|
557
|
+
safety: "Only retry for the same launchId; do not use cleanup for manually launched Electron apps.",
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
468
562
|
if (options.resultCategory === "success") {
|
|
469
563
|
if (options.command === "open") {
|
|
470
564
|
actions.push(buildNextToolAction({
|
|
@@ -390,6 +390,32 @@ export async function writeSecureTempFile(options: {
|
|
|
390
390
|
return path;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
export async function createSecureTempDirectory(prefix: string): Promise<string> {
|
|
394
|
+
const tempRoot = await getSessionTempRoot();
|
|
395
|
+
await assertSecureTempRootBudget(tempRoot, 0);
|
|
396
|
+
const directory = await mkdtemp(join(tempRoot, prefix));
|
|
397
|
+
await chmod(directory, 0o700).catch(() => undefined);
|
|
398
|
+
await refreshSecureTempRootLease(tempRoot).catch(() => undefined);
|
|
399
|
+
return directory;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function getSecureTempChildDirectoryValidationError(path: string, childPrefix: string): Promise<string | undefined> {
|
|
403
|
+
const parentDirectory = dirname(path);
|
|
404
|
+
const childName = path.slice(parentDirectory.length + 1);
|
|
405
|
+
if (!childName.startsWith(childPrefix)) {
|
|
406
|
+
return `Refusing to remove ${path}; expected wrapper temp child prefix ${childPrefix}.`;
|
|
407
|
+
}
|
|
408
|
+
const ownershipMarker = await readTempRootOwnershipMarker(parentDirectory);
|
|
409
|
+
if (!ownershipMarker) {
|
|
410
|
+
return `Refusing to remove ${path}; parent directory is not a pi-agent-browser owned temp root.`;
|
|
411
|
+
}
|
|
412
|
+
const currentUid = getCurrentProcessUid();
|
|
413
|
+
if (currentUid !== undefined && ownershipMarker.ownerUid !== undefined && ownershipMarker.ownerUid !== currentUid) {
|
|
414
|
+
return `Refusing to remove ${path}; parent temp root is owned by uid ${ownershipMarker.ownerUid}, not current uid ${currentUid}.`;
|
|
415
|
+
}
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
393
419
|
export async function writePersistentSessionArtifactFile(options: {
|
|
394
420
|
content: string | Uint8Array;
|
|
395
421
|
prefix: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.32",
|
|
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)",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"LICENSE",
|
|
39
39
|
"docs/ARCHITECTURE.md",
|
|
40
40
|
"docs/COMMAND_REFERENCE.md",
|
|
41
|
+
"docs/ELECTRON.md",
|
|
41
42
|
"docs/RELEASE.md",
|
|
42
43
|
"docs/REQUIREMENTS.md",
|
|
43
44
|
"docs/SUPPORT_MATRIX.md",
|
|
@@ -55,9 +56,9 @@
|
|
|
55
56
|
"typebox": "*"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@earendil-works/pi-ai": "^0.75.
|
|
59
|
-
"@earendil-works/pi-coding-agent": "^0.75.
|
|
60
|
-
"@earendil-works/pi-tui": "^0.75.
|
|
59
|
+
"@earendil-works/pi-ai": "^0.75.4",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
61
|
+
"@earendil-works/pi-tui": "^0.75.4",
|
|
61
62
|
"@types/node": "^25.6.1",
|
|
62
63
|
"tsx": "^4.21.0",
|
|
63
64
|
"typebox": "^1.1.38",
|