pi-agent-browser-native 0.3.0 → 0.6.5

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 (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -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
+ }
@@ -0,0 +1,270 @@
1
+ import { parseArgvDescriptor } from "./argv-descriptor.js";
2
+ import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, } from "./argv-grammar.js";
3
+ import { needsManagedSession } from "./command-policy.js";
4
+ import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
5
+ import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
6
+ const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly before page-content inspection.";
7
+ const BATCH_UNVERIFIED_PAGE_MESSAGE = `${UNVERIFIED_PAGE_MESSAGE} In a batch, put get url after the transition before later content steps, or split the batch at that boundary.`;
8
+ const UNSAFE_BATCH_ARGUMENT_MESSAGE = "Batch command arguments could not be inspected. Use batch stdin JSON command arrays instead.";
9
+ const NESTED_BATCH_ARGUMENT_MESSAGE = "Nested batch commands are not supported. Flatten the batch steps instead.";
10
+ const NON_BAIL_BATCH_NAVIGATION_MESSAGE = "Batches that change or re-verify the page target before page-content access must use exact batch --bail so a failed step cannot act on an unverified or prior page.";
11
+ const MAX_NON_BAIL_BATCH_PAGE_STATES = 64;
12
+ const EXPLICIT_NAVIGATION_COMMANDS = new Set(["a11y", "goto", "navigate", "open", "pushstate", "visit", "vitals", "web-vitals"]);
13
+ const POSITIONAL_VALUE_FLAGS = new Set([...VALUE_FLAGS, "--llms"]);
14
+ function getPositionalOperands(commandTokens) {
15
+ const values = [];
16
+ for (let index = 1; index < commandTokens.length; index += 1) {
17
+ const token = commandTokens[index];
18
+ if (!token || (token.includes("=") && token.startsWith("-")))
19
+ continue;
20
+ if (POSITIONAL_VALUE_FLAGS.has(token)) {
21
+ index += 1;
22
+ continue;
23
+ }
24
+ if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(token)) {
25
+ if (["true", "false"].includes(commandTokens[index + 1] ?? ""))
26
+ index += 1;
27
+ continue;
28
+ }
29
+ if (!token.startsWith("-") || token.includes("/") || token.includes("\\"))
30
+ values.push(token);
31
+ }
32
+ return values;
33
+ }
34
+ function getExplicitNavigationTarget(args) {
35
+ const descriptor = parseArgvDescriptor(args);
36
+ const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
37
+ if (EXPLICIT_NAVIGATION_COMMANDS.has(descriptor.commandInfo.command ?? ""))
38
+ return positionals[0];
39
+ if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
40
+ return positionals[1];
41
+ return undefined;
42
+ }
43
+ function getResultingExplicitNavigationTarget(args, currentPageUrl) {
44
+ const target = getExplicitNavigationTarget(args);
45
+ if (target === undefined)
46
+ return undefined;
47
+ if (parseArgvDescriptor(args).commandInfo.command !== "pushstate")
48
+ return target;
49
+ try {
50
+ return new URL(target).href;
51
+ }
52
+ catch {
53
+ if (!currentPageUrl)
54
+ return undefined;
55
+ try {
56
+ return new URL(target, currentPageUrl).href;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ }
63
+ function getBatchCommandSteps(args, stdin) {
64
+ const descriptor = parseArgvDescriptor(args);
65
+ if (descriptor.commandInfo.command !== "batch")
66
+ return { steps: [] };
67
+ const argumentCommands = descriptor.upstreamCommandTokens.slice(1).filter((command) => command !== "--bail");
68
+ const steps = [];
69
+ if (argumentCommands.length > 0) {
70
+ for (const command of argumentCommands) {
71
+ const parsed = parseBatchCommandArgument(command);
72
+ if (parsed.error || parsed.step === undefined)
73
+ return { error: parsed.error ?? UNSAFE_BATCH_ARGUMENT_MESSAGE, steps: [] };
74
+ if (parseArgvDescriptor(parsed.step).commandInfo.command === "batch")
75
+ return { error: NESTED_BATCH_ARGUMENT_MESSAGE, steps: [] };
76
+ steps.push(parsed.step);
77
+ }
78
+ return { steps };
79
+ }
80
+ const parsedStdin = parseUserBatchStdin(stdin);
81
+ if (parsedStdin.error)
82
+ return { error: parsedStdin.error, steps: [] };
83
+ for (const step of parsedStdin.steps ?? []) {
84
+ if (parseArgvDescriptor(step).commandInfo.command === "batch")
85
+ return { error: NESTED_BATCH_ARGUMENT_MESSAGE, steps: [] };
86
+ steps.push(step);
87
+ }
88
+ return { steps };
89
+ }
90
+ function batchBailsOnFirstError(args) {
91
+ const descriptor = parseArgvDescriptor(args);
92
+ return descriptor.commandInfo.command === "batch" && descriptor.upstreamCommandTokens.slice(1).includes("--bail");
93
+ }
94
+ function commandMayChangePageTarget(args, trustedBatchTabSelection) {
95
+ const descriptor = parseArgvDescriptor(args);
96
+ return getExplicitNavigationTarget(args) !== undefined
97
+ || (isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand)
98
+ && !(trustedBatchTabSelection && descriptor.commandInfo.command === "tab"));
99
+ }
100
+ function commandVerifiesPageTarget(args) {
101
+ const { command, subcommand } = parseArgvDescriptor(args).commandInfo;
102
+ return command === "get" && subcommand === "url";
103
+ }
104
+ function deduplicatePossibleBatchPageStates(states) {
105
+ const deduplicated = new Map();
106
+ for (const state of states) {
107
+ const key = `${state.pageUrlUnknown ? "unknown" : "known"}\0${state.currentPageUrl ?? ""}`;
108
+ const existing = deduplicated.get(key);
109
+ if (!existing)
110
+ deduplicated.set(key, state);
111
+ else
112
+ existing.retainedAfterFailedNavigation ||= state.retainedAfterFailedNavigation;
113
+ }
114
+ return [...deduplicated.values()];
115
+ }
116
+ export function commandRequiresLivePageVerification(args, stdin) {
117
+ const descriptor = parseArgvDescriptor(args);
118
+ if (descriptor.commandInfo.command === "eval")
119
+ return true;
120
+ if (descriptor.commandInfo.command !== "batch")
121
+ return false;
122
+ const batch = getBatchCommandSteps(args, stdin);
123
+ return batch.error === undefined && batch.steps.some((step) => commandRequiresLivePageVerification(step));
124
+ }
125
+ function getResultingPageState(options) {
126
+ const descriptor = parseArgvDescriptor(options.args);
127
+ if (descriptor.commandInfo.command === "get" && descriptor.commandInfo.subcommand === "url") {
128
+ return { currentPageUrl: options.currentPageUrl, pageUrlUnknown: false };
129
+ }
130
+ const rawExplicitTarget = getExplicitNavigationTarget(options.args);
131
+ const explicitTarget = getResultingExplicitNavigationTarget(options.args, options.currentPageUrl);
132
+ if (explicitTarget !== undefined)
133
+ return { currentPageUrl: explicitTarget, pageUrlUnknown: false };
134
+ if (rawExplicitTarget !== undefined)
135
+ return { pageUrlUnknown: true };
136
+ if (isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand)) {
137
+ return options.trustedBatchTabSelection && descriptor.commandInfo.command === "tab"
138
+ ? { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown }
139
+ : { pageUrlUnknown: true };
140
+ }
141
+ return { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown };
142
+ }
143
+ export function getResultingPageTargetState(options) {
144
+ const descriptor = parseArgvDescriptor(options.args);
145
+ let state = { currentPageUrl: options.currentPageUrl, pageUrlUnknown: options.pageUrlUnknown ?? false };
146
+ let pageTargetMayHaveChanged = false;
147
+ if (descriptor.commandInfo.command !== "batch") {
148
+ pageTargetMayHaveChanged = getExplicitNavigationTarget(options.args) !== undefined
149
+ || isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand);
150
+ return { ...getResultingPageState({ ...state, args: options.args, trustedBatchTabSelection: false }), pageTargetMayHaveChanged };
151
+ }
152
+ const batch = getBatchCommandSteps(options.args, options.stdin);
153
+ if (batch.error)
154
+ return { pageTargetMayHaveChanged: true, pageUrlUnknown: true };
155
+ for (let index = 0; index < batch.steps.length; index += 1) {
156
+ const step = batch.steps[index];
157
+ const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
158
+ const stepDescriptor = parseArgvDescriptor(step);
159
+ pageTargetMayHaveChanged ||= getExplicitNavigationTarget(step) !== undefined
160
+ || (isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand)
161
+ && !(trustedBatchTabSelection && stepDescriptor.commandInfo.command === "tab"));
162
+ state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection });
163
+ }
164
+ return { ...state, pageTargetMayHaveChanged };
165
+ }
166
+ function isRecoveringPageTransitionCommand(command, subcommand) {
167
+ return command !== "eval"
168
+ && !(command === "webmcp" && subcommand === "invoke")
169
+ && isUnverifiedPageTransitionCommand(command, subcommand);
170
+ }
171
+ function getUnverifiedPageError(options) {
172
+ if (!options.pageUrlUnknown)
173
+ return undefined;
174
+ const { command, subcommand } = parseArgvDescriptor(options.args).commandInfo;
175
+ const closesPage = ["close", "exit", "quit"].includes(command ?? "") || (command === "tab" && subcommand === "close");
176
+ const inspectsTarget = (command === "tab" && subcommand === "list") || (command === "get" && subcommand === "url");
177
+ const selectsTab = command === "tab" && subcommand !== undefined && !["close", "list", "new"].includes(subcommand);
178
+ const settlesPendingWebMcp = command === "webmcp" && ["result", "cancel"].includes(subcommand ?? "");
179
+ const handlesBlockingDialog = command === "dialog" && ["status", "accept", "dismiss"].includes(subcommand ?? "");
180
+ const transitionsPage = options.allowUnverifiedPageTransitions === true && isRecoveringPageTransitionCommand(command, subcommand);
181
+ const navigatesExplicitly = getExplicitNavigationTarget(options.args) !== undefined;
182
+ return closesPage || handlesBlockingDialog || inspectsTarget || selectsTab || settlesPendingWebMcp || transitionsPage || navigatesExplicitly || (options.trustedBatchTabSelection && command === "tab")
183
+ ? undefined
184
+ : UNVERIFIED_PAGE_MESSAGE;
185
+ }
186
+ export function getPageTargetValidationError(options) {
187
+ const descriptor = parseArgvDescriptor(options.args);
188
+ const command = descriptor.commandInfo.command;
189
+ if (["close", "exit", "quit"].includes(command ?? ""))
190
+ return undefined;
191
+ if (command === "batch") {
192
+ const batch = getBatchCommandSteps(options.args, options.stdin);
193
+ if (batch.error)
194
+ return batch.error.startsWith("agent_browser batch stdin") || batch.error === NESTED_BATCH_ARGUMENT_MESSAGE
195
+ ? batch.error
196
+ : UNSAFE_BATCH_ARGUMENT_MESSAGE;
197
+ const bailOnFirstError = batchBailsOnFirstError(options.args);
198
+ let possibleStates = [{
199
+ currentPageUrl: options.currentPageUrl,
200
+ pageUrlUnknown: options.pageUrlUnknown ?? false,
201
+ retainedAfterFailedNavigation: false,
202
+ }];
203
+ for (let index = 0; index < batch.steps.length; index += 1) {
204
+ const step = batch.steps[index];
205
+ const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
206
+ let directError;
207
+ let failedNavigationHazard = false;
208
+ for (const state of possibleStates) {
209
+ const error = getUnverifiedPageError({
210
+ allowUnverifiedPageTransitions: options.allowUnverifiedPageTransitions,
211
+ args: step,
212
+ pageUrlUnknown: state.pageUrlUnknown,
213
+ trustedBatchTabSelection,
214
+ });
215
+ if (!error)
216
+ continue;
217
+ if (state.retainedAfterFailedNavigation)
218
+ failedNavigationHazard = true;
219
+ else
220
+ directError ??= error;
221
+ }
222
+ if (directError)
223
+ return BATCH_UNVERIFIED_PAGE_MESSAGE;
224
+ if (failedNavigationHazard)
225
+ return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
226
+ const mayChangePageTarget = commandMayChangePageTarget(step, trustedBatchTabSelection);
227
+ const verifiesPageTarget = commandVerifiesPageTarget(step);
228
+ const nextStates = [];
229
+ for (const state of possibleStates) {
230
+ const successState = getResultingPageState({
231
+ args: step,
232
+ currentPageUrl: state.currentPageUrl,
233
+ pageUrlUnknown: state.pageUrlUnknown,
234
+ trustedBatchTabSelection,
235
+ });
236
+ if (nextStates.length >= MAX_NON_BAIL_BATCH_PAGE_STATES)
237
+ return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
238
+ nextStates.push({ ...successState, retainedAfterFailedNavigation: mayChangePageTarget || verifiesPageTarget ? false : state.retainedAfterFailedNavigation });
239
+ if (!bailOnFirstError && (mayChangePageTarget || (verifiesPageTarget && state.pageUrlUnknown))) {
240
+ nextStates.push({ ...state, retainedAfterFailedNavigation: true });
241
+ }
242
+ }
243
+ possibleStates = deduplicatePossibleBatchPageStates(nextStates);
244
+ }
245
+ return undefined;
246
+ }
247
+ return getUnverifiedPageError({
248
+ allowUnverifiedPageTransitions: options.allowUnverifiedPageTransitions,
249
+ args: options.args,
250
+ pageUrlUnknown: options.pageUrlUnknown,
251
+ trustedBatchTabSelection: options.trustedFirstBatchTabSelection,
252
+ });
253
+ }
254
+ export function getExplicitSessionPageVerificationRequirement(options) {
255
+ const descriptor = parseArgvDescriptor(options.args);
256
+ if (!needsManagedSession(descriptor))
257
+ return undefined;
258
+ if (isRecoveringPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand))
259
+ return undefined;
260
+ const validationError = getPageTargetValidationError({
261
+ args: options.args,
262
+ pageUrlUnknown: true,
263
+ stdin: options.stdin,
264
+ allowUnverifiedPageTransitions: true,
265
+ trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
266
+ });
267
+ return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
268
+ ? UNVERIFIED_PAGE_MESSAGE
269
+ : undefined;
270
+ }
@@ -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
  }