pi-agent-browser-native 0.2.44 → 0.2.46
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 +42 -0
- package/README.md +20 -15
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +49 -27
- package/docs/ELECTRON.md +1 -1
- package/docs/RELEASE.md +6 -5
- package/docs/REQUIREMENTS.md +6 -3
- package/docs/SUPPORT_MATRIX.md +17 -13
- package/docs/TOOL_CONTRACT.md +87 -46
- package/docs/platform-smoke.md +4 -3
- package/extensions/agent-browser/index.ts +43 -450
- package/extensions/agent-browser/lib/bash-guard.ts +205 -0
- package/extensions/agent-browser/lib/electron/cdp.ts +69 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +5 -58
- package/extensions/agent-browser/lib/electron/discovery.ts +2 -9
- package/extensions/agent-browser/lib/electron/launch.ts +11 -65
- package/extensions/agent-browser/lib/electron/text.ts +13 -0
- package/extensions/agent-browser/lib/fs-utils.ts +18 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +207 -21
- package/extensions/agent-browser/lib/input-modes/params.ts +28 -11
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +22 -2
- package/extensions/agent-browser/lib/input-modes/types.ts +5 -1
- package/extensions/agent-browser/lib/input-modes.ts +1 -0
- package/extensions/agent-browser/lib/json-schema.ts +73 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +159 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +53 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +751 -32
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +38 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -46
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +10 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +28 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +15 -3
- package/extensions/agent-browser/lib/orchestration/output-file.ts +86 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +252 -0
- package/extensions/agent-browser/lib/playbook.ts +26 -26
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/prompt-policy.ts +1 -18
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +1 -4
- package/extensions/agent-browser/lib/results/artifact-state.ts +7 -3
- package/extensions/agent-browser/lib/results/contracts.ts +6 -2
- package/extensions/agent-browser/lib/results/envelope.ts +11 -2
- package/extensions/agent-browser/lib/results/network-routes.ts +7 -4
- package/extensions/agent-browser/lib/results/network.ts +7 -1
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +88 -20
- package/extensions/agent-browser/lib/results/presentation/batch.ts +84 -12
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +81 -26
- package/extensions/agent-browser/lib/results/presentation/errors.ts +13 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +60 -0
- package/extensions/agent-browser/lib/results/presentation.ts +10 -1
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +16 -5
- package/extensions/agent-browser/lib/results/snapshot.ts +2 -0
- package/extensions/agent-browser/lib/runtime.ts +10 -1
- package/extensions/agent-browser/lib/session-page-state.ts +15 -6
- package/extensions/agent-browser/lib/string-enum-schema.ts +20 -0
- package/extensions/agent-browser/lib/web-search.ts +31 -13
- package/package.json +2 -2
- package/platform-smoke.config.mjs +5 -2
- package/scripts/platform-smoke/build-ubuntu-image.mjs +25 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +5 -1
- package/scripts/platform-smoke/doctor.mjs +6 -2
- package/scripts/platform-smoke/linux-image/Dockerfile +3 -5
- package/scripts/platform-smoke/targets.mjs +2 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +0 -154
|
@@ -7,19 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ChildProcess } from "node:child_process";
|
|
10
|
-
import { readFile } from "node:fs/promises";
|
|
11
10
|
import { dirname, join, resolve } from "node:path";
|
|
12
11
|
import { fileURLToPath } from "node:url";
|
|
13
12
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type AgentToolResult,
|
|
19
|
-
type ExtensionAPI,
|
|
20
|
-
type ExtensionContext,
|
|
21
|
-
type Theme,
|
|
22
|
-
type ToolResultEvent,
|
|
13
|
+
import type {
|
|
14
|
+
AgentToolResult,
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
ExtensionContext,
|
|
23
17
|
} from "@earendil-works/pi-coding-agent";
|
|
24
18
|
import { Text } from "@earendil-works/pi-tui";
|
|
25
19
|
import {
|
|
@@ -42,12 +36,12 @@ import {
|
|
|
42
36
|
getImplicitSessionIdleTimeoutMs,
|
|
43
37
|
hasLaunchScopedTabCorrectionFlag,
|
|
44
38
|
extractExplicitSessionName,
|
|
45
|
-
redactInvocationArgs,
|
|
46
39
|
restoreManagedSessionStateFromBranch,
|
|
47
40
|
resolveManagedSessionState,
|
|
48
41
|
validateToolArgs,
|
|
49
42
|
type CompatibilityWorkaround,
|
|
50
43
|
} from "./lib/runtime.js";
|
|
44
|
+
import { isRecord } from "./lib/parsing.js";
|
|
51
45
|
import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
|
|
52
46
|
import { isCloseCommand } from "./lib/command-taxonomy.js";
|
|
53
47
|
import {
|
|
@@ -96,7 +90,8 @@ import {
|
|
|
96
90
|
restoreElectronLaunchRecordsFromBranch,
|
|
97
91
|
type ElectronLaunchRecord,
|
|
98
92
|
} from "./lib/orchestration/electron-host/index.js";
|
|
99
|
-
import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
|
|
93
|
+
import { buildValidationFailureResult, resolveAgentBrowserInput, type AgentBrowserExecuteParams } from "./lib/orchestration/input-plan.js";
|
|
94
|
+
import { applyAgentBrowserOutputPath } from "./lib/orchestration/output-file.js";
|
|
100
95
|
import type { NetworkRouteRecord } from "./lib/results/contracts.js";
|
|
101
96
|
import type { SessionArtifactManifest } from "./lib/results/contracts.js";
|
|
102
97
|
import {
|
|
@@ -121,154 +116,30 @@ import {
|
|
|
121
116
|
import { withOptionalSessionArgs } from "./lib/results/next-actions.js";
|
|
122
117
|
import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
|
|
123
118
|
import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
|
|
119
|
+
import {
|
|
120
|
+
isDirectAgentBrowserBashAllowed,
|
|
121
|
+
isHarmlessAgentBrowserInspectionCommand,
|
|
122
|
+
looksLikeDirectAgentBrowserBash,
|
|
123
|
+
} from "./lib/bash-guard.js";
|
|
124
|
+
import {
|
|
125
|
+
AgentBrowserResultComponent,
|
|
126
|
+
buildAgentBrowserToolResultPatch,
|
|
127
|
+
formatAgentBrowserRenderCall,
|
|
128
|
+
formatAgentBrowserRenderResult,
|
|
129
|
+
} from "./lib/pi-tool-rendering.js";
|
|
124
130
|
|
|
125
131
|
const DEFAULT_SESSION_MODE = "auto" as const;
|
|
126
|
-
const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
|
|
127
|
-
const PACKAGE_NAME = "pi-agent-browser-native";
|
|
128
|
-
|
|
129
|
-
const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
|
|
130
|
-
const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
|
|
131
|
-
const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
|
|
132
|
-
const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
|
|
133
|
-
|
|
134
|
-
function sanitizeDisplayText(text: string): string {
|
|
135
|
-
return text
|
|
136
|
-
.replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
|
|
137
|
-
.replace(/\r/g, "")
|
|
138
|
-
.replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function replaceTabsForDisplay(text: string): string {
|
|
142
|
-
return text.replaceAll("\t", " ");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
146
|
-
let end = lines.length;
|
|
147
|
-
while (end > 0 && lines[end - 1].trim().length === 0) {
|
|
148
|
-
end -= 1;
|
|
149
|
-
}
|
|
150
|
-
return lines.slice(0, end);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function isJsonDocumentText(text: string): boolean {
|
|
154
|
-
const trimmed = text.trim();
|
|
155
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
JSON.parse(trimmed);
|
|
160
|
-
return true;
|
|
161
|
-
} catch {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
|
|
167
|
-
const textContent = result.content.find((item) => item.type === "text");
|
|
168
|
-
return textContent?.type === "text" ? textContent.text : "";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function colorizeToolOutputLines(text: string, theme: Theme, isError: boolean): string[] {
|
|
172
|
-
const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(text)).split("\n"));
|
|
173
|
-
const normalizedText = normalizedLines.join("\n");
|
|
174
|
-
if (normalizedText.length === 0) {
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
if (isJsonDocumentText(normalizedText)) {
|
|
178
|
-
return highlightCode(normalizedText, "json");
|
|
179
|
-
}
|
|
180
|
-
return normalizedLines.map((line) => {
|
|
181
|
-
if (line.length === 0) {
|
|
182
|
-
return "";
|
|
183
|
-
}
|
|
184
|
-
return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatExpandHint(theme: Theme): string {
|
|
189
|
-
try {
|
|
190
|
-
return keyHint("app.tools.expand", "to expand");
|
|
191
|
-
} catch {
|
|
192
|
-
return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme): string {
|
|
197
|
-
return `${theme.fg("muted", `... (${remainingLines} more lines, ${totalLines} total, `)}${formatExpandHint(theme)}${theme.fg("muted", ")")}`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
201
|
-
const input = isRecord(args) ? args : {};
|
|
202
|
-
const semanticAction = compileAgentBrowserSemanticAction(input.semanticAction);
|
|
203
|
-
const job = compileAgentBrowserJob(input.job);
|
|
204
|
-
const qa = compileAgentBrowserQaPreset(input.qa);
|
|
205
|
-
const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
|
|
206
|
-
const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
|
|
207
|
-
const electron = compileAgentBrowserElectron(input.electron);
|
|
208
|
-
const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
|
|
209
|
-
const rawArgs = Array.isArray(input.args)
|
|
210
|
-
? input.args.filter((value): value is string => typeof value === "string")
|
|
211
|
-
: electron.compiled
|
|
212
|
-
? ["electron", electron.compiled.action]
|
|
213
|
-
: (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
|
|
214
|
-
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
215
|
-
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
216
|
-
const invocationPreview =
|
|
217
|
-
invocation.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
|
|
218
|
-
? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
|
|
219
|
-
: invocation;
|
|
220
|
-
let text = theme.fg("toolTitle", theme.bold("agent_browser"));
|
|
221
|
-
if (invocationPreview.length > 0) {
|
|
222
|
-
text += ` ${theme.fg("accent", invocationPreview)}`;
|
|
223
|
-
}
|
|
224
|
-
if (input.sessionMode === "fresh") {
|
|
225
|
-
text += theme.fg("dim", " sessionMode=fresh");
|
|
226
|
-
}
|
|
227
|
-
if (typeof input.stdin === "string") {
|
|
228
|
-
text += theme.fg("dim", " + stdin");
|
|
229
|
-
}
|
|
230
|
-
return text;
|
|
231
|
-
}
|
|
232
132
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
isError: boolean,
|
|
238
|
-
): string {
|
|
239
|
-
if (options.isPartial) {
|
|
240
|
-
return theme.fg("warning", "Running agent-browser...");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const outputText = getPrimaryTextContent(result);
|
|
244
|
-
const outputLines = colorizeToolOutputLines(outputText, theme, isError);
|
|
245
|
-
if (outputLines.length === 0) {
|
|
246
|
-
const details = isRecord(result.details) ? result.details : undefined;
|
|
247
|
-
const rawSummary = typeof details?.summary === "string" ? details.summary : isError ? "agent-browser failed" : "Done";
|
|
248
|
-
const sanitizedSummary = sanitizeDisplayText(rawSummary).trim();
|
|
249
|
-
const summary = sanitizedSummary.length > 0 ? sanitizedSummary : isError ? "agent-browser failed" : "Done";
|
|
250
|
-
return isError ? theme.fg("error", summary) : theme.fg("success", summary);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return `\n${outputLines.join("\n")}`;
|
|
254
|
-
}
|
|
133
|
+
type BashToolCallLike = {
|
|
134
|
+
input: { command: string };
|
|
135
|
+
toolName: "bash";
|
|
136
|
+
};
|
|
255
137
|
|
|
256
|
-
function
|
|
257
|
-
if (!isRecord(
|
|
258
|
-
|
|
259
|
-
? details.failureCategory
|
|
260
|
-
: undefined;
|
|
261
|
-
return `Result category: failure${failureCategory ? `; failureCategory: ${failureCategory}` : ""}; Pi tool isError: true.`;
|
|
138
|
+
function isBashToolCallEvent(event: unknown): event is BashToolCallLike {
|
|
139
|
+
if (!isRecord(event) || event.toolName !== "bash" || !isRecord(event.input)) return false;
|
|
140
|
+
return typeof event.input.command === "string";
|
|
262
141
|
}
|
|
263
142
|
|
|
264
|
-
type AgentBrowserToolContent = AgentToolResult<unknown>["content"];
|
|
265
|
-
type AgentBrowserToolContentItem = AgentBrowserToolContent[number];
|
|
266
|
-
|
|
267
|
-
type AgentBrowserToolResultPatch = {
|
|
268
|
-
content?: AgentBrowserToolContent;
|
|
269
|
-
isError?: boolean;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
143
|
type OwnedManagedSession = {
|
|
273
144
|
branchOwned: boolean;
|
|
274
145
|
cwd: string;
|
|
@@ -283,290 +154,7 @@ interface BranchManagedResourceEvents {
|
|
|
283
154
|
managedSessionCloseRanks: Map<string, number>;
|
|
284
155
|
}
|
|
285
156
|
|
|
286
|
-
function
|
|
287
|
-
const details = isRecord(event.details) ? event.details : undefined;
|
|
288
|
-
const detailArgs = Array.isArray(details?.args) ? details.args : undefined;
|
|
289
|
-
const inputArgs = isRecord(event.input) && Array.isArray(event.input.args) ? event.input.args : undefined;
|
|
290
|
-
return detailArgs?.includes("--json") === true || inputArgs?.includes("--json") === true;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function agentBrowserToolResultHasParseableJsonContent(content: AgentBrowserToolContent): boolean {
|
|
294
|
-
return content.some((item) => {
|
|
295
|
-
if (item.type !== "text" || typeof item.text !== "string") return false;
|
|
296
|
-
const text = item.text.trim();
|
|
297
|
-
if (text.length === 0) return false;
|
|
298
|
-
try {
|
|
299
|
-
JSON.parse(text);
|
|
300
|
-
return true;
|
|
301
|
-
} catch {
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function appendModelVisibleFailureCategoryNotice(content: AgentBrowserToolContent, notice: string): AgentBrowserToolContent | undefined {
|
|
308
|
-
const noticeContent: AgentBrowserToolContentItem = { type: "text", text: notice };
|
|
309
|
-
const textIndex = content.findIndex((item) => item.type === "text" && typeof item.text === "string");
|
|
310
|
-
if (textIndex === -1) return [noticeContent, ...content];
|
|
311
|
-
const textItem = content[textIndex];
|
|
312
|
-
if (textItem.type !== "text" || typeof textItem.text !== "string" || textItem.text.includes(notice)) return undefined;
|
|
313
|
-
return content.map((item, index) => index === textIndex
|
|
314
|
-
? { ...item, text: `${textItem.text}\n\n${notice}` }
|
|
315
|
-
: item);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function buildAgentBrowserToolResultPatch(event: ToolResultEvent): AgentBrowserToolResultPatch | undefined {
|
|
319
|
-
if (event.toolName !== "agent_browser") return undefined;
|
|
320
|
-
const preservesParseableJson = agentBrowserToolResultRequestedJson(event) && agentBrowserToolResultHasParseableJsonContent(event.content);
|
|
321
|
-
const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
|
|
322
|
-
const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
|
|
323
|
-
const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;
|
|
324
|
-
if (!shouldMarkError && !content) return undefined;
|
|
325
|
-
return {
|
|
326
|
-
...(content ? { content } : {}),
|
|
327
|
-
...(shouldMarkError ? { isError: true } : {}),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
class AgentBrowserResultComponent {
|
|
332
|
-
private expanded = false;
|
|
333
|
-
private theme: Theme | undefined;
|
|
334
|
-
private readonly text = new Text("", 0, 0);
|
|
335
|
-
|
|
336
|
-
setState(value: string, expanded: boolean, theme: Theme): void {
|
|
337
|
-
this.text.setText(value);
|
|
338
|
-
this.expanded = expanded;
|
|
339
|
-
this.theme = theme;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
render(width: number): string[] {
|
|
343
|
-
const lines = this.text.render(width);
|
|
344
|
-
if (this.expanded || lines.length <= TUI_COLLAPSED_OUTPUT_MAX_LINES) {
|
|
345
|
-
return lines;
|
|
346
|
-
}
|
|
347
|
-
const theme = this.theme;
|
|
348
|
-
if (!theme) {
|
|
349
|
-
return lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES);
|
|
350
|
-
}
|
|
351
|
-
const hiddenLineCount = lines.length - TUI_COLLAPSED_OUTPUT_MAX_LINES;
|
|
352
|
-
return [
|
|
353
|
-
...lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES),
|
|
354
|
-
formatVisualTruncationNotice(hiddenLineCount, lines.length, theme),
|
|
355
|
-
];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
invalidate(): void {
|
|
359
|
-
this.text.invalidate();
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN = /^(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser$/;
|
|
365
|
-
const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /^\s*(?:command\s+-v|which|type\s+-P)\s+agent-browser\s*$/;
|
|
366
|
-
|
|
367
|
-
type ShellQuoteState = 'double' | 'single' | undefined;
|
|
368
|
-
|
|
369
|
-
function isShellAssignmentToken(token: string): boolean {
|
|
370
|
-
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function stripOuterQuotes(token: string): string {
|
|
374
|
-
if (token.length >= 2 && ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'")))) {
|
|
375
|
-
return token.slice(1, -1);
|
|
376
|
-
}
|
|
377
|
-
return token;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function segmentLaunchesAgentBrowser(tokens: string[]): boolean {
|
|
381
|
-
let index = 0;
|
|
382
|
-
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
383
|
-
index += 1;
|
|
384
|
-
}
|
|
385
|
-
if (index >= tokens.length) {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let executableToken = tokens[index];
|
|
390
|
-
if (executableToken === 'env') {
|
|
391
|
-
index += 1;
|
|
392
|
-
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
393
|
-
index += 1;
|
|
394
|
-
}
|
|
395
|
-
executableToken = tokens[index] ?? '';
|
|
396
|
-
}
|
|
397
|
-
if (executableToken === 'npx' || executableToken === 'bunx') {
|
|
398
|
-
index += 1;
|
|
399
|
-
while (index < tokens.length && tokens[index].startsWith('-')) {
|
|
400
|
-
index += 1;
|
|
401
|
-
}
|
|
402
|
-
executableToken = tokens[index] ?? '';
|
|
403
|
-
}
|
|
404
|
-
if (executableToken === 'pnpm' || executableToken === 'yarn') {
|
|
405
|
-
index += 1;
|
|
406
|
-
if (tokens[index] !== 'dlx') {
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
index += 1;
|
|
410
|
-
while (index < tokens.length && tokens[index].startsWith('-')) {
|
|
411
|
-
index += 1;
|
|
412
|
-
}
|
|
413
|
-
executableToken = tokens[index] ?? '';
|
|
414
|
-
}
|
|
415
|
-
return DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN.test(executableToken);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Best-effort detection for common direct launches only. This is an ergonomics guard,
|
|
419
|
-
// not a general-purpose bash parser or security boundary.
|
|
420
|
-
function looksLikeDirectAgentBrowserBash(command: string): boolean {
|
|
421
|
-
let currentToken = '';
|
|
422
|
-
let quoteState: ShellQuoteState;
|
|
423
|
-
let awaitingHeredocDelimiter: { stripTabs: boolean } | undefined;
|
|
424
|
-
let pendingHeredoc: { delimiter: string; stripTabs: boolean } | undefined;
|
|
425
|
-
let pendingHeredocLine = '';
|
|
426
|
-
let segmentTokens: string[] = [];
|
|
427
|
-
|
|
428
|
-
const acceptToken = (token: string) => {
|
|
429
|
-
if (token.length === 0) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (awaitingHeredocDelimiter) {
|
|
433
|
-
pendingHeredoc = {
|
|
434
|
-
delimiter: stripOuterQuotes(token),
|
|
435
|
-
stripTabs: awaitingHeredocDelimiter.stripTabs,
|
|
436
|
-
};
|
|
437
|
-
awaitingHeredocDelimiter = undefined;
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
segmentTokens.push(token);
|
|
441
|
-
};
|
|
442
|
-
const flushToken = () => {
|
|
443
|
-
acceptToken(currentToken);
|
|
444
|
-
currentToken = '';
|
|
445
|
-
};
|
|
446
|
-
const flushSegment = () => {
|
|
447
|
-
const launchesAgentBrowser = segmentLaunchesAgentBrowser(segmentTokens);
|
|
448
|
-
segmentTokens = [];
|
|
449
|
-
return launchesAgentBrowser;
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
453
|
-
const char = command[index];
|
|
454
|
-
if (pendingHeredoc) {
|
|
455
|
-
if (char === '\n') {
|
|
456
|
-
const candidate = pendingHeredoc.stripTabs ? pendingHeredocLine.replace(/^\t+/, '') : pendingHeredocLine;
|
|
457
|
-
if (candidate === pendingHeredoc.delimiter) {
|
|
458
|
-
pendingHeredoc = undefined;
|
|
459
|
-
}
|
|
460
|
-
pendingHeredocLine = '';
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
pendingHeredocLine += char;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (quoteState === 'single') {
|
|
468
|
-
currentToken += char;
|
|
469
|
-
if (char === "'") {
|
|
470
|
-
quoteState = undefined;
|
|
471
|
-
}
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (quoteState === 'double') {
|
|
475
|
-
currentToken += char;
|
|
476
|
-
if (char === '\\' && index + 1 < command.length) {
|
|
477
|
-
currentToken += command[index + 1];
|
|
478
|
-
index += 1;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
if (char === '"') {
|
|
482
|
-
quoteState = undefined;
|
|
483
|
-
}
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (char === "'" || char === '"') {
|
|
487
|
-
currentToken += char;
|
|
488
|
-
quoteState = char === "'" ? 'single' : 'double';
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (char === '\\' && index + 1 < command.length) {
|
|
492
|
-
currentToken += char;
|
|
493
|
-
currentToken += command[index + 1];
|
|
494
|
-
index += 1;
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (char === '\n') {
|
|
498
|
-
flushToken();
|
|
499
|
-
if (flushSegment()) {
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
if (/\s/.test(char)) {
|
|
505
|
-
flushToken();
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const threeCharOperator = command.slice(index, index + 3);
|
|
509
|
-
if (threeCharOperator === '<<-') {
|
|
510
|
-
flushToken();
|
|
511
|
-
awaitingHeredocDelimiter = { stripTabs: true };
|
|
512
|
-
index += 2;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
const twoCharOperator = command.slice(index, index + 2);
|
|
516
|
-
if (twoCharOperator === '<<') {
|
|
517
|
-
flushToken();
|
|
518
|
-
awaitingHeredocDelimiter = { stripTabs: false };
|
|
519
|
-
index += 1;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (twoCharOperator === '&&' || twoCharOperator === '||') {
|
|
523
|
-
flushToken();
|
|
524
|
-
if (flushSegment()) {
|
|
525
|
-
return true;
|
|
526
|
-
}
|
|
527
|
-
index += 1;
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
if (char === '|' || char === ';' || char === '&') {
|
|
531
|
-
flushToken();
|
|
532
|
-
if (flushSegment()) {
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
currentToken += char;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
flushToken();
|
|
541
|
-
return flushSegment();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
|
|
545
|
-
return HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN.test(command);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function isTruthyEnvValue(value: string | undefined): boolean {
|
|
549
|
-
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
async function isPackageDevelopmentCwd(cwd: string): Promise<boolean> {
|
|
553
|
-
try {
|
|
554
|
-
const packageJson = JSON.parse(await readFile(join(cwd, "package.json"), "utf8")) as { name?: unknown };
|
|
555
|
-
return packageJson.name === PACKAGE_NAME;
|
|
556
|
-
} catch {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function isDirectAgentBrowserBashAllowed(cwd: string): Promise<boolean> {
|
|
562
|
-
return isTruthyEnvValue(process.env[DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV]) || await isPackageDevelopmentCwd(cwd);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
566
|
-
return typeof value === "object" && value !== null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function getBatchAnnotateValidationError(args: string[], stdin: string | undefined): string | undefined {
|
|
157
|
+
function getBatchPreflightValidationError(args: string[], stdin: string | undefined): string | undefined {
|
|
570
158
|
const commandTokens = extractCommandTokens(args);
|
|
571
159
|
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
572
160
|
return undefined;
|
|
@@ -575,14 +163,18 @@ function getBatchAnnotateValidationError(args: string[], stdin: string | undefin
|
|
|
575
163
|
if (parsed.error || parsed.steps === undefined) {
|
|
576
164
|
return undefined;
|
|
577
165
|
}
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
166
|
+
for (const [index, step] of parsed.steps.entries()) {
|
|
167
|
+
if (!Array.isArray(step) || !step.every((token) => typeof token === "string") || step.length === 0) continue;
|
|
168
|
+
const stepValidationError = validateToolArgs(step);
|
|
169
|
+
if (stepValidationError) return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
|
|
170
|
+
if (step[0] === "screenshot" && step.includes("--annotate")) {
|
|
171
|
+
return [
|
|
172
|
+
`Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
|
|
173
|
+
`Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
581
176
|
}
|
|
582
|
-
return
|
|
583
|
-
`Unsupported batch screenshot annotation in step ${badStepIndex + 1}: put --annotate in top-level args, not inside the batch step.`,
|
|
584
|
-
`Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
|
|
585
|
-
].join("\n");
|
|
177
|
+
return undefined;
|
|
586
178
|
}
|
|
587
179
|
|
|
588
180
|
function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
|
|
@@ -1170,7 +762,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1170
762
|
pi.on("tool_call", async (event, ctx) => {
|
|
1171
763
|
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
1172
764
|
if (
|
|
1173
|
-
|
|
765
|
+
isBashToolCallEvent(event) &&
|
|
1174
766
|
!promptPolicy.allowLegacyAgentBrowserBash &&
|
|
1175
767
|
looksLikeDirectAgentBrowserBash(event.input.command) &&
|
|
1176
768
|
!isHarmlessAgentBrowserInspectionCommand(event.input.command) &&
|
|
@@ -1206,10 +798,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1206
798
|
component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
|
|
1207
799
|
return component;
|
|
1208
800
|
},
|
|
1209
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
801
|
+
async execute(_toolCallId, params: AgentBrowserExecuteParams, signal, onUpdate, ctx) {
|
|
1210
802
|
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
803
|
+
const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
|
|
1211
804
|
const resolvedInput = resolveAgentBrowserInput({
|
|
1212
|
-
|
|
805
|
+
getBatchPreflightValidationError,
|
|
1213
806
|
managedSessionActive,
|
|
1214
807
|
params,
|
|
1215
808
|
});
|
|
@@ -1270,7 +863,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1270
863
|
? await managedSessionExecutionQueue.run(runElectronHostInput)
|
|
1271
864
|
: await runElectronHostInput();
|
|
1272
865
|
if (electronHostResult) {
|
|
1273
|
-
return electronHostResult;
|
|
866
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: electronHostResult });
|
|
1274
867
|
}
|
|
1275
868
|
|
|
1276
869
|
const explicitSessionName = extractExplicitSessionName(toolArgs);
|
|
@@ -1335,7 +928,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1335
928
|
});
|
|
1336
929
|
if (serializeBrowserCommand) branchStateGeneration += 1;
|
|
1337
930
|
}
|
|
1338
|
-
return result;
|
|
931
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
|
|
1339
932
|
};
|
|
1340
933
|
|
|
1341
934
|
return serializeBrowserCommand
|