pi-agent-browser-native 0.2.0 → 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 +22 -2
- package/README.md +19 -11
- package/docs/ARCHITECTURE.md +12 -7
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/TOOL_CONTRACT.md +27 -11
- package/extensions/agent-browser/index.ts +155 -81
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/results/envelope.ts +7 -0
- package/extensions/agent-browser/lib/results/presentation.ts +32 -3
- package/extensions/agent-browser/lib/results/shared.ts +8 -0
- package/extensions/agent-browser/lib/runtime.ts +369 -25
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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",
|
|
@@ -49,6 +60,8 @@ const GLOBAL_FLAGS_WITH_VALUES = new Set([
|
|
|
49
60
|
]);
|
|
50
61
|
const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
|
|
51
62
|
const MAX_PROJECT_SLUG_LENGTH = 24;
|
|
63
|
+
const SESSION_NAME_CWD_HASH_LENGTH = 8;
|
|
64
|
+
const SESSION_NAME_SESSION_ID_LENGTH = 12;
|
|
52
65
|
|
|
53
66
|
export interface CommandInfo {
|
|
54
67
|
command?: string;
|
|
@@ -64,9 +77,19 @@ export interface SessionRecoveryHint {
|
|
|
64
77
|
recommendedSessionMode: "fresh";
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
export interface InvalidValueFlagDetails {
|
|
81
|
+
flag: string;
|
|
82
|
+
index: number;
|
|
83
|
+
reason: "missing-value" | "unexpected-flag";
|
|
84
|
+
receivedToken?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
export interface ExecutionPlan {
|
|
68
88
|
commandInfo: CommandInfo;
|
|
69
89
|
effectiveArgs: string[];
|
|
90
|
+
invalidValueFlag?: InvalidValueFlagDetails;
|
|
91
|
+
managedSessionName?: string;
|
|
92
|
+
plainTextInspection: boolean;
|
|
70
93
|
recoveryHint?: SessionRecoveryHint;
|
|
71
94
|
sessionName?: string;
|
|
72
95
|
startupScopedFlags: string[];
|
|
@@ -74,10 +97,152 @@ export interface ExecutionPlan {
|
|
|
74
97
|
validationError?: string;
|
|
75
98
|
}
|
|
76
99
|
|
|
100
|
+
export interface ManagedSessionState {
|
|
101
|
+
active: boolean;
|
|
102
|
+
replacedSessionName?: string;
|
|
103
|
+
sessionName: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RestoredManagedSessionState extends ManagedSessionState {
|
|
107
|
+
freshSessionOrdinal: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
export interface PromptPolicy {
|
|
78
111
|
allowLegacyAgentBrowserBash: boolean;
|
|
79
112
|
}
|
|
80
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
|
+
|
|
81
246
|
export function hasUsableBraveApiKey(apiKey: string | null | undefined = process.env[BRAVE_API_KEY_ENV]): boolean {
|
|
82
247
|
return typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
83
248
|
}
|
|
@@ -105,25 +270,106 @@ export function getImplicitSessionCloseTimeoutMs(env: NodeJS.ProcessEnv = proces
|
|
|
105
270
|
return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
|
|
106
271
|
}
|
|
107
272
|
|
|
108
|
-
export function
|
|
273
|
+
export function resolveManagedSessionState(options: {
|
|
109
274
|
command?: string;
|
|
275
|
+
managedSessionName?: string;
|
|
110
276
|
priorActive: boolean;
|
|
277
|
+
priorSessionName: string;
|
|
111
278
|
succeeded: boolean;
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
279
|
+
}): ManagedSessionState {
|
|
280
|
+
const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
|
|
281
|
+
if (!managedSessionName) {
|
|
282
|
+
return { active: priorActive, sessionName: priorSessionName };
|
|
283
|
+
}
|
|
284
|
+
if (command === "close" && managedSessionName === priorSessionName) {
|
|
285
|
+
return { active: succeeded ? false : priorActive, sessionName: priorSessionName };
|
|
286
|
+
}
|
|
287
|
+
if (!succeeded) {
|
|
288
|
+
return { active: priorActive, sessionName: priorSessionName };
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
active: true,
|
|
292
|
+
replacedSessionName: priorActive && priorSessionName !== managedSessionName ? priorSessionName : undefined,
|
|
293
|
+
sessionName: managedSessionName,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
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
|
+
}
|
|
118
357
|
}
|
|
119
|
-
|
|
120
|
-
return
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
...restoredState,
|
|
361
|
+
freshSessionOrdinal,
|
|
362
|
+
};
|
|
121
363
|
}
|
|
122
364
|
|
|
123
365
|
export function createEphemeralSessionSeed(): string {
|
|
124
366
|
return randomUUID();
|
|
125
367
|
}
|
|
126
368
|
|
|
369
|
+
function createCwdHash(cwd: string): string {
|
|
370
|
+
return createHash("sha256").update(`cwd:${cwd}`).digest("hex").slice(0, SESSION_NAME_CWD_HASH_LENGTH);
|
|
371
|
+
}
|
|
372
|
+
|
|
127
373
|
export function createImplicitSessionName(
|
|
128
374
|
sessionId: string | undefined,
|
|
129
375
|
cwd: string,
|
|
@@ -135,13 +381,25 @@ export function createImplicitSessionName(
|
|
|
135
381
|
.replace(/[^a-z0-9]+/g, "-")
|
|
136
382
|
.replace(/^-+|-+$/g, "")
|
|
137
383
|
.slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
|
|
138
|
-
const
|
|
384
|
+
const cwdHash = createCwdHash(cwd);
|
|
385
|
+
const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
139
386
|
if (stableSessionId && stableSessionId.length > 0) {
|
|
140
|
-
return `piab-${slug}-${stableSessionId}`;
|
|
387
|
+
return `piab-${slug}-${stableSessionId}-${cwdHash}`;
|
|
141
388
|
}
|
|
142
389
|
|
|
143
|
-
const digest = createHash("sha256")
|
|
144
|
-
|
|
390
|
+
const digest = createHash("sha256")
|
|
391
|
+
.update(`ephemeral:${cwd}:${ephemeralSeed}`)
|
|
392
|
+
.digest("hex")
|
|
393
|
+
.slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
394
|
+
return `piab-${slug}-${digest}-${cwdHash}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function createFreshSessionName(baseSessionName: string, ephemeralSeed: string, ordinal: number): string {
|
|
398
|
+
const suffix = createHash("sha256")
|
|
399
|
+
.update(`fresh:${baseSessionName}:${ephemeralSeed}:${ordinal}`)
|
|
400
|
+
.digest("hex")
|
|
401
|
+
.slice(0, 10);
|
|
402
|
+
return `${baseSessionName}-fresh-${suffix}`;
|
|
145
403
|
}
|
|
146
404
|
|
|
147
405
|
export function validateToolArgs(args: string[]): string | undefined {
|
|
@@ -157,6 +415,54 @@ export function validateToolArgs(args: string[]): string | undefined {
|
|
|
157
415
|
return undefined;
|
|
158
416
|
}
|
|
159
417
|
|
|
418
|
+
function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
|
|
419
|
+
for (const [index, token] of args.entries()) {
|
|
420
|
+
if (!token.startsWith("-")) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const normalizedToken = token.split("=", 1)[0] ?? token;
|
|
424
|
+
if (!GLOBAL_FLAGS_WITH_VALUES.has(normalizedToken)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (token.includes("=")) {
|
|
428
|
+
const value = token.slice(token.indexOf("=") + 1).trim();
|
|
429
|
+
if (value.length === 0) {
|
|
430
|
+
return {
|
|
431
|
+
flag: normalizedToken,
|
|
432
|
+
index,
|
|
433
|
+
reason: "missing-value",
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const receivedToken = args[index + 1];
|
|
439
|
+
if (receivedToken === undefined) {
|
|
440
|
+
return {
|
|
441
|
+
flag: normalizedToken,
|
|
442
|
+
index,
|
|
443
|
+
reason: "missing-value",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (receivedToken.startsWith("-")) {
|
|
447
|
+
return {
|
|
448
|
+
flag: normalizedToken,
|
|
449
|
+
index,
|
|
450
|
+
reason: "unexpected-flag",
|
|
451
|
+
receivedToken,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formatInvalidValueFlagError(details: InvalidValueFlagDetails): string {
|
|
460
|
+
if (details.reason === "unexpected-flag" && details.receivedToken) {
|
|
461
|
+
return `Flag \`${details.flag}\` requires a value, but received \`${details.receivedToken}\` instead. Pass a non-flag value immediately after \`${details.flag}\`.`;
|
|
462
|
+
}
|
|
463
|
+
return `Flag \`${details.flag}\` requires a value immediately after it. Pass a non-flag token like \`${details.flag} demo\`.`;
|
|
464
|
+
}
|
|
465
|
+
|
|
160
466
|
function hasFlagToken(args: string[], flag: string): boolean {
|
|
161
467
|
return args.some((token) => token === flag || token.startsWith(`${flag}=`));
|
|
162
468
|
}
|
|
@@ -213,35 +519,72 @@ export function getLatestUserPrompt(branch: unknown[]): string {
|
|
|
213
519
|
|
|
214
520
|
export function buildExecutionPlan(
|
|
215
521
|
args: string[],
|
|
216
|
-
options: {
|
|
522
|
+
options: {
|
|
523
|
+
freshSessionName: string;
|
|
524
|
+
managedSessionActive: boolean;
|
|
525
|
+
managedSessionName: string;
|
|
526
|
+
sessionMode: SessionMode;
|
|
527
|
+
},
|
|
217
528
|
): ExecutionPlan {
|
|
529
|
+
const invalidValueFlag = getInvalidValueFlagDetails(args);
|
|
530
|
+
const startupScopedFlags = getStartupScopedFlags(args);
|
|
531
|
+
const plainTextInspection = isPlainTextInspectionArgs(args);
|
|
218
532
|
const commandInfo = parseCommandInfo(args);
|
|
533
|
+
const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
|
|
534
|
+
if (invalidValueFlag) {
|
|
535
|
+
return {
|
|
536
|
+
commandInfo: {},
|
|
537
|
+
effectiveArgs,
|
|
538
|
+
invalidValueFlag,
|
|
539
|
+
plainTextInspection: false,
|
|
540
|
+
startupScopedFlags: [],
|
|
541
|
+
usedImplicitSession: false,
|
|
542
|
+
validationError: formatInvalidValueFlagError(invalidValueFlag),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (plainTextInspection) {
|
|
547
|
+
return {
|
|
548
|
+
commandInfo,
|
|
549
|
+
effectiveArgs,
|
|
550
|
+
plainTextInspection,
|
|
551
|
+
startupScopedFlags,
|
|
552
|
+
usedImplicitSession: false,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
219
556
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
220
|
-
const
|
|
221
|
-
|
|
557
|
+
const shouldCreateFreshManagedSession =
|
|
558
|
+
!explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
|
|
559
|
+
let managedSessionName: string | undefined;
|
|
222
560
|
let recoveryHint: SessionRecoveryHint | undefined;
|
|
223
561
|
let sessionName = explicitSessionName;
|
|
224
562
|
let usedImplicitSession = false;
|
|
225
563
|
let validationError: string | undefined;
|
|
226
564
|
|
|
227
565
|
if (!explicitSessionName && options.sessionMode === "auto") {
|
|
228
|
-
if (options.
|
|
566
|
+
if (options.managedSessionActive && startupScopedFlags.length > 0) {
|
|
229
567
|
recoveryHint = {
|
|
230
568
|
exampleArgs: args,
|
|
231
569
|
exampleParams: { args, sessionMode: "fresh" },
|
|
232
570
|
reason:
|
|
233
|
-
"Startup-scoped flags like --profile, --session-name, and --cdp need a fresh upstream launch once the
|
|
571
|
+
"Startup-scoped flags like --profile, --session-name, and --cdp need a fresh upstream launch once the extension-managed session is already active.",
|
|
234
572
|
recommendedSessionMode: "fresh",
|
|
235
573
|
};
|
|
236
574
|
validationError = [
|
|
237
|
-
`The current
|
|
575
|
+
`The current extension-managed agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
|
|
238
576
|
"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.",
|
|
239
577
|
].join(" ");
|
|
240
578
|
} else {
|
|
241
|
-
effectiveArgs.push("--session", options.
|
|
242
|
-
|
|
579
|
+
effectiveArgs.push("--session", options.managedSessionName);
|
|
580
|
+
managedSessionName = options.managedSessionName;
|
|
581
|
+
sessionName = options.managedSessionName;
|
|
243
582
|
usedImplicitSession = true;
|
|
244
583
|
}
|
|
584
|
+
} else if (shouldCreateFreshManagedSession) {
|
|
585
|
+
effectiveArgs.push("--session", options.freshSessionName);
|
|
586
|
+
managedSessionName = options.freshSessionName;
|
|
587
|
+
sessionName = options.freshSessionName;
|
|
245
588
|
}
|
|
246
589
|
|
|
247
590
|
effectiveArgs.push(...args);
|
|
@@ -249,6 +592,8 @@ export function buildExecutionPlan(
|
|
|
249
592
|
return {
|
|
250
593
|
commandInfo,
|
|
251
594
|
effectiveArgs,
|
|
595
|
+
managedSessionName,
|
|
596
|
+
plainTextInspection,
|
|
252
597
|
recoveryHint,
|
|
253
598
|
sessionName,
|
|
254
599
|
startupScopedFlags,
|
|
@@ -280,4 +625,3 @@ export function parseCommandInfo(args: string[]): CommandInfo {
|
|
|
280
625
|
|
|
281
626
|
return { command: commands[0], subcommand: commands[1] };
|
|
282
627
|
}
|
|
283
|
-
|
package/package.json
CHANGED