pi-agent-browser-native 0.3.0 → 0.5.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -0,0 +1,299 @@
1
+ import { open } from "node:fs/promises";
2
+ import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES, AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES, AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES, createAgentBrowserScriptCloseArgs, isAgentBrowserScriptSessionName, validateAgentBrowserScriptBrowserParams, } from "../input-modes/script.js";
3
+ import { isRecord } from "../parsing.js";
4
+ import { parseCommandInfo, redactSensitiveText } from "../runtime.js";
5
+ import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
6
+ import { redactPresentationData } from "../results/presentation/diagnostics.js";
7
+ import { truncateText } from "../results/text.js";
8
+ const SCRIPT_SESSION_ENTRY_TYPE = "agent-browser-script-session";
9
+ const SCRIPT_BROWSER_SUMMARY_MAX_CHARS = 1_024;
10
+ const SCRIPT_BROWSER_TEXT_MAX_CHARS = 8_192;
11
+ export function getScriptSessionLeasesFromBranch(branch) {
12
+ const leases = new Map();
13
+ for (const entry of branch) {
14
+ if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== SCRIPT_SESSION_ENTRY_TYPE || !isRecord(entry.data))
15
+ continue;
16
+ const { cleanup, closeCommandArgs, launchAttempted, sessionName } = entry.data;
17
+ if (!isAgentBrowserScriptSessionName(sessionName))
18
+ continue;
19
+ const expectedCloseCommandArgs = createAgentBrowserScriptCloseArgs(sessionName);
20
+ if ((cleanup !== "active" && cleanup !== "closed" && cleanup !== "failed")
21
+ || launchAttempted !== true
22
+ || !Array.isArray(closeCommandArgs)
23
+ || closeCommandArgs.length !== expectedCloseCommandArgs.length
24
+ || !closeCommandArgs.every((token, index) => token === expectedCloseCommandArgs[index]))
25
+ continue;
26
+ leases.set(sessionName, { cleanup, closeCommandArgs: expectedCloseCommandArgs, launchAttempted: true, sessionName });
27
+ }
28
+ return leases;
29
+ }
30
+ export function appendScriptSessionLease(pi, sessionName, cleanup) {
31
+ pi.appendEntry(SCRIPT_SESSION_ENTRY_TYPE, {
32
+ cleanup,
33
+ closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName),
34
+ launchAttempted: true,
35
+ sessionName,
36
+ });
37
+ }
38
+ async function readVerifiedScriptSpill(result) {
39
+ const details = isRecord(result.details) ? result.details : undefined;
40
+ const path = typeof details?.fullOutputPath === "string" ? details.fullOutputPath : undefined;
41
+ const manifest = isSessionArtifactManifest(details?.artifactManifest) ? details.artifactManifest : undefined;
42
+ if (!path || !manifest?.entries.some((entry) => entry.kind === "spill"
43
+ && (entry.path === path || entry.absolutePath === path)
44
+ && (entry.storageScope === "persistent-session" || entry.storageScope === "process-temp")
45
+ && (entry.retentionState === "live" || entry.retentionState === "ephemeral")))
46
+ return undefined;
47
+ let handle;
48
+ try {
49
+ handle = await open(path, "r");
50
+ const stats = await handle.stat();
51
+ if (!stats.isFile() || stats.size < 0 || stats.size > AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES)
52
+ return undefined;
53
+ const buffer = Buffer.alloc(stats.size);
54
+ let offset = 0;
55
+ while (offset < buffer.length) {
56
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
57
+ if (bytesRead === 0)
58
+ break;
59
+ offset += bytesRead;
60
+ }
61
+ const text = buffer.subarray(0, offset).toString("utf8");
62
+ try {
63
+ return JSON.parse(text);
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ finally {
73
+ await handle?.close().catch(() => undefined);
74
+ }
75
+ }
76
+ function getToolResultText(result) {
77
+ return result.content
78
+ .filter((item) => item.type === "text")
79
+ .map((item) => item.text)
80
+ .join("\n\n");
81
+ }
82
+ function stripOwnedScriptSessionArgs(args, sessionName) {
83
+ if (!isAgentBrowserScriptSessionName(sessionName))
84
+ return args;
85
+ if (args[0] === "--namespace" && args[1] === "" && args[2] === "--session" && args[3] === sessionName)
86
+ return args.slice(4);
87
+ return args[0] === "--session" && args[1] === sessionName ? args.slice(2) : args;
88
+ }
89
+ function getScriptCompatibleNextActions(value, sessionName) {
90
+ if (!Array.isArray(value))
91
+ return undefined;
92
+ const actions = value.flatMap((action) => {
93
+ if (!isRecord(action))
94
+ return [];
95
+ if (!isRecord(action.params))
96
+ return typeof action.artifactPath === "string" ? [action] : [];
97
+ if (Object.keys(action.params).some((key) => key !== "args" && key !== "stdin"))
98
+ return [];
99
+ if (!Array.isArray(action.params.args) || action.params.args.some((token) => typeof token !== "string"))
100
+ return [];
101
+ if (action.params.stdin !== undefined && typeof action.params.stdin !== "string")
102
+ return [];
103
+ const normalizedParams = {
104
+ args: stripOwnedScriptSessionArgs(action.params.args, sessionName),
105
+ ...(typeof action.params.stdin === "string" ? { stdin: action.params.stdin } : {}),
106
+ };
107
+ if (!validateAgentBrowserScriptBrowserParams(normalizedParams).params)
108
+ return [];
109
+ return [{ ...action, params: normalizedParams }];
110
+ });
111
+ return actions.length > 0 ? actions : undefined;
112
+ }
113
+ function scriptBrowserEnvelopeFitsIpc(envelope) {
114
+ try {
115
+ return Buffer.byteLength(JSON.stringify({ envelope, id: Number.MAX_SAFE_INTEGER, type: "response" }), "utf8") + 1 <= AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ function buildOversizedScriptBrowserEnvelope() {
122
+ const text = "Browser result exceeds the script IPC response limit; narrow the inner browser call output.";
123
+ return {
124
+ data: null,
125
+ details: { failureCategory: "upstream-error", resultCategory: "failure" },
126
+ error: text,
127
+ failureCategory: "upstream-error",
128
+ ok: false,
129
+ resultCategory: "failure",
130
+ summary: text,
131
+ text,
132
+ };
133
+ }
134
+ export async function buildScriptBrowserEnvelope(result, args, scriptSessionName) {
135
+ const details = isRecord(result.details) ? result.details : undefined;
136
+ const fullData = await readVerifiedScriptSpill(result);
137
+ const commandInfo = parseCommandInfo(args);
138
+ const data = redactPresentationData(commandInfo, fullData ?? details?.data ?? null);
139
+ const resultCategory = details?.resultCategory === "failure" || result.isError === true ? "failure" : "success";
140
+ const text = truncateText(redactSensitiveText(getToolResultText(result)), SCRIPT_BROWSER_TEXT_MAX_CHARS);
141
+ const summary = truncateText(redactSensitiveText(typeof details?.summary === "string" ? details.summary : text.split("\n", 1)[0] || "Browser call completed."), SCRIPT_BROWSER_SUMMARY_MAX_CHARS);
142
+ const compatibleNextActions = getScriptCompatibleNextActions(details?.nextActions, scriptSessionName);
143
+ const redactedNextActions = getScriptCompatibleNextActions(redactPresentationData(commandInfo, compatibleNextActions), undefined);
144
+ const failureCategory = resultCategory === "failure" && typeof details?.failureCategory === "string"
145
+ ? details.failureCategory
146
+ : undefined;
147
+ const successCategory = resultCategory === "success" && typeof details?.successCategory === "string"
148
+ ? details.successCategory
149
+ : undefined;
150
+ const envelopeDetails = redactPresentationData(commandInfo, {
151
+ artifactVerification: details?.artifactVerification,
152
+ artifacts: details?.artifacts,
153
+ failureCategory,
154
+ pageChangeSummary: details?.pageChangeSummary,
155
+ resultCategory,
156
+ successCategory,
157
+ });
158
+ const envelope = {
159
+ data,
160
+ details: isRecord(envelopeDetails) ? envelopeDetails : { resultCategory },
161
+ error: resultCategory === "failure" ? summary : undefined,
162
+ failureCategory,
163
+ nextActions: Array.isArray(redactedNextActions) ? redactedNextActions : undefined,
164
+ ok: resultCategory === "success",
165
+ resultCategory,
166
+ successCategory,
167
+ summary,
168
+ text,
169
+ };
170
+ return scriptBrowserEnvelopeFitsIpc(envelope) ? envelope : buildOversizedScriptBrowserEnvelope();
171
+ }
172
+ function collectUniqueArtifacts(results) {
173
+ const records = results.flatMap((result) => {
174
+ const details = isRecord(result.details) ? result.details : undefined;
175
+ return Array.isArray(details?.artifacts) ? details.artifacts.filter(isRecord) : [];
176
+ });
177
+ const unique = new Map(records.map((record) => [String(record.absolutePath ?? record.path ?? JSON.stringify(record)), record]));
178
+ return unique.size > 0 ? [...unique.values()] : undefined;
179
+ }
180
+ function collectScriptArtifactVerification(results) {
181
+ const entries = results.flatMap((result) => {
182
+ const details = isRecord(result.details) ? result.details : undefined;
183
+ const verification = isRecord(details?.artifactVerification) ? details.artifactVerification : undefined;
184
+ return Array.isArray(verification?.artifacts) ? verification.artifacts.filter(isRecord) : [];
185
+ });
186
+ const unique = [...new Map(entries.map((entry) => [String(entry.absolutePath ?? entry.path ?? JSON.stringify(entry)), entry])).values()];
187
+ if (unique.length === 0)
188
+ return undefined;
189
+ const count = (state) => unique.filter((entry) => entry.state === state).length;
190
+ return {
191
+ artifacts: unique,
192
+ missingCount: count("missing"),
193
+ pendingCount: count("pending"),
194
+ unverifiedCount: count("unverified"),
195
+ verified: unique.every((entry) => entry.state === "verified"),
196
+ verifiedCount: count("verified"),
197
+ };
198
+ }
199
+ function getLatestScriptResultDetail(results, key) {
200
+ for (let index = results.length - 1; index >= 0; index -= 1) {
201
+ const rawDetails = results[index]?.details;
202
+ const details = isRecord(rawDetails) ? rawDetails : undefined;
203
+ if (details?.[key] !== undefined)
204
+ return details[key];
205
+ }
206
+ return undefined;
207
+ }
208
+ function buildScriptCloseNextAction(sessionName) {
209
+ return {
210
+ id: "close-script-session-after-cleanup-failure",
211
+ params: { args: createAgentBrowserScriptCloseArgs(sessionName) },
212
+ reason: "Retry closing the isolated wrapper-owned script session after automatic cleanup failed.",
213
+ safety: "Use these exact args; do not add profile, state, restore, namespace, or connection flags.",
214
+ tool: "agent_browser",
215
+ };
216
+ }
217
+ export function buildScriptToolResult(options) {
218
+ let data;
219
+ let serializedData;
220
+ let outputError;
221
+ try {
222
+ data = redactPresentationData({ command: "script" }, options.run.data);
223
+ serializedData = data === undefined ? undefined : JSON.stringify(data);
224
+ if (data !== undefined && serializedData === undefined)
225
+ throw new TypeError("Script output is not JSON-serializable.");
226
+ if (serializedData !== undefined && Buffer.byteLength(serializedData, "utf8") > AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES) {
227
+ throw new RangeError(`Redacted script output exceeds ${AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES} bytes.`);
228
+ }
229
+ }
230
+ catch {
231
+ data = undefined;
232
+ serializedData = undefined;
233
+ outputError = "Final script output could not be safely rendered as bounded JSON.";
234
+ }
235
+ const steps = options.run.steps.map((step) => ({ ...step, summary: redactSensitiveText(step.summary) }));
236
+ const rejectedFailureCategory = steps.find((step) => step.failureCategory === "policy-blocked")?.failureCategory
237
+ ?? (options.run.rejectedCallCount > 0 ? "validation-error" : undefined);
238
+ const failureCategory = options.cleanupError
239
+ ? "cleanup-failed"
240
+ : outputError ? "validation-error" : options.run.failureCategory ?? rejectedFailureCategory;
241
+ const failed = failureCategory !== undefined || !options.run.ok;
242
+ const failedStepCount = steps.filter((step) => !step.ok).length;
243
+ const failedCallCount = Math.max(0, failedStepCount - options.run.rejectedCallCount);
244
+ const successfulCallCount = steps.filter((step) => step.ok).length;
245
+ const cleanupSuffix = options.sessionName && !options.cleanupError ? " Isolated script session closed." : "";
246
+ const summary = `${options.cleanupError
247
+ ? `Script completed, but isolated session cleanup failed: ${redactSensitiveText(options.cleanupError)}`
248
+ : outputError
249
+ ? `Script failed: ${outputError}`
250
+ : options.run.ok
251
+ ? rejectedFailureCategory
252
+ ? `Script failed: ${options.run.rejectedCallCount} browser call${options.run.rejectedCallCount === 1 ? " was" : "s were"} rejected before dispatch.`
253
+ : failedCallCount > 0
254
+ ? `Script completed (${options.run.callCount} browser call${options.run.callCount === 1 ? "" : "s"}; ${failedCallCount} returned failure for script handling).`
255
+ : `Script completed (${options.run.callCount} browser call${options.run.callCount === 1 ? "" : "s"}).`
256
+ : `Script failed: ${redactSensitiveText(options.run.error ?? "sandbox execution failed")}`}${cleanupSuffix}`;
257
+ const dataText = serializedData === undefined ? "" : `\n\n${serializedData}`;
258
+ const cleanupActionText = options.cleanupError && options.sessionName
259
+ ? `\n\nNext action: ${JSON.stringify({ args: createAgentBrowserScriptCloseArgs(options.sessionName) })}`
260
+ : "";
261
+ const artifactManifest = getLatestScriptResultDetail(options.innerResults, "artifactManifest");
262
+ const artifactRetentionSummary = getLatestScriptResultDetail(options.innerResults, "artifactRetentionSummary");
263
+ const artifacts = collectUniqueArtifacts(options.innerResults);
264
+ const artifactVerification = collectScriptArtifactVerification(options.innerResults);
265
+ const nextActions = options.cleanupError && options.sessionName ? [buildScriptCloseNextAction(options.sessionName)] : undefined;
266
+ return {
267
+ content: [{ type: "text", text: `${summary}${dataText}${cleanupActionText}` }],
268
+ details: {
269
+ artifactManifest,
270
+ artifactRetentionSummary,
271
+ artifacts,
272
+ artifactVerification,
273
+ data,
274
+ failureCategory,
275
+ nextActions,
276
+ resultCategory: failed ? "failure" : "success",
277
+ scriptRun: {
278
+ aborted: options.run.aborted,
279
+ callCount: options.run.callCount,
280
+ emitCount: options.run.emitCount,
281
+ failedCallCount,
282
+ preDispatchRejectedCallCount: options.run.rejectedCallCount,
283
+ successfulCallCount,
284
+ timedOut: options.run.timedOut,
285
+ },
286
+ scriptSession: options.sessionName ? {
287
+ cleanup: options.cleanupError ? "failed" : "closed",
288
+ closeCommandArgs: createAgentBrowserScriptCloseArgs(options.sessionName),
289
+ ...(options.cleanupError ? { error: redactSensitiveText(options.cleanupError) } : {}),
290
+ launchAttempted: true,
291
+ sessionName: options.sessionName,
292
+ } : undefined,
293
+ scriptSteps: steps,
294
+ successCategory: failed ? undefined : "completed",
295
+ summary,
296
+ },
297
+ isError: failed,
298
+ };
299
+ }
@@ -7,14 +7,20 @@ import { isRecord } from "./parsing.js";
7
7
  import { redactInvocationArgs } from "./runtime.js";
8
8
  const TUI_INVOCATION_PREVIEW_MAX_CHARS = 160;
9
9
  const TUI_COLLAPSED_OUTPUT_MAX_LINES = 12;
10
- const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
10
+ const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B\r\n\u2028\u2029]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B\r\n\u2028\u2029]*(?:\x1B\\)|_[^\x1B\r\n\u2028\u2029]*(?:\x1B\\)|\^[^\x1B\r\n\u2028\u2029]*(?:\x1B\\)|[@-Z\\-_])/g;
11
11
  const JSON_TOKEN_PATTERN = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\],:]/g;
