pi-agent-browser-native 0.2.22 → 0.2.24
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 +16 -0
- package/README.md +207 -191
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/COMMAND_REFERENCE.md +6 -4
- package/docs/TOOL_CONTRACT.md +9 -2
- package/extensions/agent-browser/index.ts +282 -49
- package/extensions/agent-browser/lib/playbook.ts +4 -4
- package/extensions/agent-browser/lib/results/envelope.ts +14 -1
- package/extensions/agent-browser/lib/results/presentation.ts +5 -2
- package/extensions/agent-browser/lib/runtime.ts +53 -9
- package/package.json +1 -1
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -78,7 +78,7 @@ Examples:
|
|
|
78
78
|
|
|
79
79
|
- type: `string`
|
|
80
80
|
- optional
|
|
81
|
-
- raw stdin for `eval --stdin` and `
|
|
81
|
+
- raw stdin for `eval --stdin`, `batch`, and `auth save --password-stdin`
|
|
82
82
|
- rejected before launch for any other command/stdin combination, including commands such as `click`, `snapshot`, or `open`
|
|
83
83
|
|
|
84
84
|
Examples:
|
|
@@ -91,6 +91,10 @@ Examples:
|
|
|
91
91
|
{ "args": ["batch"], "stdin": "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
```json
|
|
95
|
+
{ "args": ["auth", "save", "my-login", "--password-stdin"], "stdin": "password from the user-approved secret source" }
|
|
96
|
+
```
|
|
97
|
+
|
|
94
98
|
### `sessionMode`
|
|
95
99
|
|
|
96
100
|
- type: `"auto" | "fresh"`
|
|
@@ -190,9 +194,12 @@ For oversized snapshots and other oversized tool outputs, details should switch
|
|
|
190
194
|
|
|
191
195
|
"Rendering" here means how results appear inside `pi`, not embedding a browser UI.
|
|
192
196
|
|
|
197
|
+
The TUI renderer is user-facing only. It may compact or colorize what the human sees in the Pi transcript, but it must not further truncate, summarize, or remove the model-facing `content` returned by the tool. Use the existing `details.fullOutputPath` / spill-file contracts for content that is too large for the model.
|
|
198
|
+
|
|
193
199
|
Worth doing in v1:
|
|
194
200
|
- screenshots → saved-path summary, visible artifact metadata, `details.artifacts` metadata, and inline image attachment when safe; screenshot paths that upstream would treat ambiguously, such as `.dogfood/run/foo.png`, are normalized to absolute paths before launch and repaired from upstream temp output when possible
|
|
195
201
|
- file artifacts such as PDFs, downloads, `wait --download` files, traces, CPU profiles, completed WebM recordings, and path-bearing HAR captures → concise saved-path summaries plus metadata in `details.artifacts` and bounded recent metadata in `details.artifactManifest`; `record start` reports recording lifecycle state and the future output path without adding a missing manifest entry; direct saved-file workflows also expose `details.savedFilePath` / `details.savedFile`; large or binary artifacts are not inlined into model context; the recent manifest cap can age out explicit-file metadata but does not remove explicit saved files from disk
|
|
202
|
+
- TUI display → custom `agent_browser` call/result rendering with colorized command/output text and a built-in-style collapsed view for long visible output; `ctrl+o` expansion reveals the full rendered tool result without changing the model-facing content
|
|
196
203
|
- snapshots → origin + ref count + main-content-first compact preview, with the raw snapshot spill path printed directly in content and kept in `details.fullOutputPath` plus `details.artifactManifest` when the inline result would otherwise be too large
|
|
197
204
|
- oversized generic outputs such as large `eval --stdin` payloads → compact preview plus the actual spill file path instead of dumping the whole payload into model context
|
|
198
205
|
- extraction-style commands like `eval --stdin` and `get title` → scalar-first text with lightweight origin context when available
|
|
@@ -222,7 +229,7 @@ If `agent-browser` is not on `PATH`, fail with a message that:
|
|
|
222
229
|
- reconstruct the current extension-managed session and latest `artifactManifest` from persisted tool details on resume/reload so later default calls keep following the active managed browser and can continue reporting artifact retention state
|
|
223
230
|
- when an unnamed `sessionMode: "fresh"` launch succeeds, make it the new extension-managed session so later default calls keep using it
|
|
224
231
|
- if that unnamed fresh launch replaced an already-active managed session, best-effort close the old managed session after the switch succeeds
|
|
225
|
-
- treat explicit caller-provided `--session` choices as user-managed
|
|
232
|
+
- treat explicit caller-provided `--session` choices as user-managed; `--session` isolates a live browser session but is not a persisted tab/auth restore mechanism after `close`, so use `--profile`, `--session-name`, or `--state` when persisted auth/tab state is required
|
|
226
233
|
- pass explicit `--profile` straight through to upstream `agent-browser`; no profile-cloning or isolation layer is added in v1
|
|
227
234
|
<!-- agent-browser-playbook:start wrapper-tab-recovery -->
|
|
228
235
|
<!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
|
|
@@ -10,7 +10,15 @@ import { copyFile, mkdir, readFile, rm, stat } from "node:fs/promises";
|
|
|
10
10
|
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
highlightCode,
|
|
15
|
+
isToolCallEventType,
|
|
16
|
+
keyHint,
|
|
17
|
+
type AgentToolResult,
|
|
18
|
+
type ExtensionAPI,
|
|
19
|
+
type Theme,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
14
22
|
import { Type } from "typebox";
|
|
15
23
|
|
|
16
24
|
import {
|
|
@@ -73,7 +81,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
73
81
|
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
|
|
74
82
|
minItems: 1,
|
|
75
83
|
}),
|
|
76
|
-
stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch and
|
|
84
|
+
stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch, eval --stdin, and auth save --password-stdin." })),
|
|
77
85
|
sessionMode: Type.Optional(
|
|
78
86
|
StringEnum(["auto", "fresh"] as const, {
|
|
79
87
|
description:
|
|
@@ -97,6 +105,154 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
|
|
|
97
105
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
|
|
109
|
+
const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
|
|
110
|
+
const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
|
|
111
|
+
const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
|
|
112
|
+
|
|
113
|
+
function sanitizeDisplayText(text: string): string {
|
|
114
|
+
return text
|
|
115
|
+
.replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
|
|
116
|
+
.replace(/\r/g, "")
|
|
117
|
+
.replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function replaceTabsForDisplay(text: string): string {
|
|
121
|
+
return text.replaceAll("\t", " ");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
125
|
+
let end = lines.length;
|
|
126
|
+
while (end > 0 && lines[end - 1].trim().length === 0) {
|
|
127
|
+
end -= 1;
|
|
128
|
+
}
|
|
129
|
+
return lines.slice(0, end);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isJsonDocumentText(text: string): boolean {
|
|
133
|
+
const trimmed = text.trim();
|
|
134
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
JSON.parse(trimmed);
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
|
|
146
|
+
const textContent = result.content.find((item) => item.type === "text");
|
|
147
|
+
return textContent?.type === "text" ? textContent.text : "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function colorizeToolOutputLines(text: string, theme: Theme, isError: boolean): string[] {
|
|
151
|
+
const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(text)).split("\n"));
|
|
152
|
+
const normalizedText = normalizedLines.join("\n");
|
|
153
|
+
if (normalizedText.length === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (isJsonDocumentText(normalizedText)) {
|
|
157
|
+
return highlightCode(normalizedText, "json");
|
|
158
|
+
}
|
|
159
|
+
return normalizedLines.map((line) => {
|
|
160
|
+
if (line.length === 0) {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatExpandHint(theme: Theme): string {
|
|
168
|
+
try {
|
|
169
|
+
return keyHint("app.tools.expand", "to expand");
|
|
170
|
+
} catch {
|
|
171
|
+
return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme): string {
|
|
176
|
+
return `${theme.fg("muted", `... (${remainingLines} more lines, ${totalLines} total, `)}${formatExpandHint(theme)}${theme.fg("muted", ")")}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
180
|
+
const input = isRecord(args) ? args : {};
|
|
181
|
+
const rawArgs = Array.isArray(input.args) ? input.args.filter((value): value is string => typeof value === "string") : [];
|
|
182
|
+
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
183
|
+
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
184
|
+
const invocationPreview =
|
|
185
|
+
invocation.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
|
|
186
|
+
? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
|
|
187
|
+
: invocation;
|
|
188
|
+
let text = theme.fg("toolTitle", theme.bold("agent_browser"));
|
|
189
|
+
if (invocationPreview.length > 0) {
|
|
190
|
+
text += ` ${theme.fg("accent", invocationPreview)}`;
|
|
191
|
+
}
|
|
192
|
+
if (input.sessionMode === "fresh") {
|
|
193
|
+
text += theme.fg("dim", " sessionMode=fresh");
|
|
194
|
+
}
|
|
195
|
+
if (typeof input.stdin === "string") {
|
|
196
|
+
text += theme.fg("dim", " + stdin");
|
|
197
|
+
}
|
|
198
|
+
return text;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatAgentBrowserRenderResult(
|
|
202
|
+
result: AgentToolResult<unknown>,
|
|
203
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
204
|
+
theme: Theme,
|
|
205
|
+
isError: boolean,
|
|
206
|
+
): string {
|
|
207
|
+
if (options.isPartial) {
|
|
208
|
+
return theme.fg("warning", "Running agent-browser...");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const outputText = getPrimaryTextContent(result);
|
|
212
|
+
const outputLines = colorizeToolOutputLines(outputText, theme, isError);
|
|
213
|
+
if (outputLines.length === 0) {
|
|
214
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
215
|
+
const rawSummary = typeof details?.summary === "string" ? details.summary : isError ? "agent-browser failed" : "Done";
|
|
216
|
+
const sanitizedSummary = sanitizeDisplayText(rawSummary).trim();
|
|
217
|
+
const summary = sanitizedSummary.length > 0 ? sanitizedSummary : isError ? "agent-browser failed" : "Done";
|
|
218
|
+
return isError ? theme.fg("error", summary) : theme.fg("success", summary);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return `\n${outputLines.join("\n")}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class AgentBrowserResultComponent {
|
|
225
|
+
private expanded = false;
|
|
226
|
+
private theme: Theme | undefined;
|
|
227
|
+
private readonly text = new Text("", 0, 0);
|
|
228
|
+
|
|
229
|
+
setState(value: string, expanded: boolean, theme: Theme): void {
|
|
230
|
+
this.text.setText(value);
|
|
231
|
+
this.expanded = expanded;
|
|
232
|
+
this.theme = theme;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render(width: number): string[] {
|
|
236
|
+
const lines = this.text.render(width);
|
|
237
|
+
if (this.expanded || lines.length <= TUI_COLLAPSED_OUTPUT_MAX_LINES) {
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
const theme = this.theme;
|
|
241
|
+
if (!theme) {
|
|
242
|
+
return lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES);
|
|
243
|
+
}
|
|
244
|
+
const hiddenLineCount = lines.length - TUI_COLLAPSED_OUTPUT_MAX_LINES;
|
|
245
|
+
return [
|
|
246
|
+
...lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES),
|
|
247
|
+
formatVisualTruncationNotice(hiddenLineCount, lines.length, theme),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
invalidate(): void {
|
|
252
|
+
this.text.invalidate();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
100
256
|
function buildWrapperRecoveryHint(options: {
|
|
101
257
|
pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
|
|
102
258
|
sessionTabCorrection?: OpenResultTabCorrection;
|
|
@@ -936,6 +1092,45 @@ function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactMa
|
|
|
936
1092
|
return restoredManifest;
|
|
937
1093
|
}
|
|
938
1094
|
|
|
1095
|
+
function isPasswordStdinAuthSave(options: { command?: string; commandTokens: string[] }): boolean {
|
|
1096
|
+
return options.command === "auth" && options.commandTokens[1] === "save" && options.commandTokens.includes("--password-stdin");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function getExactSensitiveStdinValues(options: { command?: string; commandTokens: string[]; stdin?: string }): string[] {
|
|
1100
|
+
if (options.stdin === undefined || !isPasswordStdinAuthSave(options)) {
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
return [...new Set([options.stdin, options.stdin.trimEnd(), options.stdin.trim()].filter((value) => value.length > 0))];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function redactExactSensitiveText(text: string, sensitiveValues: string[]): string {
|
|
1107
|
+
let redacted = text;
|
|
1108
|
+
for (const value of sensitiveValues) {
|
|
1109
|
+
redacted = redacted.split(value).join("[REDACTED]");
|
|
1110
|
+
}
|
|
1111
|
+
return redacted;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function redactExactSensitiveValue(value: unknown, sensitiveValues: string[]): unknown {
|
|
1115
|
+
if (sensitiveValues.length === 0) {
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
if (typeof value === "string") {
|
|
1119
|
+
return redactExactSensitiveText(value, sensitiveValues);
|
|
1120
|
+
}
|
|
1121
|
+
if (Array.isArray(value)) {
|
|
1122
|
+
return value.map((item) => redactExactSensitiveValue(item, sensitiveValues));
|
|
1123
|
+
}
|
|
1124
|
+
if (!isRecord(value)) {
|
|
1125
|
+
return value;
|
|
1126
|
+
}
|
|
1127
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactSensitiveValue(entryValue, sensitiveValues)]));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function redactToolDetails(details: Record<string, unknown>, sensitiveValues: string[]): Record<string, unknown> {
|
|
1131
|
+
return redactSensitiveValue(redactExactSensitiveValue(details, sensitiveValues)) as Record<string, unknown>;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
939
1134
|
function validateStdinCommandContract(options: { command?: string; commandTokens: string[]; stdin?: string }): string | undefined {
|
|
940
1135
|
if (options.stdin === undefined) {
|
|
941
1136
|
return undefined;
|
|
@@ -946,8 +1141,11 @@ function validateStdinCommandContract(options: { command?: string; commandTokens
|
|
|
946
1141
|
if (options.command === "eval" && options.commandTokens.includes("--stdin")) {
|
|
947
1142
|
return undefined;
|
|
948
1143
|
}
|
|
1144
|
+
if (isPasswordStdinAuthSave(options)) {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
949
1147
|
const commandLabel = options.command ? `\`${options.command}\`` : "the requested command";
|
|
950
|
-
return `agent_browser stdin is only supported for \`batch\` and \`
|
|
1148
|
+
return `agent_browser stdin is only supported for \`batch\`, \`eval --stdin\`, and \`auth save --password-stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
|
|
951
1149
|
}
|
|
952
1150
|
|
|
953
1151
|
function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
|
|
@@ -1029,6 +1227,17 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
|
|
|
1029
1227
|
}
|
|
1030
1228
|
}
|
|
1031
1229
|
|
|
1230
|
+
function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
1231
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
1232
|
+
return commandTokens;
|
|
1233
|
+
}
|
|
1234
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
1235
|
+
if (parsed.error || parsed.steps === undefined) {
|
|
1236
|
+
return commandTokens;
|
|
1237
|
+
}
|
|
1238
|
+
return parsed.steps.flatMap((step) => step);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1032
1241
|
function buildPinnedBatchPlan(options: {
|
|
1033
1242
|
command?: string;
|
|
1034
1243
|
commandTokens: string[];
|
|
@@ -1293,6 +1502,7 @@ function getPersistentSessionArtifactStore(ctx: {
|
|
|
1293
1502
|
|
|
1294
1503
|
async function preserveParseFailureOutput(options: {
|
|
1295
1504
|
artifactManifest?: SessionArtifactManifest;
|
|
1505
|
+
exactSensitiveValues?: string[];
|
|
1296
1506
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
1297
1507
|
stdoutSpillPath?: string;
|
|
1298
1508
|
}): Promise<{
|
|
@@ -1306,7 +1516,7 @@ async function preserveParseFailureOutput(options: {
|
|
|
1306
1516
|
}
|
|
1307
1517
|
|
|
1308
1518
|
try {
|
|
1309
|
-
const rawOutput = await readFile(options.stdoutSpillPath);
|
|
1519
|
+
const rawOutput = redactExactSensitiveText(await readFile(options.stdoutSpillPath, "utf8"), options.exactSensitiveValues ?? []);
|
|
1310
1520
|
const nowMs = Date.now();
|
|
1311
1521
|
let evictedArtifacts: PersistentSessionArtifactEviction[] = [];
|
|
1312
1522
|
let fullOutputPath: string;
|
|
@@ -1500,6 +1710,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1500
1710
|
"Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
|
|
1501
1711
|
promptGuidelines: toolPromptGuidelines,
|
|
1502
1712
|
parameters: AGENT_BROWSER_PARAMS,
|
|
1713
|
+
renderCall(args, theme, context) {
|
|
1714
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1715
|
+
text.setText(formatAgentBrowserRenderCall(args, theme));
|
|
1716
|
+
return text;
|
|
1717
|
+
},
|
|
1718
|
+
renderResult(result, options, theme, context) {
|
|
1719
|
+
const component = context.lastComponent instanceof AgentBrowserResultComponent
|
|
1720
|
+
? context.lastComponent
|
|
1721
|
+
: new AgentBrowserResultComponent();
|
|
1722
|
+
component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
|
|
1723
|
+
return component;
|
|
1724
|
+
},
|
|
1503
1725
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1504
1726
|
const redactedArgs = redactInvocationArgs(params.args);
|
|
1505
1727
|
const validationError = validateToolArgs(params.args) ?? getBatchAnnotateValidationError(params.args, params.stdin);
|
|
@@ -1546,6 +1768,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1546
1768
|
}
|
|
1547
1769
|
|
|
1548
1770
|
const commandTokens = extractCommandTokens(preparedArgs.args);
|
|
1771
|
+
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
1772
|
+
command: executionPlan.commandInfo.command,
|
|
1773
|
+
commandTokens,
|
|
1774
|
+
stdin: params.stdin,
|
|
1775
|
+
});
|
|
1549
1776
|
const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
|
|
1550
1777
|
command: executionPlan.commandInfo.command,
|
|
1551
1778
|
sessionName: executionPlan.sessionName,
|
|
@@ -1755,9 +1982,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1755
1982
|
presentationEnvelope = repairedBatchScreenshots.envelope;
|
|
1756
1983
|
const screenshotArtifactRequest = repairedScreenshot.request;
|
|
1757
1984
|
const batchScreenshotArtifactRequests = repairedBatchScreenshots.requests;
|
|
1985
|
+
if (presentationEnvelope && exactSensitiveValues.length > 0) {
|
|
1986
|
+
presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, exactSensitiveValues) as AgentBrowserEnvelope;
|
|
1987
|
+
}
|
|
1758
1988
|
const parseFailureOutput = parseError
|
|
1759
1989
|
? await preserveParseFailureOutput({
|
|
1760
1990
|
artifactManifest,
|
|
1991
|
+
exactSensitiveValues,
|
|
1761
1992
|
persistentArtifactStore,
|
|
1762
1993
|
stdoutSpillPath: processResult.stdoutSpillPath,
|
|
1763
1994
|
})
|
|
@@ -1934,6 +2165,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1934
2165
|
exitCode: processResult.exitCode,
|
|
1935
2166
|
parseError,
|
|
1936
2167
|
plainTextInspection,
|
|
2168
|
+
staleRefArgs: getStaleRefArgs(commandTokens, params.stdin),
|
|
1937
2169
|
spawnError: processResult.spawnError,
|
|
1938
2170
|
stderr: processResult.stderr,
|
|
1939
2171
|
timedOut: processResult.timedOut,
|
|
@@ -2009,54 +2241,55 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2009
2241
|
contentWithSessionWarnings.unshift({ type: "text", text: warningText });
|
|
2010
2242
|
}
|
|
2011
2243
|
}
|
|
2012
|
-
const redactedContent = contentWithSessionWarnings.map((item) =>
|
|
2013
|
-
item.type
|
|
2014
|
-
|
|
2244
|
+
const redactedContent = contentWithSessionWarnings.map((item) => {
|
|
2245
|
+
if (item.type !== "text") return item;
|
|
2246
|
+
const exactRedactedText = redactExactSensitiveText(item.text, exactSensitiveValues);
|
|
2247
|
+
return userRequestedJson && !plainTextInspection
|
|
2248
|
+
? { ...item, text: exactRedactedText }
|
|
2249
|
+
: { ...item, text: redactSensitiveText(exactRedactedText) };
|
|
2250
|
+
});
|
|
2251
|
+
const details = {
|
|
2252
|
+
args: redactedArgs,
|
|
2253
|
+
artifactManifest: presentation.artifactManifest,
|
|
2254
|
+
artifactRetentionSummary: presentation.artifactRetentionSummary,
|
|
2255
|
+
artifacts: presentation.artifacts,
|
|
2256
|
+
batchFailure: presentation.batchFailure,
|
|
2257
|
+
batchSteps: presentation.batchSteps,
|
|
2258
|
+
command: executionPlan.commandInfo.command,
|
|
2259
|
+
compatibilityWorkaround,
|
|
2260
|
+
subcommand: executionPlan.commandInfo.subcommand,
|
|
2261
|
+
data: presentation.data,
|
|
2262
|
+
error: plainTextInspection ? undefined : presentationEnvelope?.error,
|
|
2263
|
+
inspection: plainTextInspection || undefined,
|
|
2264
|
+
navigationSummary,
|
|
2265
|
+
aboutBlankSessionMismatch,
|
|
2266
|
+
openResultTabCorrection,
|
|
2267
|
+
effectiveArgs: redactedProcessArgs,
|
|
2268
|
+
exitCode: processResult.exitCode,
|
|
2269
|
+
fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
|
|
2270
|
+
fullOutputPaths: presentation.fullOutputPaths,
|
|
2271
|
+
fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
|
|
2272
|
+
imagePath: presentation.imagePath,
|
|
2273
|
+
imagePaths: presentation.imagePaths,
|
|
2274
|
+
parseError: plainTextInspection ? undefined : parseError,
|
|
2275
|
+
savedFile: presentation.savedFile,
|
|
2276
|
+
savedFilePath: presentation.savedFilePath,
|
|
2277
|
+
sessionMode,
|
|
2278
|
+
sessionTabCorrection,
|
|
2279
|
+
sessionTabTarget: currentSessionTabTarget,
|
|
2280
|
+
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
2281
|
+
sessionRecoveryHint: redactedRecoveryHint,
|
|
2282
|
+
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
2283
|
+
stderr: processResult.stderr,
|
|
2284
|
+
stdout: plainTextInspection ? inspectionText ?? "" : parseSucceeded ? undefined : processResult.stdout,
|
|
2285
|
+
summary: presentation.summary,
|
|
2286
|
+
timedOut: processResult.timedOut || undefined,
|
|
2287
|
+
timeoutMs: processResult.timeoutMs,
|
|
2288
|
+
};
|
|
2015
2289
|
|
|
2016
2290
|
return {
|
|
2017
2291
|
content: redactedContent,
|
|
2018
|
-
details:
|
|
2019
|
-
args: redactedArgs,
|
|
2020
|
-
artifactManifest: redactSensitiveValue(presentation.artifactManifest),
|
|
2021
|
-
artifactRetentionSummary: presentation.artifactRetentionSummary,
|
|
2022
|
-
artifacts: redactSensitiveValue(presentation.artifacts),
|
|
2023
|
-
batchFailure: redactSensitiveValue(presentation.batchFailure),
|
|
2024
|
-
batchSteps: redactSensitiveValue(presentation.batchSteps),
|
|
2025
|
-
command: executionPlan.commandInfo.command,
|
|
2026
|
-
compatibilityWorkaround,
|
|
2027
|
-
subcommand: executionPlan.commandInfo.subcommand,
|
|
2028
|
-
data: redactSensitiveValue(presentation.data),
|
|
2029
|
-
error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
|
|
2030
|
-
inspection: plainTextInspection || undefined,
|
|
2031
|
-
navigationSummary: redactSensitiveValue(navigationSummary),
|
|
2032
|
-
aboutBlankSessionMismatch: redactSensitiveValue(aboutBlankSessionMismatch),
|
|
2033
|
-
openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
|
|
2034
|
-
effectiveArgs: redactedProcessArgs,
|
|
2035
|
-
exitCode: processResult.exitCode,
|
|
2036
|
-
fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
|
|
2037
|
-
fullOutputPaths: presentation.fullOutputPaths,
|
|
2038
|
-
fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
|
|
2039
|
-
imagePath: presentation.imagePath,
|
|
2040
|
-
imagePaths: presentation.imagePaths,
|
|
2041
|
-
parseError: plainTextInspection ? undefined : parseError,
|
|
2042
|
-
savedFile: redactSensitiveValue(presentation.savedFile),
|
|
2043
|
-
savedFilePath: presentation.savedFilePath ? redactSensitiveText(presentation.savedFilePath) : undefined,
|
|
2044
|
-
sessionMode,
|
|
2045
|
-
sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
|
|
2046
|
-
sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
|
|
2047
|
-
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
2048
|
-
sessionRecoveryHint: redactedRecoveryHint,
|
|
2049
|
-
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
2050
|
-
stderr: processResult.stderr ? redactSensitiveText(processResult.stderr) : undefined,
|
|
2051
|
-
stdout: plainTextInspection
|
|
2052
|
-
? redactSensitiveText(inspectionText ?? "")
|
|
2053
|
-
: parseSucceeded
|
|
2054
|
-
? undefined
|
|
2055
|
-
: redactSensitiveText(processResult.stdout),
|
|
2056
|
-
summary: redactSensitiveText(presentation.summary),
|
|
2057
|
-
timedOut: processResult.timedOut || undefined,
|
|
2058
|
-
timeoutMs: processResult.timeoutMs,
|
|
2059
|
-
},
|
|
2292
|
+
details: redactToolDetails(details, exactSensitiveValues),
|
|
2060
2293
|
isError: !succeeded,
|
|
2061
2294
|
};
|
|
2062
2295
|
} finally {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Responsibilities: Define stable guidance bullets, native tool-call examples, and wrapper-behavior notes without importing runtime/browser process code.
|
|
4
4
|
* Scope: Agent-facing documentation and prompt-guidance text only; command execution and wrapper state behavior live in runtime modules.
|
|
5
5
|
* Usage: Imported by the extension entrypoint for promptGuidelines and by the documentation drift-check script for generated Markdown blocks.
|
|
6
|
-
* Invariants/Assumptions: The native pi tool receives args after the agent-browser binary, stdin is only for batch/eval --stdin, and wrapper behavior documented here must match implemented behavior.
|
|
6
|
+
* Invariants/Assumptions: The native pi tool receives args after the agent-browser binary, stdin is only for batch/eval --stdin/auth save --password-stdin, and wrapper behavior documented here must match implemented behavior.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const PROJECT_RULE_PROMPT =
|
|
@@ -14,9 +14,9 @@ export const TOOL_PROMPT_GUIDELINES_PREFIX = [
|
|
|
14
14
|
] as const;
|
|
15
15
|
|
|
16
16
|
export const QUICK_START_GUIDELINES = [
|
|
17
|
-
"Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch and
|
|
17
|
+
"Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch, eval --stdin, and auth save --password-stdin, and other command/stdin combinations are rejected before launch; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable state.",
|
|
18
18
|
"Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
|
|
19
|
-
"Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }.",
|
|
19
|
+
"Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"auth\", \"save\", \"name\", \"--password-stdin\"], stdin: \"<password from user-approved secret source>\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }.",
|
|
20
20
|
"High-value command reference: download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [path] captures an image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree/inspect/renders/suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation.",
|
|
21
21
|
"For artifact-producing commands, read the visible artifact block for requested path, absolute path, existence, size, type, cwd, and session; details.artifacts contains the same machine-readable metadata. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step.",
|
|
22
22
|
] as const;
|
|
@@ -47,7 +47,7 @@ export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
|
47
47
|
"Prefer agent_browser over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
|
|
48
48
|
"Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when agent_browser can do the job.",
|
|
49
49
|
"Pass exact agent-browser CLI arguments in args, excluding the binary name.",
|
|
50
|
-
"Use stdin only for eval --stdin and
|
|
50
|
+
"Use stdin only for eval --stdin, batch, and auth save --password-stdin instead of shell heredocs or password args; other command/stdin combinations are rejected before launch.",
|
|
51
51
|
"Let the extension-managed session handle the common path unless you explicitly need a fresh launch for upstream flags like --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable.",
|
|
52
52
|
"Use sessionMode=fresh when switching from an existing implicit session to a new profile/debug/init-script launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
|
|
53
53
|
] as const;
|
|
@@ -135,6 +135,17 @@ function buildUpstreamIpcReadTimeoutMessage(): string {
|
|
|
135
135
|
].join(" ");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function maybeAppendStaleRefHint(message: string, args?: string[]): string {
|
|
139
|
+
const usedRef = args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
|
|
140
|
+
if (!usedRef || !/could not locate element|element not found|no element/i.test(message)) {
|
|
141
|
+
return message;
|
|
142
|
+
}
|
|
143
|
+
return [
|
|
144
|
+
message,
|
|
145
|
+
"This @ref may be stale after navigation, scrolling, or a DOM update. Run `agent_browser` with `{ \"args\": [\"snapshot\", \"-i\"] }` again and retry with a current ref, or use a stable `find` locator.",
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
export function getAgentBrowserErrorText(options: {
|
|
139
150
|
aborted: boolean;
|
|
140
151
|
command?: string;
|
|
@@ -144,6 +155,7 @@ export function getAgentBrowserErrorText(options: {
|
|
|
144
155
|
parseError?: string;
|
|
145
156
|
plainTextInspection: boolean;
|
|
146
157
|
spawnError?: Error;
|
|
158
|
+
staleRefArgs?: string[];
|
|
147
159
|
stderr: string;
|
|
148
160
|
timedOut?: boolean;
|
|
149
161
|
timeoutMs?: number;
|
|
@@ -163,7 +175,8 @@ export function getAgentBrowserErrorText(options: {
|
|
|
163
175
|
if (envelopeErrorText && isUpstreamIpcReadTimeoutMessage(envelopeErrorText)) {
|
|
164
176
|
return buildUpstreamIpcReadTimeoutMessage();
|
|
165
177
|
}
|
|
166
|
-
|
|
178
|
+
const fallback = envelopeErrorText ?? (stderr.trim() || buildFailureFallback(options));
|
|
179
|
+
return maybeAppendStaleRefHint(fallback, options.staleRefArgs ?? options.effectiveArgs);
|
|
167
180
|
}
|
|
168
181
|
if (exitCode !== 0) {
|
|
169
182
|
return stderr.trim() || buildExitCodeFallback(options);
|
|
@@ -349,6 +349,9 @@ function splitShellWords(input: string): string[] | undefined {
|
|
|
349
349
|
current += input[index];
|
|
350
350
|
continue;
|
|
351
351
|
}
|
|
352
|
+
if (char === "#" && current.length === 0) {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
352
355
|
if (/\s/.test(char)) {
|
|
353
356
|
if (current.length > 0) {
|
|
354
357
|
words.push(current);
|
|
@@ -384,7 +387,7 @@ function formatNativeSkillContent(content: string): string {
|
|
|
384
387
|
const heredocMatch = /^(.*?)\s+(<<-?)['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/.exec(rawArgsText);
|
|
385
388
|
const argsText = heredocMatch?.[1] ?? rawArgsText;
|
|
386
389
|
const args = splitShellWords(argsText);
|
|
387
|
-
if (!args) {
|
|
390
|
+
if (!args || args.length === 0) {
|
|
388
391
|
output.push(line);
|
|
389
392
|
continue;
|
|
390
393
|
}
|
|
@@ -419,7 +422,7 @@ function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | und
|
|
|
419
422
|
if (content) {
|
|
420
423
|
const note = [
|
|
421
424
|
"Pi native-tool note: upstream skill text was adapted for this native tool.",
|
|
422
|
-
"Use args for CLI tokens and stdin only for batch or
|
|
425
|
+
"Use args for CLI tokens and stdin only for batch, eval --stdin, or auth save --password-stdin; do not pipe heredocs through bash unless the user explicitly asks for a bash workflow.",
|
|
423
426
|
].join("\n");
|
|
424
427
|
return `${note}\n\n${redactModelFacingText(formatNativeSkillContent(content))}`;
|
|
425
428
|
}
|