pi-agent-browser-native 0.2.11 → 0.2.13

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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Purpose: Centralize low-level boundary parsing helpers shared by runtime planning, temp-artifact lifecycle, and result rendering.
3
+ * Responsibilities: Identify non-null object records and normalize positive-integer string configuration values.
4
+ * Scope: Tiny generic parsing predicates only; module-specific validation and error handling stay with their owning modules.
5
+ * Usage: Imported by agent-browser wrapper modules that parse untyped JSON, persisted state, or environment variables.
6
+ * Invariants/Assumptions: Arrays intentionally count as records to preserve existing object-boundary semantics, and positive integers must be safe base-10 integer strings greater than zero.
7
+ */
8
+
9
+ export function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === "object" && value !== null;
11
+ }
12
+
13
+ export function parsePositiveInteger(rawValue: string | undefined): number | undefined {
14
+ if (typeof rawValue !== "string") return undefined;
15
+ const normalizedValue = rawValue.trim();
16
+ if (!/^\d+$/.test(normalizedValue)) return undefined;
17
+ const parsedValue = Number(normalizedValue);
18
+ if (!Number.isSafeInteger(parsedValue) || parsedValue <= 0) return undefined;
19
+ return parsedValue;
20
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Purpose: Provide the canonical agent_browser operating playbook shared by runtime prompt metadata and generated documentation fragments.
3
+ * Responsibilities: Define stable guidance bullets, native tool-call examples, and wrapper-behavior notes without importing runtime/browser process code.
4
+ * Scope: Agent-facing documentation and prompt-guidance text only; command execution and wrapper state behavior live in runtime modules.
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.
7
+ */
8
+
9
+ export const PROJECT_RULE_PROMPT =
10
+ "Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
11
+
12
+ export const TOOL_PROMPT_GUIDELINES_PREFIX = [
13
+ "Use agent_browser whenever the task requires a real browser or live web content.",
14
+ ] as const;
15
+
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 eval --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, or --auto-connect state.",
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\" }, and { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }.",
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.",
21
+ ] as const;
22
+
23
+ export const BRAVE_SEARCH_PROMPT_GUIDELINE =
24
+ "When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.";
25
+
26
+ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
27
+ "Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs can become stale.",
28
+ "When a visible text or accessible-name target should survive ref churn, prefer find locators such as role, text, label, placeholder, alt, title, or testid with the intended action instead of guessing a CSS selector.",
29
+ "Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
30
+ "For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
31
+ "Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
32
+ "When using --profile, --session-name, --cdp, --state, or --auto-connect, put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.",
33
+ "If you already used the implicit session and now need launch-scoped flags like --profile, --session-name, --cdp, --state, or --auto-connect, retry with sessionMode set to fresh or pass an explicit --session for the new launch. After a successful unnamed fresh launch, later auto calls follow that new session.",
34
+ "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load <state>, --url <matcher>, --fn <js>, or --text <matcher>.",
35
+ "For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
36
+ "For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
37
+ "For downloads, prefer download <selector> <path> when an element click should save a file. Do not rely on click alone when you need the downloaded file on disk.",
38
+ "When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
39
+ "When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel.",
40
+ "Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
41
+ ] as const;
42
+
43
+ export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
44
+ "Prefer agent_browser over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
45
+ "Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when agent_browser can do the job.",
46
+ "Pass exact agent-browser CLI arguments in args, excluding the binary name.",
47
+ "Use stdin only for eval --stdin and batch instead of shell heredocs; other command/stdin combinations are rejected before launch.",
48
+ "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, or --auto-connect.",
49
+ "Use sessionMode=fresh when switching from an existing implicit session to a new profile/debug launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
50
+ ] as const;
51
+
52
+ export const INSPECTION_TOOL_CALL_EXAMPLES = [
53
+ '{ "args": ["--help"] }',
54
+ '{ "args": ["--version"] }',
55
+ ] as const;
56
+
57
+ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
58
+ "After launch-scoped open/goto/navigate calls that can restore existing tabs (for example --profile, --session-name, or --state), agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch.",
59
+ "After a target tab is known for a session, later active-tab commands best-effort pin that tab inside the same upstream invocation when reconnect drift would otherwise move the command to a restored/background tab.",
60
+ "After a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes.",
61
+ "If a known session target unexpectedly reports about:blank, agent_browser preserves the prior intended target, best-effort re-selects it when it still exists, and reports exact recovery guidance when it cannot be re-selected.",
62
+ ] as const;
63
+
64
+ export function buildSharedBrowserPlaybookGuidelines(options: { includeBraveSearch: boolean }): string[] {
65
+ return [
66
+ SHARED_BROWSER_PLAYBOOK_GUIDELINES[0],
67
+ ...(options.includeBraveSearch ? [BRAVE_SEARCH_PROMPT_GUIDELINE] : []),
68
+ ...SHARED_BROWSER_PLAYBOOK_GUIDELINES.slice(1),
69
+ ];
70
+ }
71
+
72
+ export function buildToolPromptGuidelines(options: { includeBraveSearch: boolean }): string[] {
73
+ return [
74
+ ...TOOL_PROMPT_GUIDELINES_PREFIX,
75
+ ...QUICK_START_GUIDELINES,
76
+ ...buildSharedBrowserPlaybookGuidelines(options),
77
+ ...TOOL_PROMPT_GUIDELINES_SUFFIX,
78
+ ];
79
+ }
@@ -63,12 +63,27 @@ const INHERITED_ENV_NAMES = new Set([
63
63
  "USERPROFILE",
64
64
  "WAYLAND_DISPLAY",
65
65
  "XAUTHORITY",
66
+ "AWS_ACCESS_KEY_ID",
67
+ "AWS_SECRET_ACCESS_KEY",
68
+ "AWS_SESSION_TOKEN",
69
+ "AWS_PROFILE",
70
+ "AWS_REGION",
71
+ "AWS_DEFAULT_REGION",
66
72
  httpProxyEnvName,
67
73
  httpsProxyEnvName,
68
74
  allProxyEnvName,
69
75
  noProxyEnvName,
70
76
  ]);