12
12
  const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
13
- function sanitizeDisplayText(value) {
14
- return value
15
- .replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
16
- .replace(/\r/g, "")
17
- .replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "");
13
+ const UNSAFE_DISPLAY_DIRECTIONAL_PATTERN = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g;
14
+ const UNSAFE_DISPLAY_ZERO_WIDTH_PATTERN = /[\u200B-\u200D\u2060\uFEFF]/g;
15
+ function sanitizeDisplayText(value, markRemovedSequences = false) {
16
+ let sanitized = value
17
+ .replace(/\r\n?/g, "\n")
18
+ .replace(ANSI_CONTROL_SEQUENCE_PATTERN, markRemovedSequences ? "�" : "")
19
+ .replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�")
20
+ .replace(UNSAFE_DISPLAY_DIRECTIONAL_PATTERN, "�");
21
+ if (markRemovedSequences)
22
+ sanitized = sanitized.replace(/[\u2028\u2029]/g, "\n").replace(UNSAFE_DISPLAY_ZERO_WIDTH_PATTERN, "�");
23
+ return sanitized;
18
24
  }
19
25
  function replaceTabsForDisplay(value) {
20
26
  return value.replaceAll("\t", " ");
@@ -89,6 +95,8 @@ function formatVisualTruncationNotice(remainingLines, totalLines, theme, width)
89
95
  return truncateToWidth(notice, Math.max(0, width));
90
96
  }
