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.
@@ -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 (!stdoutSpillHandle) {
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 stdoutSpillHandle.writeFile(Buffer.concat(stdoutBuffers));
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 stdoutSpillHandle.writeFile(buffer);
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: { ...processEnv, ...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
+ }