pi-agent-browser-native 0.1.6 → 0.2.1
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 +24 -0
- package/README.md +99 -5
- package/docs/ARCHITECTURE.md +16 -8
- package/docs/TOOL_CONTRACT.md +27 -17
- package/extensions/agent-browser/index.ts +196 -59
- package/extensions/agent-browser/lib/results/envelope.ts +7 -0
- package/extensions/agent-browser/lib/results/presentation.ts +263 -22
- package/extensions/agent-browser/lib/results/shared.ts +25 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +32 -16
- package/extensions/agent-browser/lib/runtime.ts +158 -32
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Build safe, deterministic agent-browser invocations for the pi-agent-browser extension.
|
|
3
|
-
* Responsibilities: Validate raw tool arguments, derive
|
|
3
|
+
* Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity, resolve managed-session timeout/state helpers, detect explicit session usage, 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
|
|
6
|
+
* Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an extension-managed `--session` when appropriate.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -16,13 +16,6 @@ const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDL
|
|
|
16
16
|
const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
|
|
17
17
|
const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
18
18
|
const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
|
|
19
|
-
const INSPECTION_ALLOW_PATTERNS = [
|
|
20
|
-
/\bagent[_ -]?browser\s+--(?:help|version)\b/i,
|
|
21
|
-
/\bagent[_ -]?browser\b.*\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b/i,
|
|
22
|
-
/\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b.*\bagent[_ -]?browser\b/i,
|
|
23
|
-
/\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
|
|
24
|
-
/\bwhy\s+(?:isn't|is not|doesn't|does not)\b.*\b(?:agent[_ -]?browser|agent_browser)\b/i,
|
|
25
|
-
];
|
|
26
19
|
const LEGACY_BASH_ALLOW_PATTERNS = [
|
|
27
20
|
/\b(?:bash-oriented workflow|bash workflow)\b/i,
|
|
28
21
|
/\b(?:use|via|through|with)\s+bash\b/i,
|
|
@@ -56,23 +49,49 @@ const GLOBAL_FLAGS_WITH_VALUES = new Set([
|
|
|
56
49
|
]);
|
|
57
50
|
const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
|
|
58
51
|
const MAX_PROJECT_SLUG_LENGTH = 24;
|
|
52
|
+
const SESSION_NAME_CWD_HASH_LENGTH = 8;
|
|
53
|
+
const SESSION_NAME_SESSION_ID_LENGTH = 12;
|
|
59
54
|
|
|
60
55
|
export interface CommandInfo {
|
|
61
56
|
command?: string;
|
|
62
57
|
subcommand?: string;
|
|
63
58
|
}
|
|
64
59
|
|
|
60
|
+
export type SessionMode = "auto" | "fresh";
|
|
61
|
+
|
|
62
|
+
export interface SessionRecoveryHint {
|
|
63
|
+
exampleArgs: string[];
|
|
64
|
+
exampleParams: { args: string[]; sessionMode: "fresh" };
|
|
65
|
+
reason: string;
|
|
66
|
+
recommendedSessionMode: "fresh";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface InvalidValueFlagDetails {
|
|
70
|
+
flag: string;
|
|
71
|
+
index: number;
|
|
72
|
+
reason: "missing-value" | "unexpected-flag";
|
|
73
|
+
receivedToken?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
export interface ExecutionPlan {
|
|
66
77
|
commandInfo: CommandInfo;
|
|
67
78
|
effectiveArgs: string[];
|
|
79
|
+
invalidValueFlag?: InvalidValueFlagDetails;
|
|
80
|
+
managedSessionName?: string;
|
|
81
|
+
recoveryHint?: SessionRecoveryHint;
|
|
68
82
|
sessionName?: string;
|
|
69
83
|
startupScopedFlags: string[];
|
|
70
84
|
usedImplicitSession: boolean;
|
|
71
85
|
validationError?: string;
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
export interface ManagedSessionState {
|
|
89
|
+
active: boolean;
|
|
90
|
+
replacedSessionName?: string;
|
|
91
|
+
sessionName: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
export interface PromptPolicy {
|
|
75
|
-
allowAgentBrowserInspection: boolean;
|
|
76
95
|
allowLegacyAgentBrowserBash: boolean;
|
|
77
96
|
}
|
|
78
97
|
|
|
@@ -103,25 +122,38 @@ export function getImplicitSessionCloseTimeoutMs(env: NodeJS.ProcessEnv = proces
|
|
|
103
122
|
return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
export function
|
|
125
|
+
export function resolveManagedSessionState(options: {
|
|
107
126
|
command?: string;
|
|
127
|
+
managedSessionName?: string;
|
|
108
128
|
priorActive: boolean;
|
|
129
|
+
priorSessionName: string;
|
|
109
130
|
succeeded: boolean;
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (command === "close") {
|
|
115
|
-
return succeeded ? false : priorActive;
|
|
131
|
+
}): ManagedSessionState {
|
|
132
|
+
const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
|
|
133
|
+
if (!managedSessionName) {
|
|
134
|
+
return { active: priorActive, sessionName: priorSessionName };
|
|
116
135
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
136
|
+
if (command === "close" && managedSessionName === priorSessionName) {
|
|
137
|
+
return { active: succeeded ? false : priorActive, sessionName: priorSessionName };
|
|
138
|
+
}
|
|
139
|
+
if (!succeeded) {
|
|
140
|
+
return { active: priorActive, sessionName: priorSessionName };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
active: true,
|
|
144
|
+
replacedSessionName: priorActive && priorSessionName !== managedSessionName ? priorSessionName : undefined,
|
|
145
|
+
sessionName: managedSessionName,
|
|
146
|
+
};
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
export function createEphemeralSessionSeed(): string {
|
|
122
150
|
return randomUUID();
|
|
123
151
|
}
|
|
124
152
|
|
|
153
|
+
function createCwdHash(cwd: string): string {
|
|
154
|
+
return createHash("sha256").update(`cwd:${cwd}`).digest("hex").slice(0, SESSION_NAME_CWD_HASH_LENGTH);
|
|
155
|
+
}
|
|
156
|
+
|
|
125
157
|
export function createImplicitSessionName(
|
|
126
158
|
sessionId: string | undefined,
|
|
127
159
|
cwd: string,
|
|
@@ -133,13 +165,25 @@ export function createImplicitSessionName(
|
|
|
133
165
|
.replace(/[^a-z0-9]+/g, "-")
|
|
134
166
|
.replace(/^-+|-+$/g, "")
|
|
135
167
|
.slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
|
|
136
|
-
const
|
|
168
|
+
const cwdHash = createCwdHash(cwd);
|
|
169
|
+
const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
137
170
|
if (stableSessionId && stableSessionId.length > 0) {
|
|
138
|
-
return `piab-${slug}-${stableSessionId}`;
|
|
171
|
+
return `piab-${slug}-${stableSessionId}-${cwdHash}`;
|
|
139
172
|
}
|
|
140
173
|
|
|
141
|
-
const digest = createHash("sha256")
|
|
142
|
-
|
|
174
|
+
const digest = createHash("sha256")
|
|
175
|
+
.update(`ephemeral:${cwd}:${ephemeralSeed}`)
|
|
176
|
+
.digest("hex")
|
|
177
|
+
.slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
178
|
+
return `piab-${slug}-${digest}-${cwdHash}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createFreshSessionName(baseSessionName: string, ephemeralSeed: string, ordinal: number): string {
|
|
182
|
+
const suffix = createHash("sha256")
|
|
183
|
+
.update(`fresh:${baseSessionName}:${ephemeralSeed}:${ordinal}`)
|
|
184
|
+
.digest("hex")
|
|
185
|
+
.slice(0, 10);
|
|
186
|
+
return `${baseSessionName}-fresh-${suffix}`;
|
|
143
187
|
}
|
|
144
188
|
|
|
145
189
|
export function validateToolArgs(args: string[]): string | undefined {
|
|
@@ -155,6 +199,54 @@ export function validateToolArgs(args: string[]): string | undefined {
|
|
|
155
199
|
return undefined;
|
|
156
200
|
}
|
|
157
201
|
|
|
202
|
+
function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
|
|
203
|
+
for (const [index, token] of args.entries()) {
|
|
204
|
+
if (!token.startsWith("-")) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const normalizedToken = token.split("=", 1)[0] ?? token;
|
|
208
|
+
if (!GLOBAL_FLAGS_WITH_VALUES.has(normalizedToken)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (token.includes("=")) {
|
|
212
|
+
const value = token.slice(token.indexOf("=") + 1).trim();
|
|
213
|
+
if (value.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
flag: normalizedToken,
|
|
216
|
+
index,
|
|
217
|
+
reason: "missing-value",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const receivedToken = args[index + 1];
|
|
223
|
+
if (receivedToken === undefined) {
|
|
224
|
+
return {
|
|
225
|
+
flag: normalizedToken,
|
|
226
|
+
index,
|
|
227
|
+
reason: "missing-value",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (receivedToken.startsWith("-")) {
|
|
231
|
+
return {
|
|
232
|
+
flag: normalizedToken,
|
|
233
|
+
index,
|
|
234
|
+
reason: "unexpected-flag",
|
|
235
|
+
receivedToken,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatInvalidValueFlagError(details: InvalidValueFlagDetails): string {
|
|
244
|
+
if (details.reason === "unexpected-flag" && details.receivedToken) {
|
|
245
|
+
return `Flag \`${details.flag}\` requires a value, but received \`${details.receivedToken}\` instead. Pass a non-flag value immediately after \`${details.flag}\`.`;
|
|
246
|
+
}
|
|
247
|
+
return `Flag \`${details.flag}\` requires a value immediately after it. Pass a non-flag token like \`${details.flag} demo\`.`;
|
|
248
|
+
}
|
|
249
|
+
|
|
158
250
|
function hasFlagToken(args: string[], flag: string): boolean {
|
|
159
251
|
return args.some((token) => token === flag || token.startsWith(`${flag}=`));
|
|
160
252
|
}
|
|
@@ -177,7 +269,6 @@ export function getStartupScopedFlags(args: string[]): string[] {
|
|
|
177
269
|
|
|
178
270
|
export function buildPromptPolicy(prompt: string): PromptPolicy {
|
|
179
271
|
return {
|
|
180
|
-
allowAgentBrowserInspection: INSPECTION_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
|
|
181
272
|
allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
|
|
182
273
|
};
|
|
183
274
|
}
|
|
@@ -212,27 +303,60 @@ export function getLatestUserPrompt(branch: unknown[]): string {
|
|
|
212
303
|
|
|
213
304
|
export function buildExecutionPlan(
|
|
214
305
|
args: string[],
|
|
215
|
-
options: {
|
|
306
|
+
options: {
|
|
307
|
+
freshSessionName: string;
|
|
308
|
+
managedSessionActive: boolean;
|
|
309
|
+
managedSessionName: string;
|
|
310
|
+
sessionMode: SessionMode;
|
|
311
|
+
},
|
|
216
312
|
): ExecutionPlan {
|
|
313
|
+
const effectiveArgs = args.includes("--json") ? [] : ["--json"];
|
|
314
|
+
const invalidValueFlag = getInvalidValueFlagDetails(args);
|
|
315
|
+
if (invalidValueFlag) {
|
|
316
|
+
return {
|
|
317
|
+
commandInfo: {},
|
|
318
|
+
effectiveArgs,
|
|
319
|
+
invalidValueFlag,
|
|
320
|
+
startupScopedFlags: [],
|
|
321
|
+
usedImplicitSession: false,
|
|
322
|
+
validationError: formatInvalidValueFlagError(invalidValueFlag),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
217
326
|
const commandInfo = parseCommandInfo(args);
|
|
218
327
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
219
328
|
const startupScopedFlags = getStartupScopedFlags(args);
|
|
220
|
-
const
|
|
329
|
+
const shouldCreateFreshManagedSession =
|
|
330
|
+
!explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
|
|
331
|
+
let managedSessionName: string | undefined;
|
|
332
|
+
let recoveryHint: SessionRecoveryHint | undefined;
|
|
221
333
|
let sessionName = explicitSessionName;
|
|
222
334
|
let usedImplicitSession = false;
|
|
223
335
|
let validationError: string | undefined;
|
|
224
336
|
|
|
225
|
-
if (!explicitSessionName && options.
|
|
226
|
-
if (options.
|
|
337
|
+
if (!explicitSessionName && options.sessionMode === "auto") {
|
|
338
|
+
if (options.managedSessionActive && startupScopedFlags.length > 0) {
|
|
339
|
+
recoveryHint = {
|
|
340
|
+
exampleArgs: args,
|
|
341
|
+
exampleParams: { args, sessionMode: "fresh" },
|
|
342
|
+
reason:
|
|
343
|
+
"Startup-scoped flags like --profile, --session-name, and --cdp need a fresh upstream launch once the extension-managed session is already active.",
|
|
344
|
+
recommendedSessionMode: "fresh",
|
|
345
|
+
};
|
|
227
346
|
validationError = [
|
|
228
|
-
`The current
|
|
229
|
-
"
|
|
347
|
+
`The current extension-managed agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
|
|
348
|
+
"Retry this call with `sessionMode: \"fresh\"` to force a fresh upstream launch, or pass an explicit `--session ...` if you want to name the new session yourself.",
|
|
230
349
|
].join(" ");
|
|
231
350
|
} else {
|
|
232
|
-
effectiveArgs.push("--session", options.
|
|
233
|
-
|
|
351
|
+
effectiveArgs.push("--session", options.managedSessionName);
|
|
352
|
+
managedSessionName = options.managedSessionName;
|
|
353
|
+
sessionName = options.managedSessionName;
|
|
234
354
|
usedImplicitSession = true;
|
|
235
355
|
}
|
|
356
|
+
} else if (shouldCreateFreshManagedSession) {
|
|
357
|
+
effectiveArgs.push("--session", options.freshSessionName);
|
|
358
|
+
managedSessionName = options.freshSessionName;
|
|
359
|
+
sessionName = options.freshSessionName;
|
|
236
360
|
}
|
|
237
361
|
|
|
238
362
|
effectiveArgs.push(...args);
|
|
@@ -240,6 +364,8 @@ export function buildExecutionPlan(
|
|
|
240
364
|
return {
|
|
241
365
|
commandInfo,
|
|
242
366
|
effectiveArgs,
|
|
367
|
+
managedSessionName,
|
|
368
|
+
recoveryHint,
|
|
243
369
|
sessionName,
|
|
244
370
|
startupScopedFlags,
|
|
245
371
|
usedImplicitSession,
|
package/package.json
CHANGED