pi-agent-browser-native 0.2.1 → 0.2.2
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 +12 -2
- package/README.md +4 -3
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/TOOL_CONTRACT.md +16 -2
- package/extensions/agent-browser/index.ts +88 -54
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/runtime.ts +235 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 - 2026-04-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- plain-text inspection commands like `agent_browser --help` and `--version` now stay stateless: they no longer claim the implicit managed session or leave behind ambiguous `parseError` details on success
|
|
7
|
+
- extension-managed session ownership is now reconstructed from persisted tool details on resume/reload while still preserving cwd-hash isolation across same-named checkouts and worktrees
|
|
8
|
+
- echoed tool updates/details now redact sensitive invocation values and structured secret-bearing fields instead of replaying headers, proxy credentials, cookies, or auth-bearing URL params back into `pi`
|
|
9
|
+
- the subprocess wrapper no longer forwards ambient parent-shell `AGENT_BROWSER_*` state into child runs, reducing surprising hidden configuration leaks from the caller environment
|
|
10
|
+
- browser-specific system-prompt injection is now minimal and only added for clearly browser-oriented turns, while the full playbook stays in tool metadata where it belongs
|
|
11
|
+
- published docs and changelog notes now match the current result/details contract, resume behavior, prompt behavior, and release workflow
|
|
12
|
+
|
|
3
13
|
## 0.2.1 - 2026-04-12
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -18,7 +28,7 @@
|
|
|
18
28
|
- plain-text inspection commands like `agent_browser --help` and `--version` are now always allowed, removing the old prompt-dependent inspection gate and making the inspection contract local and predictable
|
|
19
29
|
- navigation actions like `click`, `dblclick`, `back`, `forward`, and `reload` now include lightweight post-action title/url summaries when the wrapper can address the active session, reducing guess-and-check follow-up snapshots
|
|
20
30
|
- compact snapshot rendering is leaner by default: fewer additional sections, fewer refs, smaller role summaries, and the raw spill path now stays in `details.fullOutputPath` instead of dominating the visible snapshot body
|
|
21
|
-
- README and
|
|
31
|
+
- README and tool prompt guidance now include a compact agent quick start with the core call shapes for `open` + `snapshot`, `click` + re-snapshot, `batch`, `eval --stdin`, and fresh profiled launches, while turn-level system-prompt injection stays minimal
|
|
22
32
|
|
|
23
33
|
### Migration notes
|
|
24
34
|
- replace any use of `useActiveSession` with `sessionMode`
|
|
@@ -29,7 +39,7 @@
|
|
|
29
39
|
### Changed
|
|
30
40
|
- 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
|
|
31
41
|
- 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
|
|
32
|
-
- consolidated the shared browser operating playbook
|
|
42
|
+
- consolidated the shared browser operating playbook into the tool prompt guidance while keeping turn-level system-prompt injection minimal, and added direct extension-hook coverage for prompt injection, bash blocking, and session resets
|
|
33
43
|
- 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
|
|
34
44
|
- 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
|
|
35
45
|
|
package/README.md
CHANGED
|
@@ -181,16 +181,17 @@ Validated workflow examples:
|
|
|
181
181
|
- run `batch` with JSON via `stdin`
|
|
182
182
|
- run `eval --stdin`
|
|
183
183
|
- take a screenshot with inline attachment support
|
|
184
|
-
- inspect `agent_browser --help` and `--version` via the tool's plain-text inspection fallback
|
|
184
|
+
- inspect `agent_browser --help` and `--version` via the tool's stateless plain-text inspection fallback
|
|
185
185
|
|
|
186
|
-
Inspection commands like `agent_browser --help` and `--version` are always supported. They return plain text
|
|
186
|
+
Inspection commands like `agent_browser --help` and `--version` are always supported. They return plain text, are useful for debugging or capability checks, and stay stateless: the extension does not inject its implicit session for them and they do not consume the managed-session slot needed for a later `--profile`, `--session-name`, or `--cdp` launch.
|
|
187
187
|
|
|
188
188
|
Current cautions:
|
|
189
189
|
- passing `--profile` is an explicit upstream choice; this extension does not add its own profile-cloning or isolation layer
|
|
190
190
|
- startup-scoped flags like `--profile`, `--session-name`, and `--cdp` are for the first command that launches a session; if the implicit session is already active, retry that call with `sessionMode: "fresh"` or provide an explicit `--session ...` for the new launch
|
|
191
|
-
- implicit `piab-*` sessions are extension-managed convenience sessions; they are best-effort closed on `pi` shutdown, get an idle timeout to reduce stale background daemons,
|
|
191
|
+
- implicit `piab-*` sessions are extension-managed convenience sessions; they are best-effort closed on `pi` shutdown, get an idle timeout to reduce stale background daemons, clean up private temp spill artifacts on shutdown, and are reconstructed from persisted tool details on resume/reload so later default calls keep following the active managed browser
|
|
192
192
|
- `sessionMode: "fresh"` without an explicit `--session` rotates that extension-managed session to the new browser so later auto calls keep using it
|
|
193
193
|
- explicit caller-provided `--session` values are treated as user-managed and are not auto-closed by the extension
|
|
194
|
+
- tool progress/details redact sensitive invocation values such as `--headers`, proxy credentials, and auth-bearing URL parameters before echoing them back into Pi
|
|
194
195
|
|
|
195
196
|
### Switching from public browsing to a fresh profile/debug launch
|
|
196
197
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -84,6 +84,7 @@ Practical policy:
|
|
|
84
84
|
- on normal `pi` shutdown, best-effort close the current extension-managed session
|
|
85
85
|
- also set an idle timeout on extension-managed sessions so abandoned daemons self-clean after inactivity
|
|
86
86
|
- clean up private temp spill artifacts owned by the extension-managed session on shutdown
|
|
87
|
+
- reconstruct the current extension-managed session from persisted tool details on resume/reload so later default calls keep following the active managed browser
|
|
87
88
|
- if an unnamed fresh launch replaces an active extension-managed session, best-effort close the old managed session after the switch succeeds
|
|
88
89
|
- leave explicit caller-provided `--session` choices alone unless the caller closes them explicitly
|
|
89
90
|
|
package/docs/REQUIREMENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ The design should comfortably support workflows such as:
|
|
|
84
84
|
- web research
|
|
85
85
|
- using browser UIs for other LLMs such as ChatGPT, Grok, Gemini, and Claude
|
|
86
86
|
- isolated authenticated browser sessions
|
|
87
|
-
-
|
|
87
|
+
- upstream profile/debug workflows without adding a local profile-cloning layer in this package
|
|
88
88
|
|
|
89
89
|
## Implications for the implementation
|
|
90
90
|
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -121,7 +121,8 @@ Recommended details:
|
|
|
121
121
|
```json
|
|
122
122
|
{
|
|
123
123
|
"args": ["snapshot", "-i"],
|
|
124
|
-
"effectiveArgs": ["--session", "pi-abc123", "
|
|
124
|
+
"effectiveArgs": ["--json", "--session", "pi-abc123", "snapshot", "-i"],
|
|
125
|
+
"command": "snapshot",
|
|
125
126
|
"sessionMode": "auto",
|
|
126
127
|
"sessionName": "pi-abc123",
|
|
127
128
|
"usedImplicitSession": true,
|
|
@@ -131,10 +132,21 @@ Recommended details:
|
|
|
131
132
|
"e1": { "name": "Example Domain", "role": "heading" }
|
|
132
133
|
},
|
|
133
134
|
"snapshot": "- heading \"Example Domain\" [level=1, ref=e1]"
|
|
134
|
-
}
|
|
135
|
+
},
|
|
136
|
+
"summary": "Snapshot: 1 refs on https://example.com/"
|
|
135
137
|
}
|
|
136
138
|
```
|
|
137
139
|
|
|
140
|
+
Additional structured fields can appear when relevant:
|
|
141
|
+
- `batchFailure` and `batchSteps` for `batch` rendering, including mixed-success runs
|
|
142
|
+
- `navigationSummary` for navigation-style commands like `click`, `back`, `forward`, and `reload`
|
|
143
|
+
- `imagePath` / `imagePaths` for screenshots and batched image outputs
|
|
144
|
+
- `fullOutputPath` / `fullOutputPaths` when large snapshot output is compacted and spilled to a private temp file
|
|
145
|
+
- `sessionRecoveryHint` when startup-scoped flags need `sessionMode: "fresh"`
|
|
146
|
+
- `inspection: true` plus `stdout` for successful plain-text inspection commands like `--help` and `--version`
|
|
147
|
+
|
|
148
|
+
When the tool echoes `args` or `effectiveArgs` back into Pi, sensitive values such as `--headers`, proxy credentials, and auth-bearing URL parameters should be redacted first.
|
|
149
|
+
|
|
138
150
|
For oversized snapshots, details should switch to a compact metadata object and include `fullOutputPath` pointing at a private temp JSON spill file with the full upstream snapshot payload.
|
|
139
151
|
|
|
140
152
|
## High-value result rendering
|
|
@@ -164,10 +176,12 @@ If `agent-browser` is not on `PATH`, fail with a message that:
|
|
|
164
176
|
- on normal `pi` shutdown, best-effort close the current extension-managed session
|
|
165
177
|
- set an idle timeout on extension-managed sessions so abandoned daemons eventually self-clean
|
|
166
178
|
- clean up private temp spill artifacts owned by the extension-managed session on shutdown
|
|
179
|
+
- reconstruct the current extension-managed session from persisted tool details on resume/reload so later default calls keep following the active managed browser
|
|
167
180
|
- when an unnamed `sessionMode: "fresh"` launch succeeds, make it the new extension-managed session so later default calls keep using it
|
|
168
181
|
- if that unnamed fresh launch replaced an already-active managed session, best-effort close the old managed session after the switch succeeds
|
|
169
182
|
- treat explicit caller-provided `--session` choices as user-managed
|
|
170
183
|
- pass explicit `--profile` straight through to upstream `agent-browser`; no profile-cloning or isolation layer is added in v1
|
|
184
|
+
- treat successful plain-text inspection commands like `--help` and `--version` as stateless: do not inject the implicit managed session and do not let those calls claim the managed-session slot
|
|
171
185
|
- if startup-scoped flags like `--profile`, `--session-name`, or `--cdp` are supplied after the implicit session is already active while `sessionMode` is `"auto"`, return a validation error with a structured recovery hint that recommends `sessionMode: "fresh"`
|
|
172
186
|
|
|
173
187
|
## Non-goals
|
|
@@ -23,7 +23,12 @@ import {
|
|
|
23
23
|
getImplicitSessionIdleTimeoutMs,
|
|
24
24
|
getLatestUserPrompt,
|
|
25
25
|
hasUsableBraveApiKey,
|
|
26
|
+
redactInvocationArgs,
|
|
27
|
+
redactSensitiveText,
|
|
28
|
+
redactSensitiveValue,
|
|
29
|
+
restoreManagedSessionStateFromBranch,
|
|
26
30
|
resolveManagedSessionState,
|
|
31
|
+
shouldAppendBrowserSystemPrompt,
|
|
27
32
|
validateToolArgs,
|
|
28
33
|
} from "./lib/runtime.js";
|
|
29
34
|
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
@@ -106,10 +111,6 @@ function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
|
|
|
106
111
|
return HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN.test(command);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
function isPlainTextInspectionArgs(args: string[]): boolean {
|
|
110
|
-
return args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-V");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
114
|
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
114
115
|
|
|
115
116
|
interface NavigationSummary {
|
|
@@ -195,18 +196,6 @@ function buildSharedBrowserPlaybookGuidelines(hasBraveApiKey: boolean): string[]
|
|
|
195
196
|
];
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
function buildBrowserSystemPromptAppendix(hasBraveApiKey: boolean): string {
|
|
199
|
-
return [
|
|
200
|
-
PROJECT_RULE_PROMPT,
|
|
201
|
-
"",
|
|
202
|
-
"Quick start:",
|
|
203
|
-
...QUICK_START_GUIDELINES.map((guideline) => `- ${guideline}`),
|
|
204
|
-
"",
|
|
205
|
-
"Browser operating playbook:",
|
|
206
|
-
...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey).map((guideline) => `- ${guideline}`),
|
|
207
|
-
].join("\n");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
199
|
function buildToolPromptGuidelines(hasBraveApiKey: boolean): string[] {
|
|
211
200
|
return [
|
|
212
201
|
...TOOL_PROMPT_GUIDELINES_PREFIX,
|
|
@@ -216,6 +205,30 @@ function buildToolPromptGuidelines(hasBraveApiKey: boolean): string[] {
|
|
|
216
205
|
];
|
|
217
206
|
}
|
|
218
207
|
|
|
208
|
+
function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
|
|
209
|
+
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function redactRecoveryHint(recoveryHint: {
|
|
213
|
+
exampleArgs: string[];
|
|
214
|
+
exampleParams: { args: string[]; sessionMode: "fresh" };
|
|
215
|
+
reason: string;
|
|
216
|
+
recommendedSessionMode: "fresh";
|
|
217
|
+
} | undefined): typeof recoveryHint {
|
|
218
|
+
if (!recoveryHint) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const exampleArgs = redactInvocationArgs(recoveryHint.exampleArgs);
|
|
222
|
+
return {
|
|
223
|
+
...recoveryHint,
|
|
224
|
+
exampleArgs,
|
|
225
|
+
exampleParams: {
|
|
226
|
+
...recoveryHint.exampleParams,
|
|
227
|
+
args: exampleArgs,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<void> {
|
|
220
233
|
const controller = new AbortController();
|
|
221
234
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
@@ -235,7 +248,6 @@ async function closeManagedSession(options: { cwd: string; sessionName: string;
|
|
|
235
248
|
export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
236
249
|
const ephemeralSessionSeed = createEphemeralSessionSeed();
|
|
237
250
|
const hasBraveApiKey = hasUsableBraveApiKey();
|
|
238
|
-
const browserSystemPromptAppendix = buildBrowserSystemPromptAppendix(hasBraveApiKey);
|
|
239
251
|
const toolPromptGuidelines = buildToolPromptGuidelines(hasBraveApiKey);
|
|
240
252
|
const implicitSessionIdleTimeoutMs = getImplicitSessionIdleTimeoutMs();
|
|
241
253
|
const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
|
|
@@ -246,26 +258,32 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
246
258
|
let freshSessionOrdinal = 0;
|
|
247
259
|
|
|
248
260
|
pi.on("session_start", async (_event, ctx) => {
|
|
249
|
-
managedSessionActive = false;
|
|
250
261
|
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
251
|
-
|
|
262
|
+
const restoredState = restoreManagedSessionStateFromBranch(ctx.sessionManager.getBranch(), managedSessionBaseName);
|
|
263
|
+
managedSessionActive = restoredState.active;
|
|
264
|
+
managedSessionName = restoredState.sessionName;
|
|
252
265
|
managedSessionCwd = ctx.cwd;
|
|
253
|
-
freshSessionOrdinal =
|
|
266
|
+
freshSessionOrdinal = restoredState.freshSessionOrdinal;
|
|
254
267
|
});
|
|
255
268
|
|
|
256
269
|
pi.on("session_shutdown", async () => {
|
|
270
|
+
if (managedSessionActive) {
|
|
271
|
+
await closeManagedSession({
|
|
272
|
+
cwd: managedSessionCwd,
|
|
273
|
+
sessionName: managedSessionName,
|
|
274
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
257
277
|
managedSessionActive = false;
|
|
258
|
-
await closeManagedSession({
|
|
259
|
-
cwd: managedSessionCwd,
|
|
260
|
-
sessionName: managedSessionName,
|
|
261
|
-
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
262
|
-
});
|
|
263
278
|
await cleanupSecureTempArtifacts();
|
|
264
279
|
});
|
|
265
280
|
|
|
266
281
|
pi.on("before_agent_start", async (event) => {
|
|
282
|
+
if (!shouldAppendBrowserSystemPrompt(event.prompt)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
267
285
|
return {
|
|
268
|
-
systemPrompt: `${event.systemPrompt}\n\n${
|
|
286
|
+
systemPrompt: `${event.systemPrompt}\n\n${PROJECT_RULE_PROMPT}`,
|
|
269
287
|
};
|
|
270
288
|
});
|
|
271
289
|
|
|
@@ -294,11 +312,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
294
312
|
promptGuidelines: toolPromptGuidelines,
|
|
295
313
|
parameters: AGENT_BROWSER_PARAMS,
|
|
296
314
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
315
|
+
const redactedArgs = redactInvocationArgs(params.args);
|
|
297
316
|
const validationError = validateToolArgs(params.args);
|
|
298
317
|
if (validationError) {
|
|
299
318
|
return {
|
|
300
319
|
content: [{ type: "text", text: validationError }],
|
|
301
|
-
details: { args:
|
|
320
|
+
details: { args: redactedArgs, validationError },
|
|
302
321
|
isError: true,
|
|
303
322
|
};
|
|
304
323
|
}
|
|
@@ -311,6 +330,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
311
330
|
managedSessionName,
|
|
312
331
|
sessionMode,
|
|
313
332
|
});
|
|
333
|
+
const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
|
|
334
|
+
const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
|
|
314
335
|
if (executionPlan.managedSessionName === freshSessionName) {
|
|
315
336
|
freshSessionOrdinal += 1;
|
|
316
337
|
}
|
|
@@ -319,10 +340,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
319
340
|
return {
|
|
320
341
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
321
342
|
details: {
|
|
322
|
-
args:
|
|
343
|
+
args: redactedArgs,
|
|
323
344
|
invalidValueFlag: executionPlan.invalidValueFlag,
|
|
324
345
|
sessionMode,
|
|
325
|
-
sessionRecoveryHint:
|
|
346
|
+
sessionRecoveryHint: redactedRecoveryHint,
|
|
326
347
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
327
348
|
validationError: executionPlan.validationError,
|
|
328
349
|
},
|
|
@@ -331,12 +352,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
onUpdate?.({
|
|
334
|
-
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(
|
|
355
|
+
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedEffectiveArgs)}` }],
|
|
335
356
|
details: {
|
|
336
|
-
effectiveArgs:
|
|
357
|
+
effectiveArgs: redactedEffectiveArgs,
|
|
337
358
|
sessionMode,
|
|
338
|
-
sessionName
|
|
339
|
-
usedImplicitSession: executionPlan.usedImplicitSession,
|
|
359
|
+
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
340
360
|
},
|
|
341
361
|
});
|
|
342
362
|
|
|
@@ -353,8 +373,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
353
373
|
return {
|
|
354
374
|
content: [{ type: "text", text: errorText }],
|
|
355
375
|
details: {
|
|
356
|
-
args:
|
|
357
|
-
effectiveArgs:
|
|
376
|
+
args: redactedArgs,
|
|
377
|
+
effectiveArgs: redactedEffectiveArgs,
|
|
358
378
|
sessionMode,
|
|
359
379
|
spawnError: processResult.spawnError.message,
|
|
360
380
|
},
|
|
@@ -369,10 +389,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
369
389
|
});
|
|
370
390
|
let presentationEnvelope = parsed.envelope;
|
|
371
391
|
const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
372
|
-
const plainTextInspection =
|
|
373
|
-
const envelopeSuccess = plainTextInspection ? true : parsed.envelope?.success !== false;
|
|
392
|
+
const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
|
|
374
393
|
const parseSucceeded = plainTextInspection || parsed.parseError === undefined;
|
|
394
|
+
const envelopeSuccess = plainTextInspection ? true : parsed.envelope?.success !== false;
|
|
375
395
|
const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
396
|
+
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
376
397
|
|
|
377
398
|
let navigationSummary: NavigationSummary | undefined;
|
|
378
399
|
if (succeeded && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, parsed.envelope?.data)) {
|
|
@@ -423,9 +444,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
423
444
|
|
|
424
445
|
const presentation = plainTextInspection
|
|
425
446
|
? {
|
|
426
|
-
|
|
447
|
+
batchFailure: undefined,
|
|
448
|
+
batchSteps: undefined,
|
|
449
|
+
content: [{ type: "text" as const, text: inspectionText ?? "" }],
|
|
450
|
+
data: undefined,
|
|
451
|
+
fullOutputPath: undefined,
|
|
452
|
+
fullOutputPaths: undefined,
|
|
427
453
|
imagePath: undefined,
|
|
428
|
-
|
|
454
|
+
imagePaths: undefined,
|
|
455
|
+
summary: `${redactedArgs.join(" ")} completed`,
|
|
429
456
|
}
|
|
430
457
|
: await buildToolPresentation({
|
|
431
458
|
commandInfo: executionPlan.commandInfo,
|
|
@@ -433,33 +460,40 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
433
460
|
envelope: presentationEnvelope,
|
|
434
461
|
errorText,
|
|
435
462
|
});
|
|
463
|
+
const redactedContent = presentation.content.map((item) =>
|
|
464
|
+
item.type === "text" ? { ...item, text: redactSensitiveText(item.text) } : item,
|
|
465
|
+
);
|
|
436
466
|
|
|
437
467
|
return {
|
|
438
|
-
content:
|
|
468
|
+
content: redactedContent,
|
|
439
469
|
details: {
|
|
440
|
-
args:
|
|
441
|
-
batchFailure: presentation.batchFailure,
|
|
442
|
-
batchSteps: presentation.batchSteps,
|
|
470
|
+
args: redactedArgs,
|
|
471
|
+
batchFailure: redactSensitiveValue(presentation.batchFailure),
|
|
472
|
+
batchSteps: redactSensitiveValue(presentation.batchSteps),
|
|
443
473
|
command: executionPlan.commandInfo.command,
|
|
444
474
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
445
|
-
data: presentation.data,
|
|
446
|
-
error: parsed.envelope?.error,
|
|
447
|
-
|
|
448
|
-
|
|
475
|
+
data: redactSensitiveValue(presentation.data),
|
|
476
|
+
error: plainTextInspection ? undefined : redactSensitiveValue(parsed.envelope?.error),
|
|
477
|
+
inspection: plainTextInspection || undefined,
|
|
478
|
+
navigationSummary: redactSensitiveValue(navigationSummary),
|
|
479
|
+
effectiveArgs: redactedEffectiveArgs,
|
|
449
480
|
exitCode: processResult.exitCode,
|
|
450
481
|
fullOutputPath: presentation.fullOutputPath,
|
|
451
482
|
fullOutputPaths: presentation.fullOutputPaths,
|
|
452
483
|
imagePath: presentation.imagePath,
|
|
453
484
|
imagePaths: presentation.imagePaths,
|
|
454
|
-
parseError: parsed.parseError,
|
|
485
|
+
parseError: plainTextInspection ? undefined : parsed.parseError,
|
|
455
486
|
sessionMode,
|
|
456
|
-
sessionName
|
|
457
|
-
sessionRecoveryHint:
|
|
487
|
+
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
488
|
+
sessionRecoveryHint: redactedRecoveryHint,
|
|
458
489
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
459
|
-
stderr: processResult.stderr
|
|
460
|
-
stdout:
|
|
461
|
-
|
|
462
|
-
|
|
490
|
+
stderr: processResult.stderr ? redactSensitiveText(processResult.stderr) : undefined,
|
|
491
|
+
stdout: plainTextInspection
|
|
492
|
+
? redactSensitiveText(inspectionText ?? "")
|
|
493
|
+
: parseSucceeded
|
|
494
|
+
? undefined
|
|
495
|
+
: redactSensitiveText(processResult.stdout),
|
|
496
|
+
summary: redactSensitiveText(presentation.summary),
|
|
463
497
|
},
|
|
464
498
|
isError: !succeeded,
|
|
465
499
|
};
|
|
@@ -65,7 +65,7 @@ const INHERITED_ENV_NAMES = new Set([
|
|
|
65
65
|
allProxyEnvName,
|
|
66
66
|
noProxyEnvName,
|
|
67
67
|
]);
|
|
68
|
-
const INHERITED_ENV_PREFIXES = ["
|
|
68
|
+
const INHERITED_ENV_PREFIXES = ["AI_GATEWAY_", "XDG_"] as const;
|
|
69
69
|
|
|
70
70
|
export interface ProcessRunResult {
|
|
71
71
|
aborted: boolean;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Purpose: Build safe, deterministic agent-browser invocations for the pi-agent-browser extension.
|
|
3
|
-
* Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity,
|
|
2
|
+
* Purpose: Build safe, deterministic agent-browser invocations and persisted session state for the pi-agent-browser extension.
|
|
3
|
+
* Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity, restore managed-session state from persisted tool details, redact sensitive invocation text, classify browser-oriented prompts, and build the effective CLI argument list passed to the upstream agent-browser binary.
|
|
4
4
|
* Scope: Pure runtime-planning helpers only; no subprocess execution or filesystem access lives here.
|
|
5
5
|
* Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
|
|
6
|
-
* Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an extension-managed `--session` when appropriate.
|
|
6
|
+
* Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, keeps plain-text inspection stateless, and only injects `--json` plus an extension-managed `--session` when appropriate.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -23,6 +23,17 @@ const LEGACY_BASH_ALLOW_PATTERNS = [
|
|
|
23
23
|
/\bagent-browser\s+--(?:help|version)\b/i,
|
|
24
24
|
/\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
|
|
25
25
|
];
|
|
26
|
+
const BROWSER_PROMPT_PATTERNS = [
|
|
27
|
+
/\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
|
|
28
|
+
/\bbrowser\b.*\b(?:automation|click|fill|navigate|open|page|screenshot|site|snapshot|tab|url|visit|web(?:site| page)?)\b/i,
|
|
29
|
+
/\b(?:browse|click|fill|login|navigate|open|visit)\b.*\b(?:https?:\/\/\S+|page|site|tab|url|web(?:site| page)?)\b/i,
|
|
30
|
+
];
|
|
31
|
+
const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
|
|
32
|
+
const SENSITIVE_VALUE_FLAGS = new Set(["--headers", "--proxy"]);
|
|
33
|
+
const SENSITIVE_QUERY_PARAM_PATTERN =
|
|
34
|
+
/^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|session(?:_|-)?id|sig(?:nature)?|token)$/i;
|
|
35
|
+
const SENSITIVE_FIELD_NAME_PATTERN =
|
|
36
|
+
/^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|x(?:_|-)?api(?:_|-)?key)$/i;
|
|
26
37
|
|
|
27
38
|
const GLOBAL_FLAGS_WITH_VALUES = new Set([
|
|
28
39
|
"--session",
|
|
@@ -78,6 +89,7 @@ export interface ExecutionPlan {
|
|
|
78
89
|
effectiveArgs: string[];
|
|
79
90
|
invalidValueFlag?: InvalidValueFlagDetails;
|
|
80
91
|
managedSessionName?: string;
|
|
92
|
+
plainTextInspection: boolean;
|
|
81
93
|
recoveryHint?: SessionRecoveryHint;
|
|
82
94
|
sessionName?: string;
|
|
83
95
|
startupScopedFlags: string[];
|
|
@@ -91,10 +103,146 @@ export interface ManagedSessionState {
|
|
|
91
103
|
sessionName: string;
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
export interface RestoredManagedSessionState extends ManagedSessionState {
|
|
107
|
+
freshSessionOrdinal: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
export interface PromptPolicy {
|
|
95
111
|
allowLegacyAgentBrowserBash: boolean;
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
115
|
+
return typeof value === "object" && value !== null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isStringArray(value: unknown): value is string[] {
|
|
119
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shouldRedactQueryParam(name: string): boolean {
|
|
123
|
+
return SENSITIVE_QUERY_PARAM_PATTERN.test(name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function redactUrlToken(token: string): string {
|
|
127
|
+
let parsed: URL;
|
|
128
|
+
try {
|
|
129
|
+
parsed = new URL(token);
|
|
130
|
+
} catch {
|
|
131
|
+
return token;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) {
|
|
135
|
+
return token;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (parsed.username.length > 0) {
|
|
139
|
+
parsed.username = "[REDACTED]";
|
|
140
|
+
}
|
|
141
|
+
if (parsed.password.length > 0) {
|
|
142
|
+
parsed.password = "[REDACTED]";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [name] of parsed.searchParams) {
|
|
146
|
+
if (shouldRedactQueryParam(name)) {
|
|
147
|
+
parsed.searchParams.set(name, "[REDACTED]");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hashText = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
|
|
152
|
+
if (hashText.includes("=")) {
|
|
153
|
+
const hashParams = new URLSearchParams(hashText);
|
|
154
|
+
let mutated = false;
|
|
155
|
+
for (const [name] of hashParams) {
|
|
156
|
+
if (shouldRedactQueryParam(name)) {
|
|
157
|
+
hashParams.set(name, "[REDACTED]");
|
|
158
|
+
mutated = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (mutated) {
|
|
162
|
+
parsed.hash = `#${hashParams.toString()}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parsed.toString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function redactLooseUrlMatches(text: string): string {
|
|
170
|
+
return text.replace(/\b(?:https?|wss?):\/\/[^\s"'`<>\])]+/g, (match) => redactUrlToken(match));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function redactSensitiveText(text: string): string {
|
|
174
|
+
return redactLooseUrlMatches(text)
|
|
175
|
+
.replace(/\b(Bearer)\s+[^\s",]+/gi, "$1 [REDACTED]")
|
|
176
|
+
.replace(/\b(Basic)\s+[^\s",]+/gi, "$1 [REDACTED]");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function redactSensitiveValue(value: unknown): unknown {
|
|
180
|
+
if (typeof value === "string") {
|
|
181
|
+
return redactSensitiveText(value);
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
return value.map((item) => redactSensitiveValue(item));
|
|
185
|
+
}
|
|
186
|
+
if (!isRecord(value)) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
return Object.fromEntries(
|
|
190
|
+
Object.entries(value).map(([key, entryValue]) => {
|
|
191
|
+
if (SENSITIVE_FIELD_NAME_PATTERN.test(key)) {
|
|
192
|
+
return [key, "[REDACTED]"];
|
|
193
|
+
}
|
|
194
|
+
return [key, redactSensitiveValue(entryValue)];
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function redactFlagValue(flag: string, value: string): string {
|
|
200
|
+
if (SENSITIVE_VALUE_FLAGS.has(flag)) {
|
|
201
|
+
return "[REDACTED]";
|
|
202
|
+
}
|
|
203
|
+
return redactUrlToken(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function redactInvocationArgs(args: string[]): string[] {
|
|
207
|
+
const redacted: string[] = [];
|
|
208
|
+
let pendingValueFlag: string | undefined;
|
|
209
|
+
|
|
210
|
+
for (const token of args) {
|
|
211
|
+
if (pendingValueFlag) {
|
|
212
|
+
redacted.push(redactFlagValue(pendingValueFlag, token));
|
|
213
|
+
pendingValueFlag = undefined;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const normalizedToken = token.split("=", 1)[0] ?? token;
|
|
218
|
+
if (SENSITIVE_VALUE_FLAGS.has(normalizedToken)) {
|
|
219
|
+
if (token.includes("=")) {
|
|
220
|
+
redacted.push(`${normalizedToken}=[REDACTED]`);
|
|
221
|
+
} else {
|
|
222
|
+
redacted.push(token);
|
|
223
|
+
pendingValueFlag = normalizedToken;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
redacted.push(redactUrlToken(token));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return redacted;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function shouldAppendBrowserSystemPrompt(prompt: string): boolean {
|
|
235
|
+
const normalizedPrompt = prompt.trim();
|
|
236
|
+
if (normalizedPrompt.length === 0) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return BROWSER_PROMPT_PATTERNS.some((pattern) => pattern.test(normalizedPrompt));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function isPlainTextInspectionArgs(args: string[]): boolean {
|
|
243
|
+
return args.some((token) => INSPECTION_FLAGS.has(token));
|
|
244
|
+
}
|
|
245
|
+
|
|
98
246
|
export function hasUsableBraveApiKey(apiKey: string | null | undefined = process.env[BRAVE_API_KEY_ENV]): boolean {
|
|
99
247
|
return typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
100
248
|
}
|
|
@@ -146,6 +294,74 @@ export function resolveManagedSessionState(options: {
|
|
|
146
294
|
};
|
|
147
295
|
}
|
|
148
296
|
|
|
297
|
+
function isRestorableManagedSessionName(sessionName: string, fallbackSessionName: string): boolean {
|
|
298
|
+
return sessionName === fallbackSessionName || sessionName.startsWith(`${fallbackSessionName}-fresh-`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function restoreManagedSessionStateFromBranch(
|
|
302
|
+
branch: unknown[],
|
|
303
|
+
fallbackSessionName: string,
|
|
304
|
+
): RestoredManagedSessionState {
|
|
305
|
+
let restoredState: ManagedSessionState = {
|
|
306
|
+
active: false,
|
|
307
|
+
sessionName: fallbackSessionName,
|
|
308
|
+
};
|
|
309
|
+
let freshSessionOrdinal = 0;
|
|
310
|
+
|
|
311
|
+
for (const entry of branch) {
|
|
312
|
+
if (!isRecord(entry) || entry.type !== "message") {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
316
|
+
if (!message || message.toolName !== "agent_browser") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
320
|
+
if (!details) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const args = isStringArray(details.args) ? details.args : [];
|
|
324
|
+
if (isPlainTextInspectionArgs(args)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const explicitSessionName = extractExplicitSessionName(args);
|
|
329
|
+
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
330
|
+
const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
|
|
331
|
+
const usedImplicitSession = details.usedImplicitSession === true;
|
|
332
|
+
const managedSessionName =
|
|
333
|
+
!explicitSessionName &&
|
|
334
|
+
sessionName &&
|
|
335
|
+
isRestorableManagedSessionName(sessionName, fallbackSessionName) &&
|
|
336
|
+
(usedImplicitSession || sessionMode === "fresh")
|
|
337
|
+
? sessionName
|
|
338
|
+
: undefined;
|
|
339
|
+
if (!managedSessionName) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const messageIsError = typeof message.isError === "boolean" ? message.isError : undefined;
|
|
344
|
+
const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
|
|
345
|
+
const succeeded = messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
|
|
346
|
+
const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
|
|
347
|
+
restoredState = resolveManagedSessionState({
|
|
348
|
+
command,
|
|
349
|
+
managedSessionName,
|
|
350
|
+
priorActive: restoredState.active,
|
|
351
|
+
priorSessionName: restoredState.sessionName,
|
|
352
|
+
succeeded,
|
|
353
|
+
});
|
|
354
|
+
if (succeeded && sessionMode === "fresh") {
|
|
355
|
+
freshSessionOrdinal += 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
...restoredState,
|
|
361
|
+
freshSessionOrdinal,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
149
365
|
export function createEphemeralSessionSeed(): string {
|
|
150
366
|
return randomUUID();
|
|
151
367
|
}
|
|
@@ -310,22 +526,34 @@ export function buildExecutionPlan(
|
|
|
310
526
|
sessionMode: SessionMode;
|
|
311
527
|
},
|
|
312
528
|
): ExecutionPlan {
|
|
313
|
-
const effectiveArgs = args.includes("--json") ? [] : ["--json"];
|
|
314
529
|
const invalidValueFlag = getInvalidValueFlagDetails(args);
|
|
530
|
+
const startupScopedFlags = getStartupScopedFlags(args);
|
|
531
|
+
const plainTextInspection = isPlainTextInspectionArgs(args);
|
|
532
|
+
const commandInfo = parseCommandInfo(args);
|
|
533
|
+
const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
|
|
315
534
|
if (invalidValueFlag) {
|
|
316
535
|
return {
|
|
317
536
|
commandInfo: {},
|
|
318
537
|
effectiveArgs,
|
|
319
538
|
invalidValueFlag,
|
|
539
|
+
plainTextInspection: false,
|
|
320
540
|
startupScopedFlags: [],
|
|
321
541
|
usedImplicitSession: false,
|
|
322
542
|
validationError: formatInvalidValueFlagError(invalidValueFlag),
|
|
323
543
|
};
|
|
324
544
|
}
|
|
325
545
|
|
|
326
|
-
|
|
546
|
+
if (plainTextInspection) {
|
|
547
|
+
return {
|
|
548
|
+
commandInfo,
|
|
549
|
+
effectiveArgs,
|
|
550
|
+
plainTextInspection,
|
|
551
|
+
startupScopedFlags,
|
|
552
|
+
usedImplicitSession: false,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
327
556
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
328
|
-
const startupScopedFlags = getStartupScopedFlags(args);
|
|
329
557
|
const shouldCreateFreshManagedSession =
|
|
330
558
|
!explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
|
|
331
559
|
let managedSessionName: string | undefined;
|
|
@@ -365,6 +593,7 @@ export function buildExecutionPlan(
|
|
|
365
593
|
commandInfo,
|
|
366
594
|
effectiveArgs,
|
|
367
595
|
managedSessionName,
|
|
596
|
+
plainTextInspection,
|
|
368
597
|
recoveryHint,
|
|
369
598
|
sessionName,
|
|
370
599
|
startupScopedFlags,
|
|
@@ -396,4 +625,3 @@ export function parseCommandInfo(args: string[]): CommandInfo {
|
|
|
396
625
|
|
|
397
626
|
return { command: commands[0], subcommand: commands[1] };
|
|
398
627
|
}
|
|
399
|
-
|
package/package.json
CHANGED