71
- const INHERITED_ENV_PREFIXES = ["AI_GATEWAY_", "XDG_"] as const;
77
+ const INHERITED_ENV_PREFIXES = [
78
+ "AGENT_BROWSER_",
79
+ "AGENTCORE_",
80
+ "AI_GATEWAY_",
81
+ "BROWSERBASE_",
82
+ "BROWSERLESS_",
83
+ "BROWSER_USE_",
84
+ "KERNEL_",
85
+ "XDG_",
86
+ ] as const;
72
87
 
73
88
  export interface ProcessRunResult {
74
89
  aborted: boolean;
@@ -140,8 +155,9 @@ export async function runAgentBrowserProcess(options: {
140
155
  stdin?: string;
141
156
  }): Promise<ProcessRunResult> {
142
157
  const { args, cwd, env, signal, stdin } = options;
143
- let effectiveEnv = env;
144
- const requestedSocketDir = env?.[AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
158
+ const explicitSocketDir = env?.[AGENT_BROWSER_SOCKET_DIR_ENV];
159
+ let effectiveEnv = explicitSocketDir === undefined ? { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : env;
160
+ const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
145
161
  if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
146
162
  effectiveEnv = { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
147
163
  }
@@ -159,6 +175,7 @@ export async function runAgentBrowserProcess(options: {
159
175
  let pendingStdoutWrite = Promise.resolve();
160
176
  let stdoutSpillError: Error | undefined;
161
177
  let killTimer: NodeJS.Timeout | undefined;
178
+ let abortListener: (() => void) | undefined;
162
179
 
163
180
  const queueStdoutChunk = (buffer: Buffer) => {
164
181
  stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
@@ -193,10 +210,17 @@ export async function runAgentBrowserProcess(options: {
193
210
  });
194
211
  };
195
212
 
213
+ const removeAbortListener = () => {
214
+ if (!signal || !abortListener) return;
215
+ signal.removeEventListener("abort", abortListener);
216
+ abortListener = undefined;
217
+ };
218
+
196
219
  const finish = (exitCode: number) => {
197
220
  if (settled) return;
198
221
  settled = true;
199
222
  void pendingStdoutWrite.finally(async () => {
223
+ removeAbortListener();
200
224
  if (killTimer) {
201
225
  clearTimeout(killTimer);
202
226
  }
@@ -230,7 +254,33 @@ export async function runAgentBrowserProcess(options: {
230
254
  child.kill("SIGKILL");
231
255
  }, 2_000);
232
256
  };
257
+ const recordStdinError = (error: unknown) => {
258
+ const stdinError = error instanceof Error ? error : new Error(String(error));
259
+ const errorCode = (stdinError as NodeJS.ErrnoException).code;
260
+ if (errorCode === "EPIPE" || errorCode === "ERR_STREAM_DESTROYED") {
261
+ return;
262
+ }
263
+ if (!spawnError) {
264
+ spawnError = stdinError;
265
+ }
266
+ };
267
+ const writeChildStdin = () => {
268
+ if (aborted || signal?.aborted) {
269
+ child.stdin.destroy();
270
+ return;
271
+ }
272
+ try {
273
+ if (stdin) {
274
+ child.stdin.write(stdin);
275
+ }
276
+ child.stdin.end();
277
+ } catch (error) {
278
+ recordStdinError(error);
279
+ child.stdin.destroy();
280
+ }
281
+ };
233
282
 
283
+ child.stdin.on("error", recordStdinError);
234
284
  child.once("error", (error) => {
235
285
  spawnError = error instanceof Error ? error : new Error(String(error));
236
286
  finish(127);
@@ -249,13 +299,11 @@ export async function runAgentBrowserProcess(options: {
249
299
  if (signal.aborted) {
250
300
  abortChild();
251
301
  } else {
252
- signal.addEventListener("abort", abortChild, { once: true });
302
+ abortListener = abortChild;
303
+ signal.addEventListener("abort", abortListener, { once: true });
253
304
  }
254
305
  }
255
306
 
256
- if (stdin) {
257
- child.stdin.write(stdin);
258
- }
259
- child.stdin.end();
307
+ writeChildStdin();
260
308
  });
261
309
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Purpose: Detect upstream guarded-action confirmation-needed result shapes without creating wrapper-owned confirmation state.
3
+ * Responsibilities: Recognize confirmation-required markers, extract the pending upstream confirmation id, and optionally surface a short upstream action label.
4
+ * Scope: Pure result-shape detection shared by presentation and error derivation; command execution, approval state, and redaction stay in their existing modules.
5
+ * Usage: Imported by result presentation to render recovery commands and by envelope error handling to avoid hiding actionable confirmation payloads behind generic failure text.
6
+ * Invariants/Assumptions: Detection must be conservative: a confirmation marker and a non-empty upstream id are both required before a result is treated as actionable.
7
+ */
8
+
9
+ import { isRecord } from "../parsing.js";
10
+
11
+ export interface ConfirmationRequiredPresentation {
12
+ id: string;
13
+ actionText?: string;
14
+ }
15
+
16
+ const CONFIRMATION_REQUIRED_FIELD_NAMES = [
17
+ "confirmation_required",
18
+ "confirmationRequired",
19
+ "requires_confirmation",
20
+ "requiresConfirmation",
21
+ ] as const;
22
+ const CONFIRMATION_REQUIRED_RECORD_FIELD_NAMES = ["confirmation", "pendingConfirmation", "pending_confirmation"] as const;
23
+ const CONFIRMATION_ID_FIELD_NAMES = ["confirmation_id", "confirmationId", "id"] as const;
24
+ const CONFIRMATION_ACTION_TEXT_FIELD_NAMES = ["action", "description", "message", "summary"] as const;
25
+ const CONFIRMATION_REQUIRED_MARKER = "confirmation_required";
26
+
27
+ function getTrimmedStringField(data: Record<string, unknown>, fieldNames: readonly string[]): string | undefined {
28
+ for (const fieldName of fieldNames) {
29
+ const value = data[fieldName];
30
+ if (typeof value === "string" && value.trim().length > 0) {
31
+ return value.trim();
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ function hasConfirmationRequiredMarker(data: Record<string, unknown>): boolean {
38
+ return CONFIRMATION_REQUIRED_FIELD_NAMES.some((fieldName) => data[fieldName] === true)
39
+ || data.type === CONFIRMATION_REQUIRED_MARKER
40
+ || data.status === CONFIRMATION_REQUIRED_MARKER
41
+ || data.kind === CONFIRMATION_REQUIRED_MARKER;
42
+ }
43
+
44
+ function getNestedConfirmationRecord(data: Record<string, unknown>): Record<string, unknown> | undefined {
45
+ for (const fieldName of CONFIRMATION_REQUIRED_RECORD_FIELD_NAMES) {
46
+ const value = data[fieldName];
47
+ if (isRecord(value)) {
48
+ return value;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ export function detectConfirmationRequired(data: unknown): ConfirmationRequiredPresentation | undefined {
55
+ if (!isRecord(data)) {
56
+ return undefined;
57
+ }
58
+
59
+ const nestedRecord = getNestedConfirmationRecord(data);
60
+ const candidateRecords = nestedRecord ? [data, nestedRecord] : [data];
61
+ if (!candidateRecords.some(hasConfirmationRequiredMarker)) {
62
+ return undefined;
63
+ }
64
+
65
+ for (const record of candidateRecords) {
66
+ const id = getTrimmedStringField(record, CONFIRMATION_ID_FIELD_NAMES);
67
+ if (!id) {
68
+ continue;
69
+ }
70
+ return {
71
+ actionText: getTrimmedStringField(record, CONFIRMATION_ACTION_TEXT_FIELD_NAMES),
72
+ id,
73
+ };
74
+ }
75
+ return undefined;
76
+ }
@@ -8,7 +8,9 @@
8
8
 
9
9
  import { readFile } from "node:fs/promises";
10
10
 
11
- import { type AgentBrowserBatchResult, type AgentBrowserEnvelope, isRecord, stringifyUnknown } from "./shared.js";
11
+ import { isRecord } from "../parsing.js";
12
+ import { detectConfirmationRequired } from "./confirmation.js";
13
+ import { type AgentBrowserBatchResult, type AgentBrowserEnvelope, stringifyUnknown } from "./shared.js";
12
14
 
13
15
  function hasStructuredBatchStepFailure(data: unknown): data is AgentBrowserBatchResult[] {
14
16
  return Array.isArray(data) && data.some((item) => isRecord(item) && item.success === false);
@@ -75,21 +77,56 @@ export async function parseAgentBrowserEnvelope(options: string | { stdout: stri
75
77
  if (!isRecord(parsed)) {
76
78
  return { parseError: "agent-browser returned JSON, but it was not an object envelope." };
77
79
  }
78
- return { envelope: parsed };
80
+ if (!("success" in parsed)) {
81
+ return { parseError: "agent-browser returned an invalid JSON envelope: missing boolean success field." };
82
+ }
83
+ if (typeof parsed.success !== "boolean") {
84
+ return { parseError: "agent-browser returned an invalid JSON envelope: success field must be boolean." };
85
+ }
86
+ return { envelope: parsed as AgentBrowserEnvelope };
79
87
  } catch (error) {
80
88
  const message = error instanceof Error ? error.message : String(error);
81
89
  return { parseError: `agent-browser returned invalid JSON: ${message}` };
82
90
  }
83
91
  }
84
92
 
93
+ function buildInvocationLabel(options: { command?: string; effectiveArgs?: string[] }): string {
94
+ if (options.effectiveArgs && options.effectiveArgs.length > 0) {
95
+ return `agent-browser ${options.effectiveArgs.join(" ")}`;
96
+ }
97
+ if (options.command && options.command.trim().length > 0) {
98
+ return `agent-browser ${options.command.trim()}`;
99
+ }
100
+ return "agent-browser";
101
+ }
102
+
103
+ function appendWrapperRecoveryHint(message: string, wrapperRecoveryHint?: string): string {
104
+ const hint = wrapperRecoveryHint?.trim();
105
+ return hint ? `${message}\n${hint}` : message;
106
+ }
107
+
108
+ function buildFailureFallback(options: { command?: string; effectiveArgs?: string[]; exitCode: number; wrapperRecoveryHint?: string }): string {
109
+ const invocation = buildInvocationLabel(options);
110
+ const exitSuffix = options.exitCode !== 0 ? ` (exit code ${options.exitCode})` : "";
111
+ return appendWrapperRecoveryHint(`${invocation} reported failure${exitSuffix}.`, options.wrapperRecoveryHint);
112
+ }
113
+
114
+ function buildExitCodeFallback(options: { command?: string; effectiveArgs?: string[]; exitCode: number; wrapperRecoveryHint?: string }): string {
115
+ const invocation = buildInvocationLabel(options);
116
+ return appendWrapperRecoveryHint(`${invocation} exited with code ${options.exitCode}.`, options.wrapperRecoveryHint);
117
+ }
118
+
85
119
  export function getAgentBrowserErrorText(options: {
86
120
  aborted: boolean;
121
+ command?: string;
122
+ effectiveArgs?: string[];
87
123
  envelope?: AgentBrowserEnvelope;
88
124
  exitCode: number;
89
125
  parseError?: string;
90
126
  plainTextInspection: boolean;
91
127
  spawnError?: Error;
92
128
  stderr: string;
129
+ wrapperRecoveryHint?: string;
93
130
  }): string | undefined {
94
131
  const { aborted, envelope, exitCode, parseError, plainTextInspection, spawnError, stderr } = options;
95
132
  if (plainTextInspection) return undefined;
@@ -97,13 +134,13 @@ export function getAgentBrowserErrorText(options: {
97
134
  if (spawnError) return spawnError.message;
98
135
  if (parseError) return parseError;
99
136
  if (envelope?.success === false) {
100
- if (hasStructuredBatchStepFailure(envelope.data) && envelope.error === undefined) {
137
+ if ((hasStructuredBatchStepFailure(envelope.data) || detectConfirmationRequired(envelope.data)) && envelope.error === undefined) {
101
138
  return undefined;
102
139
  }
103
- return extractEnvelopeErrorText(envelope.error) ?? (stderr.trim() || `agent-browser reported failure${exitCode !== 0 ? ` (exit code ${exitCode})` : "."}`);
140
+ return extractEnvelopeErrorText(envelope.error) ?? (stderr.trim() || buildFailureFallback(options));
104
141
  }
105
142
  if (exitCode !== 0) {
106
- return stderr.trim() || `agent-browser exited with code ${exitCode}.`;
143
+ return stderr.trim() || buildExitCodeFallback(options);
107
144
  }
108
145
  return undefined;
109
146
  }