pi-agent-browser-native 0.1.5 → 0.2.0
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 +23 -0
- package/README.md +89 -2
- package/docs/ARCHITECTURE.md +7 -3
- package/docs/RELEASE.md +1 -1
- package/docs/TOOL_CONTRACT.md +18 -10
- package/extensions/agent-browser/index.ts +185 -62
- package/extensions/agent-browser/lib/process.ts +91 -6
- package/extensions/agent-browser/lib/results/envelope.ts +102 -0
- package/extensions/agent-browser/lib/results/presentation.ts +461 -0
- package/extensions/agent-browser/lib/results/shared.ts +91 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +648 -0
- package/extensions/agent-browser/lib/results.ts +8 -934
- package/extensions/agent-browser/lib/runtime.ts +66 -24
- package/extensions/agent-browser/lib/temp.ts +159 -16
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
|
|
3
|
-
* Responsibilities: Spawn the agent-browser subprocess without a shell, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file, and honor abort signals.
|
|
3
|
+
* Responsibilities: Spawn the agent-browser subprocess without a shell, forward a curated environment surface, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
|
|
4
4
|
* Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
|
|
5
5
|
* Usage: Called by the extension tool after argument validation and session planning are complete.
|
|
6
6
|
* Invariants/Assumptions: The binary name is always `agent-browser`, the wrapper never shells out, and callers handle semantic success/error interpretation.
|
|
@@ -9,12 +9,63 @@
|
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { env as processEnv } from "node:process";
|
|
11
11
|
|
|
12
|
-
import { openSecureTempFile } from "./temp.js";
|
|
12
|
+
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
13
13
|
|
|
14
14
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
15
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
16
16
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
17
17
|
const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
|
|
18
|
+
const httpProxyEnvName = "http_proxy";
|
|
19
|
+
const httpsProxyEnvName = "https_proxy";
|
|
20
|
+
const allProxyEnvName = "all_proxy";
|
|
21
|
+
const noProxyEnvName = "no_proxy";
|
|
22
|
+
const INHERITED_ENV_NAMES = new Set([
|
|
23
|
+
"ALL_PROXY",
|
|
24
|
+
"APPDATA",
|
|
25
|
+
"CI",
|
|
26
|
+
"COLORTERM",
|
|
27
|
+
"COMSPEC",
|
|
28
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
29
|
+
"DISPLAY",
|
|
30
|
+
"FORCE_COLOR",
|
|
31
|
+
"HOME",
|
|
32
|
+
"HOMEDRIVE",
|
|
33
|
+
"HOMEPATH",
|
|
34
|
+
"HTTPS_PROXY",
|
|
35
|
+
"HTTP_PROXY",
|
|
36
|
+
"LANG",
|
|
37
|
+
"LC_ALL",
|
|
38
|
+
"LC_CTYPE",
|
|
39
|
+
"LOCALAPPDATA",
|
|
40
|
+
"LOGNAME",
|
|
41
|
+
"NO_COLOR",
|
|
42
|
+
"NO_PROXY",
|
|
43
|
+
"NODE_EXTRA_CA_CERTS",
|
|
44
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
45
|
+
"OS",
|
|
46
|
+
"PATH",
|
|
47
|
+
"PATHEXT",
|
|
48
|
+
"PWD",
|
|
49
|
+
"SHELL",
|
|
50
|
+
"SSL_CERT_DIR",
|
|
51
|
+
"SSL_CERT_FILE",
|
|
52
|
+
"SYSTEMROOT",
|
|
53
|
+
"TEMP",
|
|
54
|
+
"TERM",
|
|
55
|
+
"TMP",
|
|
56
|
+
"TMPDIR",
|
|
57
|
+
"TZ",
|
|
58
|
+
"USER",
|
|
59
|
+
"USERNAME",
|
|
60
|
+
"USERPROFILE",
|
|
61
|
+
"WAYLAND_DISPLAY",
|
|
62
|
+
"XAUTHORITY",
|
|
63
|
+
httpProxyEnvName,
|
|
64
|
+
httpsProxyEnvName,
|
|
65
|
+
allProxyEnvName,
|
|
66
|
+
noProxyEnvName,
|
|
67
|
+
]);
|
|
68
|
+
const INHERITED_ENV_PREFIXES = ["AGENT_BROWSER_", "AI_GATEWAY_", "XDG_"] as const;
|
|
18
69
|
|
|
19
70
|
export interface ProcessRunResult {
|
|
20
71
|
aborted: boolean;
|
|
@@ -30,6 +81,34 @@ function appendTail(text: string, addition: string, maxChars: number): string {
|
|
|
30
81
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
31
82
|
}
|
|
32
83
|
|
|
84
|
+
export function buildAgentBrowserProcessEnv(
|
|
85
|
+
baseEnv: NodeJS.ProcessEnv = processEnv,
|
|
86
|
+
overrides: NodeJS.ProcessEnv | undefined = undefined,
|
|
87
|
+
): NodeJS.ProcessEnv {
|
|
88
|
+
const childEnv: NodeJS.ProcessEnv = {};
|
|
89
|
+
for (const [name, value] of Object.entries(baseEnv)) {
|
|
90
|
+
if (
|
|
91
|
+
value !== undefined &&
|
|
92
|
+
(INHERITED_ENV_NAMES.has(name) || INHERITED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
93
|
+
) {
|
|
94
|
+
childEnv[name] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!overrides) {
|
|
99
|
+
return childEnv;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
delete childEnv[name];
|
|
105
|
+
} else {
|
|
106
|
+
childEnv[name] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return childEnv;
|
|
110
|
+
}
|
|
111
|
+
|
|
33
112
|
export async function runAgentBrowserProcess(options: {
|
|
34
113
|
args: string[];
|
|
35
114
|
cwd: string;
|
|
@@ -55,6 +134,7 @@ export async function runAgentBrowserProcess(options: {
|
|
|
55
134
|
|
|
56
135
|
const queueStdoutChunk = (buffer: Buffer) => {
|
|
57
136
|
stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
|
|
137
|
+
if (stdoutSpillError) return;
|
|
58
138
|
if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
59
139
|
stdoutBuffers.push(buffer);
|
|
60
140
|
stdoutBufferedBytes += buffer.length;
|
|
@@ -63,17 +143,22 @@ export async function runAgentBrowserProcess(options: {
|
|
|
63
143
|
|
|
64
144
|
pendingStdoutWrite = pendingStdoutWrite
|
|
65
145
|
.then(async () => {
|
|
66
|
-
if (
|
|
146
|
+
if (stdoutSpillError) return;
|
|
147
|
+
if (!stdoutSpillHandle || !stdoutSpillPath) {
|
|
67
148
|
const tempFile = await openSecureTempFile(PROCESS_STDOUT_SPILL_FILE_PREFIX, ".json");
|
|
68
149
|
stdoutSpillHandle = tempFile.fileHandle;
|
|
69
150
|
stdoutSpillPath = tempFile.path;
|
|
70
151
|
if (stdoutBuffers.length > 0) {
|
|
71
|
-
await
|
|
152
|
+
await writeSecureTempChunk({
|
|
153
|
+
content: Buffer.concat(stdoutBuffers),
|
|
154
|
+
fileHandle: stdoutSpillHandle,
|
|
155
|
+
path: stdoutSpillPath,
|
|
156
|
+
});
|
|
72
157
|
stdoutBuffers = [];
|
|
73
158
|
stdoutBufferedBytes = 0;
|
|
74
159
|
}
|
|
75
160
|
}
|
|
76
|
-
await
|
|
161
|
+
await writeSecureTempChunk({ content: buffer, fileHandle: stdoutSpillHandle, path: stdoutSpillPath });
|
|
77
162
|
})
|
|
78
163
|
.catch((error) => {
|
|
79
164
|
stdoutSpillError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -106,7 +191,7 @@ export async function runAgentBrowserProcess(options: {
|
|
|
106
191
|
|
|
107
192
|
const child = spawn("agent-browser", args, {
|
|
108
193
|
cwd,
|
|
109
|
-
env:
|
|
194
|
+
env: buildAgentBrowserProcessEnv(processEnv, env),
|
|
110
195
|
stdio: ["pipe", "pipe", "pipe"],
|
|
111
196
|
});
|
|
112
197
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Parse upstream agent-browser output and turn failure envelopes into actionable error text.
|
|
3
|
+
* Responsibilities: Read inline or spilled stdout, parse observed JSON envelope shapes, normalize batch arrays, and extract the most useful error text from nested upstream failures.
|
|
4
|
+
* Scope: Envelope parsing and error derivation only; content rendering and snapshot compaction live in separate modules.
|
|
5
|
+
* Usage: Imported by the public `lib/results.ts` facade and by tests through that facade.
|
|
6
|
+
* Invariants/Assumptions: Upstream `agent-browser --json` responses follow the observed `{ success, data, error }` envelope shape or the array shape returned by `batch --json`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
|
|
11
|
+
import { type AgentBrowserBatchResult, type AgentBrowserEnvelope, isRecord, stringifyUnknown } from "./shared.js";
|
|
12
|
+
|
|
13
|
+
async function readEnvelopeSource(options: { stdout: string; stdoutPath?: string }): Promise<string> {
|
|
14
|
+
if (!options.stdoutPath) {
|
|
15
|
+
return options.stdout;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(options.stdoutPath, "utf8");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
throw new Error(`agent-browser output spill file could not be read: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractEnvelopeErrorText(error: unknown): string | undefined {
|
|
27
|
+
if (typeof error === "string") {
|
|
28
|
+
return error.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
if (typeof error === "number" || typeof error === "boolean") {
|
|
31
|
+
return String(error);
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(error)) {
|
|
34
|
+
const parts = error.map((item) => extractEnvelopeErrorText(item) ?? stringifyUnknown(item)).filter((item) => item.length > 0);
|
|
35
|
+
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
36
|
+
}
|
|
37
|
+
if (!isRecord(error)) {
|
|
38
|
+
return error == null ? undefined : stringifyUnknown(error);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const key of ["message", "error", "details", "cause", "stderr"] as const) {
|
|
42
|
+
const value = extractEnvelopeErrorText(error[key]);
|
|
43
|
+
if (value) return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fallback = stringifyUnknown(error).trim();
|
|
47
|
+
return fallback.length > 0 && fallback !== "{}" ? fallback : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function parseAgentBrowserEnvelope(options: string | { stdout: string; stdoutPath?: string }): Promise<{
|
|
51
|
+
envelope?: AgentBrowserEnvelope;
|
|
52
|
+
parseError?: string;
|
|
53
|
+
}> {
|
|
54
|
+
let stdout: string;
|
|
55
|
+
try {
|
|
56
|
+
stdout = typeof options === "string" ? options : await readEnvelopeSource(options);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { parseError: error instanceof Error ? error.message : String(error) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const trimmed = stdout.trim();
|
|
62
|
+
if (trimmed.length === 0) {
|
|
63
|
+
return { parseError: "agent-browser returned no JSON output." };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(trimmed) as AgentBrowserEnvelope | AgentBrowserBatchResult[];
|
|
68
|
+
if (Array.isArray(parsed)) {
|
|
69
|
+
return { envelope: { success: parsed.every((item) => !isRecord(item) || item.success !== false), data: parsed } };
|
|
70
|
+
}
|
|
71
|
+
if (!isRecord(parsed)) {
|
|
72
|
+
return { parseError: "agent-browser returned JSON, but it was not an object envelope." };
|
|
73
|
+
}
|
|
74
|
+
return { envelope: parsed };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
return { parseError: `agent-browser returned invalid JSON: ${message}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getAgentBrowserErrorText(options: {
|
|
82
|
+
aborted: boolean;
|
|
83
|
+
envelope?: AgentBrowserEnvelope;
|
|
84
|
+
exitCode: number;
|
|
85
|
+
parseError?: string;
|
|
86
|
+
plainTextInspection: boolean;
|
|
87
|
+
spawnError?: Error;
|
|
88
|
+
stderr: string;
|
|
89
|
+
}): string | undefined {
|
|
90
|
+
const { aborted, envelope, exitCode, parseError, plainTextInspection, spawnError, stderr } = options;
|
|
91
|
+
if (plainTextInspection) return undefined;
|
|
92
|
+
if (aborted) return "agent-browser was aborted.";
|
|
93
|
+
if (spawnError) return spawnError.message;
|
|
94
|
+
if (parseError) return parseError;
|
|
95
|
+
if (envelope?.success === false) {
|
|
96
|
+
return extractEnvelopeErrorText(envelope.error) ?? (stderr.trim() || `agent-browser reported failure${exitCode !== 0 ? ` (exit code ${exitCode})` : "."}`);
|
|
97
|
+
}
|
|
98
|
+
if (exitCode !== 0) {
|
|
99
|
+
return stderr.trim() || `agent-browser exited with code ${exitCode}.`;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|