91
97
  function getStructuredModeInvocation(input) {
98
+ if (typeof input.script === "string")
99
+ return { mode: "script", rawArgs: [], scriptSource: input.script };
92
100
  if (Array.isArray(input.args))
93
101
  return { rawArgs: input.args.filter((value) => typeof value === "string") };
94
102
  if (input.semanticAction !== undefined)
@@ -114,14 +122,28 @@ function formatInvocationPreview(rawArgs) {
114
122
  ? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
115
123
  : invocation;
116
124
  }
117
- export function formatAgentBrowserRenderCall(args, theme) {
125
+ function formatScriptSourceForDisplay(source, expanded) {
126
+ const sanitizedSource = replaceTabsForDisplay(sanitizeDisplayText(source, true));
127
+ if (expanded)
128
+ return sanitizedSource;
129
+ const preview = sanitizedSource.replace(/\n/g, " ↵ ").replace(/\s+/g, " ").trim();
130
+ return preview.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
131
+ ? `${preview.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
132
+ : preview;
133
+ }
134
+ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
118
135
  const input = isRecord(args) ? args : {};
119
- const { mode, rawArgs } = getStructuredModeInvocation(input);
120
- const invocationPreview = formatInvocationPreview(rawArgs);
136
+ const { mode, rawArgs, scriptSource } = getStructuredModeInvocation(input);
137
+ const invocationPreview = scriptSource === undefined
138
+ ? formatInvocationPreview(rawArgs)
139
+ : formatScriptSourceForDisplay(scriptSource, expanded);
121
140
  let text = theme.fg("toolTitle", theme.bold("agent_browser"));
122
141
  if (mode) {
123
142
  text += ` ${theme.fg("accent", mode)}`;
124
- if (invocationPreview.length > 0) {
143
+ if (scriptSource !== undefined && expanded) {
144
+ text += `\n${theme.fg("dim", "Source:")}\n${theme.fg("accent", invocationPreview)}`;
145
+ }
146
+ else if (invocationPreview.length > 0) {
125
147
  text += ` ${theme.fg("dim", "→")} ${theme.fg("accent", invocationPreview)}`;
126
148
  }
127
149
  }
@@ -7,30 +7,32 @@ export function buildInstalledDocsGuideline(paths) {
7
7
  return `For detailed agent_browser docs, read targeted sections: ${paths.readmePath} (setup), ${paths.commandReferencePath} (commands), ${paths.toolContractPath} (result/details). Do not load the full command reference unless needed.`;
8
8
  }
9
9
  export const QUICK_START_GUIDELINES = [
10
- `Quick start mental model: use exactly one of args (exact agent-browser CLI args after the binary), semanticAction (a thin shorthand compiled to find argv for locator actions, direct selector/ref click/check/fill, or select argv for native dropdowns), job (a constrained short-workflow schema compiled to batch --bail by default; set failFast:false only when later diagnostics remain safe if an earlier navigation fails), qa (a lightweight fail-fast QA preset built on batch --bail with bounded visible expected-text checks, including qa.attached for current sessions), electron (desktop Electron list/launch/status/cleanup/probe), or the experimental sourceLookup / networkSourceLookup helpers (candidates only; each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and is rejected with electron; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) to apply. Use outputPath for durable eval/get/snapshot captures. Do not pass --json in args; the wrapper injects it.`,
11
- "There is no first-class reusable named browser recipe runtime above top-level job, the qa preset, and raw batch stdin; keep recurring flows in documentation examples or those inputs (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
10
+ `Quick start mental model: use exactly one of script (one-shot JavaScript orchestration), args (exact agent-browser CLI args after the binary), semanticAction (a thin shorthand compiled to find argv for locator actions, direct selector/ref click/check/fill, or select argv for native dropdowns), job (a constrained short-workflow schema compiled to batch --bail by default; set failFast:false only when later diagnostics remain safe if an earlier navigation fails), qa (a lightweight fail-fast QA preset built on batch --bail with bounded visible expected-text checks, including qa.attached for current sessions), electron (desktop Electron list/launch/status/cleanup/probe), or the experimental sourceLookup / networkSourceLookup helpers (candidates only; each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and is rejected with electron; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) to apply. Use outputPath for durable eval/get/snapshot captures. Do not pass --json in args; the wrapper injects it.`,
11
+ "Use script only when loops, conditional page branches, or multi-page aggregation would otherwise require several top-level calls: call await browser({ args, stdin?, timeoutMs? }), check each returned { ok, data, error, details, failureCategory, nextActions, resultCategory, summary, text } envelope, and call emit(value) for the one final JSON value. Script runs in a one-shot isolated session without profiles, attachments, imports, host filesystem/network/process access, caller session controls, or inherited agent-browser launch/proxy settings; inner browser calls and their response envelopes are serialized and bounded, compatible suggested next actions can be passed back to browser(), and the wrapper always closes that session. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear work.",
12
+ "There is no first-class reusable named browser recipe runtime: script is ad hoc source for one call only, with no saved names, registry, or versioned workflow state. Keep recurring flows in documentation examples, job, qa, or raw batch (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
12
13
  "Common first calls (first-call recipe): { args: [\"open\", \"<url>\"] } → { args: [\"snapshot\", \"-i\"] } → { args: [\"click\", \"@eN\"] } or { args: [\"fill\", \"@eN\", \"<text>\"] } using @refs and visible labels from that snapshot, then { args: [\"snapshot\", \"-i\"] } after navigation or DOM changes. On https://example.com/ the main link label is Learn more (use exact snapshot text, not guessed link copy).",
13
14
  "Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, direct current targets such as { semanticAction: { action: \"fill\", selector: \"@e1\", text: \"prompt\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/fill actions to avoid hidden duplicate matches; semanticAction does not expose uncheck while upstream find ... uncheck is not runtime-supported, so use raw uncheck with a stable selector or current ref; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
14
15
  `Common advanced calls: { args: ["batch", "--bail"], stdin: "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }, { job: { steps: [{ action: "open", url: "https://example.com" }, { action: "assertText", text: "Example Domain" }, { action: "screenshot", path: ".dogfood/example.png" }] } }, { qa: { url: "https://example.com", expectedText: "Example Domain", screenshotPath: ".dogfood/qa-example.png" } } (example.com smoke only; elsewhere match exact visible text from snapshot -i), { electron: { action: "list", query: "code" } }, { electron: { action: "launch", appName: "Visual Studio Code", handoff: "snapshot" } }, { electron: { action: "probe" } }, { qa: { attached: true, expectedText: "Explorer" } }, { args: ["eval", "--stdin"], stdin: "document.title", outputPath: "logs/page-title.json" }, { args: ["auth", "save", "name", "--password-stdin"], stdin: "<password from user-approved secret source>" }, { args: ["--profile", "Default", "open", "https://example.com/account"], sessionMode: "fresh" }, and { args: ["open", "--enable", "react-devtools", "https://example.com"], sessionMode: "fresh" }. For app pages with a native dropdown, job steps can include { action: "select", selector: "#flavor", value: "chocolate" } before the dependent assertion; for locator-friendly pages, job click/fill steps can use semantic locator fields such as { action: "fill", locator: "role", role: "searchbox", name: "Search", text: "agent browser" }; for human-paced input, job type steps can use { action: "type", selector: "#prompt", text: "hello", delayMs: 20, press: "Enter" }; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in visible batch prose while full rows remain in details.batchSteps.`,
15
16
  "Constrained job navigation is explicit only: click (and select/submit flows that may navigate) does not prove the next page loaded; add assertUrl and/or assertText after navigation-prone steps before screenshot or later interactions. Keep jobs short around navigation, click, and rerender boundaries on dynamic React/product apps; avoid a whole checkout in one job. If a long job times out and details.timeoutPartialProgress shows a mutating incomplete step, inspect current page state and continue with a shorter job or single action instead of blindly retrying the mutating step. Example: { job: { steps: [{ action: \"open\", url: \"https://shop.example/checkout\" }, { action: \"fill\", selector: \"#email\", text: \"user@example.com\" }, { action: \"click\", selector: \"#continue\" }, { action: \"assertUrl\", url: \"**/shipping\" }, { action: \"assertText\", text: \"Shipping address\" }, { action: \"screenshot\", path: \".dogfood/shipping.png\" }] } }. Top-level click may add pageChangeSummary hints, but job never auto-inserts post-click asserts.",
16
17
  "High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; wrapper-handled scroll <dir> [px|percent] and scroll to end/top target document scrolling before upstream fallback, while scroll <selector> <dir> [px|percent] targets nested scrollers; download <selector> <path> saves a file triggered by a click; read [url] returns agent-readable text (explicit URLs prefer markdown without launching Chrome; omit the URL for rendered active-tab DOM); get title/url need no selector; get text/html/value/count <selector> and get attr <selector> <name> read elements/page state (use body for whole-page text/html); screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree, react inspect <fiberId>, react renders start/stop, and react suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
17
- "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target. The wrapper creates parent directories for direct artifact paths and can save simple loopback HTTP(S) anchor downloads directly to the requested path before upstream download fallback. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. A bare inbound image/video path is not a requested output artifact and does not block close. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
18
+ "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target. Upstream record start uses a fresh active page for video capture, so prior in-page DOM and JavaScript state does not carry over; the wrapper blocks prior @e… refs as stale-ref even when the start fails as already-active, and record restart with a URL navigates and invalidates the same way (plain record restart keeps the page), so take a fresh snapshot before continuing. The wrapper creates parent directories for direct artifact paths and can save simple loopback HTTP(S) anchor downloads directly to the requested path before upstream download fallback. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. A bare inbound image/video path is not a requested output artifact and does not block close. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
18
19
  "When details.nextActions is present, prefer those exact native agent_browser follow-up payloads over prose guidance; they may include args, stdin, sessionMode, networkSourceLookup, safety notes, or artifactPath for saved files.",
19
20
  ];
20
21
  export const WEB_SEARCH_PROMPT_GUIDELINE = "Use agent_browser_web_search for quick live search/URL discovery; prefer it over public search-engine forms that can hit anti-bot/CAPTCHA-gated pages. Use agent_browser after you have a target URL; one query, one follow-up max; stop on HTTP 429.";
21
22
  export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
23
+ "Use top-level script only for one-shot loops, conditional page branches, or multi-page aggregation that would otherwise require several calls: await browser({ args, stdin?, timeoutMs? }), branch on its ok field, and emit one bounded JSON value. Script gets an isolated non-profile browser session that is always closed, cannot use caller session/namespace/lifecycle/attachment controls, inherited agent-browser launch/proxy settings, or host APIs, and is not a reusable named recipe. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear flows.",
22
24
  "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 are page-scoped; the wrapper fails mutation-prone stale/recycled refs before upstream can silently target a different current-page element. On dense pages, use wrapper-side snapshot -i --search <text> or snapshot -i --filter role=<role> to render matching refs while preserving the full ref map in details.refSnapshot, add snapshot --viewport when scroll position or above/below-fold context matters, and add snapshot --diff when a quick before/after ref-map delta would prevent reading a full spill file.",
23
25
  "For ordinary forms from one snapshot, batch multiple fill @refs before the submit/click step to avoid serial tool calls; if a fill may autosubmit, navigate, or rerender later fields, split the flow and refresh refs first.",
24
26
  "Do not use browser automation to drive public search-engine forms such as Google for discovery; headless jobs that type a query and press Enter can be redirected to anti-bot or CAPTCHA pages. Use agent_browser_web_search when configured, ask for/search from a direct target URL, or navigate to known result URLs. Do not attempt CAPTCHA bypass.",
25
- "Snapshot choice: prefer snapshot -i for routine clicks/fills (interactive @refs, main-content-first). Use snapshot --compact when you need a denser same-page tree without full spill; use full snapshot (no -i) only when you need the complete accessibility tree. Re-snapshot after navigation or major DOM changes. When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
27
+ "Snapshot choice: prefer snapshot -i for routine clicks/fills (interactive @refs, main-content-first). Use snapshot --compact when you need a denser same-page tree without full spill; use full snapshot (no -i) only when you need the complete accessibility tree. Re-snapshot after navigation or major DOM changes. When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, named action links, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
26
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.",
27
29
  "For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard inserttext or keyboard type with the intended text. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
28
30
  "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.",
29
- "For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data; use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
31
+ "For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data. On macOS, copied Chrome profiles may omit encrypted cookies, so profile selection alone is not proof of authentication; verify the target page and use a user-approved headed login once when needed. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
30
32
  "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.",
31
33
  `When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), 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.`,
32
34
  "Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.",
33
- "After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume, omits local-launch-only `--args` / `--allow-file-access` defaults from follow-up and cleanup subprocesses so upstream keeps the existing CDP connection, and live-checks the URL before later page reads/interactions because an attached browser can drift externally; a successful close clears the marker. First-use attach plus content calls are blocked until the URL is verified.",
35
+ "After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume, omits local-launch-only `--args` / `--allow-file-access` defaults from follow-up and cleanup subprocesses so upstream keeps the existing CDP connection, and live-checks the URL before later page reads/interactions because an attached browser can drift externally; a successful close clears the marker. First-use attach plus content calls are blocked until the URL is verified. When several named sessions share one Chrome, pass --pin-tab once (AGENT_BROWSER_PIN_TAB) so a closed bound tab fails as tab_gone instead of acting on a neighbor; recover with tab new or tab list. --no-pin-tab turns the sticky pin off. tab list includes each tab's CDP targetId, accepted as a tab ref.",
34
36
  `If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
35
37
  "For WebGPU pages, use args [\"--webgpu\", \"open\", \"<url>\"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.",
36
38
  "For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.",
@@ -53,14 +55,14 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
53
55
  "When details.pageChangeSummary is present, use changeType and summary as a compact signal for navigation, DOM mutation, confirmations, or artifacts; when nextActionIds is set, match those ids to entries in details.nextActions (or per-step nextActions inside batch) for concrete follow-up payloads instead of inferring from prose alone. If details.clickDispatch reports a click-dispatch miss, refresh/inspect/retry the real click first; for static local fixtures only, an explicit eval --stdin programmatic .click() can exercise app handlers, but treat it as an untrusted scripted workaround and never use it to bypass stop-before-submit/order/purchase boundaries. If a no-navigation click surfaces details.overlayBlockers, inspect the fresh snapshot evidence before using a close/dismiss candidate nextAction; ordinary page chrome without dialog/alertdialog evidence should not trigger this diagnostic.",
54
56
  "When commands save or spill files (screenshots, downloads, PDFs, traces, recordings, HAR, large snapshot spills), use the user's exact requested paths when given and treat paths as provisional until details.artifactVerification shows every row verified: branch on missingCount, pendingCount, unverifiedCount, per-entry state, and optional limitation before downstream file use or PASS/FAIL reporting.",
55
57
  "For evidence-only screenshots, QA captures, or other audit artifacts, save to an explicit path and branch on details.artifactVerification plus details.artifacts before reporting PASS/FAIL; do not require vision review of inline image attachments unless the user asked for visual inspection.",
56
- "Respect explicit user stop boundaries yourself: if the user says to stop before order/post/purchase/submit, do not click that final action. The wrapper does not infer broad business intent from prompt text; details.promptGuard is reserved for concrete artifact-before-close checks.",
58
+ "Respect explicit user stop boundaries yourself. When the surrounding authenticated employee or automation context is explicitly unattended/auto-approved, ordinary non-destructive form submissions within the requested flow may proceed without separate confirmation. Still require explicit authorization for purchases, production-control actions, destructive or irreversible actions, and account, security, or privacy changes. The wrapper does not infer broad business intent from prompt text; details.promptGuard is reserved for concrete artifact-before-close checks.",
57
59
  "Successful record stop needs ffmpeg on PATH; the wrapper may warn after record start when ffmpeg is missing.",
58
60
  "Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
59
61
  ];
60
62
  export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
61
63
  "Prefer agent_browser over bash, osascript, AppleScript, or generic browser shell for sites, docs, clicks, fills, screenshots, eval, and batch.",
62
- "Pass exact agent-browser CLI arguments in agent_browser args when you are not using semanticAction, job, or qa, excluding the binary name and --json (agent_browser injects --json automatically).",
63
- "Use agent_browser stdin only for eval --stdin, batch, auth save --password-stdin, or wrapper-generated job/qa batches instead of shell heredocs or password args; other command/stdin combinations are rejected before launch.",
64
+ "Pass exact agent-browser CLI arguments in agent_browser args when you are not using script, semanticAction, job, or qa, excluding the binary name and --json (agent_browser injects --json automatically).",
65
+ "Use top-level agent_browser stdin only for eval --stdin, batch, auth save --password-stdin, or wrapper-generated job/qa batches instead of shell heredocs or password args; script puts any inner stdin on browser({ stdin }), and other command/stdin combinations are rejected before launch.",
64
66
  `Let the agent_browser extension-managed session handle the common path unless you explicitly need a fresh launch for launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}).`,
65
67
  "Use agent_browser sessionMode=fresh when switching from an existing implicit session to a new profile/browser executable/debug/init-script/provider launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
66
68
  ];
@@ -73,15 +75,16 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
73
75
  "After the wrapper observes tab-drift risk for a session (for example open correction, overlapping stale opens, or resumed session state), later active-tab commands best-effort pin that tab inside the same upstream invocation. Routine same-session commands are not preflighted with tab list just because a target tab or ref snapshot is known.",
74
76
  "For sessions with observed tab-drift risk, 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. Routine same-session commands skip this post-command tab-list probe.",
75
77
  "If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
78
+ "If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.",
76
79
  ];
77
80
  /** Tier A: always-on tool promptGuidelines (keep small; Tier B lives in SHARED_BROWSER_PLAYBOOK_GUIDELINES and docs). */
78
81
  export const RUNTIME_PROMPT_GUIDELINES = [
79
- "Use agent_browser with one input mode: args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin only for batch/eval/auth/wrapper batch; electron rejects stdin; never pass --json.",
80
- "For agent_browser, use open → snapshot -i → current @refs or semanticAction → re-snapshot after navigation/scroll/rerender. Batch same-snapshot forms; split before navigation/submits. Stop before order/post/purchase/submit.",
81
- "Use agent_browser sessionMode=fresh for launch-scoped flags incl. --allowed-domains; never put --session-mode in args. Use requested/configured profiles only; on profile failures run profiles/doctor. Profile content is model-visible. Restores project cookies; SSO may need --headed once.",
82
- "For agent_browser artifacts, use exact user paths and verify details.artifactVerification/details.artifacts before claiming success. Save details.promptGuard-required artifacts before close; record stop needs ffmpeg; close keeps files; waited:timeout is not proof.",
83
- "When agent_browser details.nextActions exists, use exact payloads over guessed selectors/prose. Dense snapshots: check Omitted high-value controls. Dashboards: verify scroll with screenshot/snapshot.",
84
- "For agent_browser extraction: read <url> for docs/text; read for active-tab DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>; eval --stdin for targeted state. Batch 3+ getters; heed visibility warnings.",
82
+ "Use agent_browser with one input mode: script, args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin: batch/eval/auth/wrapper batch only; electron rejects it; never pass --json.",
83
+ "For agent_browser, use open → snapshot -i → @refs; re-snapshot after changes. In authenticated unattended/auto-approved employee flows, ordinary requested non-destructive submissions may proceed. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
84
+ "Use agent_browser sessionMode=fresh for launch flags. Use requested/configured profiles only; run profiles/doctor on failure. --allowed-domains cannot restore; macOS profile copies may omit encrypted cookies. Verify auth; use a user-approved headed login if needed. Profile content is model-visible.",
85
+ "agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; record stop needs ffmpeg; close keeps files; waited:timeout proves nothing.",
86
+ "When agent_browser details.nextActions exists, use exact payloads. Check Omitted high-value controls in dense snapshots. Dashboards: verify scroll with screenshot/snapshot.",
87
+ "agent_browser: read <url> for docs/text or active DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>. Batch 3+ getters; heed visibility warnings.",
85
88
  ];
86
89
  export function buildBrowserExecutablePathGuideline(executablePath) {
87
90
  if (!executablePath)
@@ -0,0 +1,14 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const isolatedAgentBrowserEnvironment = new AsyncLocalStorage();
3
+ const PROXY_ENV_NAMES = new Set(["ALL_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"]);
4
+ export function getAgentBrowserProcessEnvironment(baseEnv = process.env) {
5
+ if (isolatedAgentBrowserEnvironment.getStore() !== true)
6
+ return baseEnv;
7
+ return Object.fromEntries(Object.entries(baseEnv).filter(([name]) => {
8
+ const normalizedName = name.toUpperCase();
9
+ return !normalizedName.startsWith("AGENT_BROWSER_") && !PROXY_ENV_NAMES.has(normalizedName);
10
+ }));
11
+ }
12
+ export function withIsolatedAgentBrowserEnvironment(run) {
13
+ return isolatedAgentBrowserEnvironment.run(true, run);
14
+ }
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
- import { win32 } from "node:path";
2
+ import { dirname, join, win32 } from "node:path";
3
3
  const WINDOWS_PROCESS_START_IDENTITY_PREFIX = "win32-powershell-ticks-v1:";
4
- const PROCESS_START_IDENTITY_TIMEOUT_MS = 1_000;
4
+ const PROCESS_START_IDENTITY_TIMEOUT_MS = 5_000;
5
5
  const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
6
6
  export function buildProcessStartIdentityCommand(pid, platform = process.platform) {
7
7
  if (!Number.isSafeInteger(pid) || pid <= 0)
@@ -22,7 +22,7 @@ export function buildProcessStartIdentityCommand(pid, platform = process.platfor
22
22
  }
23
23
  : {
24
24
  args: ["-p", String(pid), "-o", "lstart="],
25
- file: "/bin/ps",
25
+ file: platform === "android" ? join(dirname(process.execPath), "ps") : "/bin/ps",
26
26
  };
27
27
  }
28
28
  export function buildProcessStartIdentityCommands(pid, platform = process.platform) {
@@ -31,7 +31,7 @@ export function buildProcessStartIdentityCommands(pid, platform = process.platfo
31
31
  return [];
32
32
  return platform === "win32"
33
33
  ? [primary]
34
- : [primary, { ...primary, file: "/usr/bin/ps" }];
34
+ : [primary, ...(platform === "android" ? [{ ...primary, file: "/bin/ps" }, { ...primary, file: "/usr/bin/ps" }] : [{ ...primary, file: "/usr/bin/ps" }])];
35
35
  }
36
36
  export function normalizeProcessStartIdentity(stdout) {
37
37
  return stdout.trim().replace(/\s+/g, " ") || undefined;