pi-agent-browser-native 0.1.4 → 0.1.6
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 +15 -0
- package/docs/RELEASE.md +1 -1
- package/extensions/agent-browser/index.ts +66 -40
- 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 +249 -0
- package/extensions/agent-browser/lib/results/shared.ts +74 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +632 -0
- package/extensions/agent-browser/lib/results.ts +8 -934
- package/extensions/agent-browser/lib/runtime.ts +44 -12
- package/extensions/agent-browser/lib/temp.ts +159 -16
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 - 2026-04-12
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- hardened the implicit browser-session lifecycle so failed first launches no longer mark the convenience session active, startup-scoped flags behave correctly across launches and closes, and the highest-risk entrypoint paths now have direct automated and isolated-`pi` coverage
|
|
7
|
+
- added explicit temp-root ownership markers, aggregate spill-file disk budgeting, inline image size limits, and graceful fallback behavior when large snapshot or stdout artifacts exceed temp budgets
|
|
8
|
+
- consolidated the shared browser operating playbook across the injected system prompt and tool prompt guidance while adding direct extension-hook coverage for prompt injection, bash blocking, and session resets
|
|
9
|
+
- split the old result-rendering god module into focused envelope, presentation, shared, and snapshot modules, and made snapshot compaction fall back to a resilient outline mode when upstream raw snapshot formatting is unfamiliar
|
|
10
|
+
- refactored the release-package verification script into smaller testable helpers, preserved the retired autoload-shim guard, and aligned the tarball gate with the split result-rendering module layout
|
|
11
|
+
|
|
12
|
+
## 0.1.5 - 2026-04-12
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- pinned the transitive `basic-ftp` dependency to `5.2.2` via `overrides` so local development and GitHub install flows no longer pull the vulnerable `5.2.1` version through `@mariozechner/pi-coding-agent`
|
|
16
|
+
- kept the 0.1.4 startup fix and metadata updates intact while clearing the audit failure that surfaced during release verification
|
|
17
|
+
|
|
3
18
|
## 0.1.4 - 2026-04-12
|
|
4
19
|
|
|
5
20
|
### Changed
|
package/docs/RELEASE.md
CHANGED
|
@@ -31,7 +31,7 @@ npm run verify:release
|
|
|
31
31
|
- no repo-local `.pi/extensions/agent-browser.ts` autoload shim is present
|
|
32
32
|
- `LICENSE` exists in the repo and the packed tarball
|
|
33
33
|
- canonical published docs are present
|
|
34
|
-
- extension source files are present
|
|
34
|
+
- extension source files are present, including the split result-rendering modules required by the published facade
|
|
35
35
|
- agent-only and superseded docs are absent from the tarball
|
|
36
36
|
|
|
37
37
|
Current forbidden packed files include:
|
|
@@ -18,15 +18,15 @@ import {
|
|
|
18
18
|
buildPromptPolicy,
|
|
19
19
|
createEphemeralSessionSeed,
|
|
20
20
|
createImplicitSessionName,
|
|
21
|
+
getImplicitSessionCloseTimeoutMs,
|
|
22
|
+
getImplicitSessionIdleTimeoutMs,
|
|
21
23
|
getLatestUserPrompt,
|
|
22
24
|
hasUsableBraveApiKey,
|
|
25
|
+
resolveImplicitSessionActiveState,
|
|
23
26
|
validateToolArgs,
|
|
24
27
|
} from "./lib/runtime.js";
|
|
25
28
|
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
26
29
|
|
|
27
|
-
const IMPLICIT_SESSION_IDLE_TIMEOUT_MS = "900000";
|
|
28
|
-
const IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
|
|
29
|
-
|
|
30
30
|
const AGENT_BROWSER_PARAMS = Type.Object({
|
|
31
31
|
args: Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
|
|
32
32
|
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
|
|
@@ -40,6 +40,30 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
40
40
|
}),
|
|
41
41
|
),
|
|
42
42
|
});
|
|
43
|
+
const PROJECT_RULE_PROMPT =
|
|
44
|
+
"Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
|
|
45
|
+
const BRAVE_SEARCH_PROMPT_GUIDELINE =
|
|
46
|
+
"When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.";
|
|
47
|
+
const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
48
|
+
"Standard workflow: open the page, snapshot -i, interact using refs, and re-snapshot after navigation or major DOM changes.",
|
|
49
|
+
"For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
|
|
50
|
+
"Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
|
|
51
|
+
"When using --profile, --session-name, or --cdp, put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.",
|
|
52
|
+
"If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <n> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load, --url, --fn, or --text.",
|
|
53
|
+
"For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
|
|
54
|
+
"For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
|
|
55
|
+
"When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
|
|
56
|
+
"When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel.",
|
|
57
|
+
"Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
|
|
58
|
+
] as const;
|
|
59
|
+
const TOOL_PROMPT_GUIDELINES_PREFIX = ["Use this tool whenever the task requires a real browser or live web content."] as const;
|
|
60
|
+
const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
61
|
+
"Prefer this tool over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
|
|
62
|
+
"Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when this tool can do the job.",
|
|
63
|
+
"Pass exact agent-browser CLI arguments in args, excluding the binary name.",
|
|
64
|
+
"Use stdin for commands like eval --stdin and batch instead of shell heredocs.",
|
|
65
|
+
"Let the implicit session handle the common path unless you explicitly need upstream flags like --session, --profile, or --cdp.",
|
|
66
|
+
] as const;
|
|
43
67
|
|
|
44
68
|
function buildMissingBinaryMessage(): string {
|
|
45
69
|
return [
|
|
@@ -79,14 +103,38 @@ function buildInspectionDeflectionMessage(): string {
|
|
|
79
103
|
].join("\n");
|
|
80
104
|
}
|
|
81
105
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
function buildSharedBrowserPlaybookGuidelines(hasBraveApiKey: boolean): string[] {
|
|
107
|
+
return [
|
|
108
|
+
SHARED_BROWSER_PLAYBOOK_GUIDELINES[0],
|
|
109
|
+
...(hasBraveApiKey ? [BRAVE_SEARCH_PROMPT_GUIDELINE] : []),
|
|
110
|
+
...SHARED_BROWSER_PLAYBOOK_GUIDELINES.slice(1),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildBrowserSystemPromptAppendix(hasBraveApiKey: boolean): string {
|
|
115
|
+
return [
|
|
116
|
+
PROJECT_RULE_PROMPT,
|
|
117
|
+
"",
|
|
118
|
+
"Browser operating playbook:",
|
|
119
|
+
...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey).map((guideline) => `- ${guideline}`),
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildToolPromptGuidelines(hasBraveApiKey: boolean): string[] {
|
|
124
|
+
return [
|
|
125
|
+
...TOOL_PROMPT_GUIDELINES_PREFIX,
|
|
126
|
+
...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey),
|
|
127
|
+
...TOOL_PROMPT_GUIDELINES_SUFFIX,
|
|
128
|
+
];
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
88
132
|
const ephemeralSessionSeed = createEphemeralSessionSeed();
|
|
89
|
-
const
|
|
133
|
+
const hasBraveApiKey = hasUsableBraveApiKey();
|
|
134
|
+
const browserSystemPromptAppendix = buildBrowserSystemPromptAppendix(hasBraveApiKey);
|
|
135
|
+
const toolPromptGuidelines = buildToolPromptGuidelines(hasBraveApiKey);
|
|
136
|
+
const implicitSessionIdleTimeoutMs = getImplicitSessionIdleTimeoutMs();
|
|
137
|
+
const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
|
|
90
138
|
let implicitSessionActive = false;
|
|
91
139
|
let implicitSessionName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
|
|
92
140
|
let implicitSessionCwd = process.cwd();
|
|
@@ -100,7 +148,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
100
148
|
pi.on("session_shutdown", async () => {
|
|
101
149
|
implicitSessionActive = false;
|
|
102
150
|
const controller = new AbortController();
|
|
103
|
-
const timer = setTimeout(() => controller.abort(),
|
|
151
|
+
const timer = setTimeout(() => controller.abort(), implicitSessionCloseTimeoutMs);
|
|
104
152
|
try {
|
|
105
153
|
await runAgentBrowserProcess({
|
|
106
154
|
args: ["--session", implicitSessionName, "close"],
|
|
@@ -117,10 +165,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
117
165
|
|
|
118
166
|
pi.on("before_agent_start", async (event) => {
|
|
119
167
|
return {
|
|
120
|
-
systemPrompt:
|
|
121
|
-
event.systemPrompt +
|
|
122
|
-
"\n\nProject rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.\n\nBrowser operating playbook:\n- Standard workflow: open the page, then snapshot -i, then interact via refs, then re-snapshot after navigation or major DOM changes.\n- For user-specific or authenticated content like feeds, inboxes, dashboards, and accounts, start with an authenticated browser strategy instead of public browsing. Prefer `--profile Default` on the first browser call and let the current implicit session carry continuity. Use `--auto-connect` only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.\n- Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.\n- When using startup-scoped flags like `--profile`, `--session-name`, or `--cdp`, put them on the first command for that session. If you intentionally use an explicit `--session`, keep using that same explicit session for follow-ups.\n- If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an `open` call returns blocked, blank, or otherwise unexpected results, use `tab list`, `tab <n>`, and `snapshot -i` to recover state before retrying different URLs or fallback strategies. Only use `wait` with an explicit argument like milliseconds, `--load`, `--url`, `--fn`, or `--text`.\n- For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.\n- For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or `eval --stdin` on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.\n- When using `eval --stdin`, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.\n- When using `eval --stdin` for extraction, return the value you want instead of relying on `console.log` as the primary result channel.\n- Do not use `agent_browser --help` for normal browsing tasks." +
|
|
123
|
-
braveSearchGuidance,
|
|
168
|
+
systemPrompt: `${event.systemPrompt}\n\n${browserSystemPromptAppendix}`,
|
|
124
169
|
};
|
|
125
170
|
});
|
|
126
171
|
|
|
@@ -146,29 +191,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
146
191
|
"Browse and interact with websites using agent-browser. Use this for web research, reading live docs, opening pages, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work.",
|
|
147
192
|
promptSnippet:
|
|
148
193
|
"Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
|
|
149
|
-
promptGuidelines:
|
|
150
|
-
"Use this tool whenever the task requires a real browser or live web content.",
|
|
151
|
-
"Standard workflow: open the page, snapshot -i, interact using refs, and re-snapshot after navigation or major DOM changes.",
|
|
152
|
-
...(braveSearchGuidance
|
|
153
|
-
? [
|
|
154
|
-
"When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.",
|
|
155
|
-
]
|
|
156
|
-
: []),
|
|
157
|
-
"For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
|
|
158
|
-
"Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
|
|
159
|
-
"When using --profile, --session-name, or --cdp, put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.",
|
|
160
|
-
"If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <n> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load, --url, --fn, or --text.",
|
|
161
|
-
"For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
|
|
162
|
-
"For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
|
|
163
|
-
"When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
|
|
164
|
-
"When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel.",
|
|
165
|
-
"Prefer this tool over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
|
|
166
|
-
"Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
|
|
167
|
-
"Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when this tool can do the job.",
|
|
168
|
-
"Pass exact agent-browser CLI arguments in args, excluding the binary name.",
|
|
169
|
-
"Use stdin for commands like eval --stdin and batch instead of shell heredocs.",
|
|
170
|
-
"Let the implicit session handle the common path unless you explicitly need upstream flags like --session, --profile, or --cdp.",
|
|
171
|
-
],
|
|
194
|
+
promptGuidelines: toolPromptGuidelines,
|
|
172
195
|
parameters: AGENT_BROWSER_PARAMS,
|
|
173
196
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
174
197
|
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
@@ -221,16 +244,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
221
244
|
args: executionPlan.effectiveArgs,
|
|
222
245
|
cwd: ctx.cwd,
|
|
223
246
|
env: executionPlan.usedImplicitSession
|
|
224
|
-
? { AGENT_BROWSER_IDLE_TIMEOUT_MS:
|
|
247
|
+
? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs }
|
|
225
248
|
: undefined,
|
|
226
249
|
signal,
|
|
227
250
|
stdin: params.stdin,
|
|
228
251
|
});
|
|
229
252
|
|
|
230
|
-
if (executionPlan.usedImplicitSession && !processResult.aborted && !processResult.spawnError) {
|
|
231
|
-
implicitSessionActive = executionPlan.commandInfo.command !== "close";
|
|
232
|
-
}
|
|
233
|
-
|
|
234
253
|
if (processResult.spawnError?.message.includes("ENOENT")) {
|
|
235
254
|
const errorText = buildMissingBinaryMessage();
|
|
236
255
|
return {
|
|
@@ -255,6 +274,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
255
274
|
const parseSucceeded = plainTextInspection || parsed.parseError === undefined;
|
|
256
275
|
const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
257
276
|
|
|
277
|
+
implicitSessionActive = resolveImplicitSessionActiveState({
|
|
278
|
+
command: executionPlan.commandInfo.command,
|
|
279
|
+
priorActive: implicitSessionActive,
|
|
280
|
+
succeeded,
|
|
281
|
+
usedImplicitSession: executionPlan.usedImplicitSession,
|
|
282
|
+
});
|
|
283
|
+
|
|
258
284
|
const errorText = getAgentBrowserErrorText({
|
|
259
285
|
aborted: processResult.aborted,
|
|
260
286
|
envelope: parsed.envelope,
|
|
@@ -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
|
+
}
|