pi-agent-browser-native 0.9.2 → 0.9.3

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 (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +18 -6
  3. package/dist/extensions/agent-browser/index.js +39 -16
  4. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
  5. package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
  6. package/dist/extensions/agent-browser/lib/native-output.js +2 -1
  7. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
  8. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +58 -7
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
  14. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
  15. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +26 -8
  16. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
  17. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
  18. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
  19. package/dist/extensions/agent-browser/lib/playbook.js +2 -2
  20. package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
  21. package/dist/extensions/agent-browser/lib/process.js +6 -6
  22. package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
  23. package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
  24. package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
  25. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +3 -0
  26. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
  27. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
  28. package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
  29. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
  30. package/dist/extensions/agent-browser/lib/runtime.js +5 -1
  31. package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
  32. package/docs/ARCHITECTURE.md +5 -5
  33. package/docs/COMMAND_REFERENCE.md +13 -9
  34. package/docs/RELEASE.md +6 -5
  35. package/docs/REQUIREMENTS.md +1 -0
  36. package/docs/SESSION_STORAGE.md +1 -1
  37. package/docs/SUPPORT_MATRIX.md +9 -3
  38. package/docs/TOOL_CONTRACT.md +20 -14
  39. package/package.json +4 -4
@@ -6,6 +6,7 @@ import { getPageTargetValidationError } from "../../page-target-validation.js";
6
6
  import { isRecord } from "../../parsing.js";
7
7
  import { getBrowserResultMessage } from "../../browser-transcript.js";
8
8
  import { withAttachedBrowserSessionContext } from "../../process.js";
9
+ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../../process-environment.js";
9
10
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
10
11
  import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
11
12
  import { appendUniqueAgentBrowserNextActions } from "../../results/next-actions.js";
@@ -397,37 +398,39 @@ function getElectronProbeSummary(probe) {
397
398
  class ElectronManagedSessionPolicyError extends Error {
398
399
  }
399
400
  async function withOwnedElectronManagedSessionPolicy(options, run) {
400
- const autosavePolicyChangeError = getRunningHeadedAutosavePolicyChangeError(options.headedManagedAutosaveInterval);
401
- if (autosavePolicyChangeError)
402
- throw new ElectronManagedSessionPolicyError(autosavePolicyChangeError);
403
- const context = buildOwnedManagedSessionRestoreContext({
404
- args: ["--namespace", options.namespace ?? "", "--session", options.sessionName, ...options.args],
405
- cwd: options.cwd,
406
- headedManagedAutosaveDisabled: options.headedManagedAutosaveDisabled,
407
- headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
408
- managedSessionName: options.sessionName,
409
- namespace: options.namespace,
410
- restoreState: options.restoreState,
401
+ return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_CONFIRM_ACTIONS: options.confirmActions }, async () => {
402
+ const autosavePolicyChangeError = getRunningHeadedAutosavePolicyChangeError(options.headedManagedAutosaveInterval);
403
+ if (autosavePolicyChangeError)
404
+ throw new ElectronManagedSessionPolicyError(autosavePolicyChangeError);
405
+ const context = buildOwnedManagedSessionRestoreContext({
406
+ args: ["--namespace", options.namespace ?? "", "--session", options.sessionName, ...options.args],
407
+ cwd: options.cwd,
408
+ headedManagedAutosaveDisabled: options.headedManagedAutosaveDisabled,
409
+ headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
410
+ managedSessionName: options.sessionName,
411
+ namespace: options.namespace,
412
+ restoreState: options.restoreState,
413
+ });
414
+ if (!context)
415
+ throw new ElectronManagedSessionPolicyError("Electron helper could not establish wrapper ownership for its managed session.");
416
+ let policy;
417
+ try {
418
+ policy = await acquireOwnedManagedSessionDaemonPolicy({ context, electronLaunchRecord: options.electronLaunchRecord, electronVerificationTimeoutMs: options.timeoutMs, signal: options.signal });
419
+ }
420
+ catch (error) {
421
+ throw new ElectronManagedSessionPolicyError(error instanceof Error ? error.message : String(error), { cause: error });
422
+ }
423
+ try {
424
+ if (policy.error)
425
+ throw new ElectronManagedSessionPolicyError(policy.error);
426
+ if (!policy.lock)
427
+ throw new ElectronManagedSessionPolicyError(options.signal?.aborted ? "Electron helper was aborted." : "Electron helper could not acquire managed-session policy coordination.");
428
+ return await withOwnedManagedSessionContext(context, run);
429
+ }
430
+ finally {
431
+ await policy.lock?.release();
432
+ }
411
433
  });
412
- if (!context)
413
- throw new ElectronManagedSessionPolicyError("Electron helper could not establish wrapper ownership for its managed session.");
414
- let policy;
415
- try {
416
- policy = await acquireOwnedManagedSessionDaemonPolicy({ context, electronLaunchRecord: options.electronLaunchRecord, electronVerificationTimeoutMs: options.timeoutMs, signal: options.signal });
417
- }
418
- catch (error) {
419
- throw new ElectronManagedSessionPolicyError(error instanceof Error ? error.message : String(error), { cause: error });
420
- }
421
- try {
422
- if (policy.error)
423
- throw new ElectronManagedSessionPolicyError(policy.error);
424
- if (!policy.lock)
425
- throw new ElectronManagedSessionPolicyError(options.signal?.aborted ? "Electron helper was aborted." : "Electron helper could not acquire managed-session policy coordination.");
426
- return await withOwnedManagedSessionContext(context, run);
427
- }
428
- finally {
429
- await policy.lock?.release();
430
- }
431
434
  }
432
435
  async function collectOwnedElectronManagedSessionTarget(options) {
433
436
  try {
@@ -603,7 +606,7 @@ export async function cleanupTrackedElectronHostLaunches(options) {
603
606
  const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
604
607
  const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
605
608
  const managedSessionCloseError = record.sessionName
606
- ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, socketDir: managedSessionOwner?.socketDir, timeoutMs: options.timeoutMs })
609
+ ? await closeManagedSession({ confirmActions: options.sessionPageState.get(sessionKey ?? record.sessionName).confirmActions ?? process.env.AGENT_BROWSER_CONFIRM_ACTIONS, cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, socketDir: managedSessionOwner?.socketDir, timeoutMs: options.timeoutMs })
607
610
  : undefined;
608
611
  const managedSessionStep = record.sessionName
609
612
  ? managedSessionCloseError
@@ -673,6 +676,7 @@ async function handleElectronHostInputInContext(options) {
673
676
  .map((record) => {
674
677
  const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
675
678
  return collectOwnedElectronManagedSessionTarget({
679
+ confirmActions: sessionPageState.get(sessionKey).confirmActions ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS,
676
680
  cwd,
677
681
  electronLaunchRecord: record,
678
682
  headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
@@ -749,6 +753,7 @@ async function handleElectronHostInputInContext(options) {
749
753
  const headedManagedAutosaveInterval = managedSessionOwner?.headedManagedAutosaveInterval;
750
754
  const probe = await withOwnedElectronManagedSessionPolicy({
751
755
  args: ["snapshot", "-i"],
756
+ confirmActions: currentPageState.confirmActions ?? getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIRM_ACTIONS,
752
757
  cwd,
753
758
  electronLaunchRecord: launchRecord,
754
759
  headedManagedAutosaveDisabled,
@@ -821,7 +826,7 @@ async function handleElectronHostInputInContext(options) {
821
826
  const selection = selectElectronRecords(compiledElectron, electronLaunchRecords);
822
827
  if (selection.error)
823
828
  return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: selection.error, failureCategory: "validation-error" });
824
- const cleanupResults = await cleanupTrackedElectronHostLaunches({ attachedSessionKeys, cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, ownedManagedSessions, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
829
+ const cleanupResults = await cleanupTrackedElectronHostLaunches({ attachedSessionKeys, cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, ownedManagedSessions, sessionPageState, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
825
830
  return buildElectronCleanupResult(redactedCompiledElectron ?? compiledElectron, cleanupResults);
826
831
  }
827
832
  return undefined;
@@ -48,7 +48,7 @@ async function readNativeIdentity(path, cwd, signal, rootFallback, browserComman
48
48
  return {};
49
49
  const rootLaunchConfig = browserCommand && (hasLocalLaunchDefaults(config, {}) || ["cdp", "autoConnect", "provider"].some((key) => config[key] !== undefined))
50
50
  || rootFallback && ["restore", "sessionName", "state", "allowedDomains", "profile", "executablePath"].some((key) => config[key] !== undefined);
51
- if (!rootLaunchConfig && typeof config.session !== "string" && typeof config.namespace !== "string")
51
+ if (!rootLaunchConfig && config.confirmActions === undefined && typeof config.session !== "string" && typeof config.namespace !== "string")
52
52
  return {};
53
53
  const result = await runAgentBrowserProcess({ args: ["--config", path, "--json", "session"], cwd, signal, timeoutMs: 5_000 });
54
54
  try {
@@ -73,9 +73,7 @@ async function readNativeIdentity(path, cwd, signal, rootFallback, browserComman
73
73
  }
74
74
  }
75
75
  export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
76
- if (input.kind === "electron" && input.compiledElectron.action === "launch")
77
- return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
78
- if (input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
76
+ if (isPlainTextInspectionArgs(input.toolArgs))
79
77
  return run(input);
80
78
  const env = getAgentBrowserProcessEnvironment();
81
79
  const configArg = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--config")[0];
@@ -85,12 +83,19 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
85
83
  : [join(homedir(), ".agent-browser", "config.json"), join(cwd, "agent-browser.json")];
86
84
  const rootName = root && rootBrowserSessionName(root.id);
87
85
  const explicitSession = extractExplicitSessionName(input.toolArgs);
86
+ const browserCommand = input.kind !== "electron" && needsManagedSession(parseArgvDescriptor(input.toolArgs), input.toolStdin);
88
87
  const rootFallback = root !== undefined && env.AGENT_BROWSER_SESSION === undefined && (explicitSession === undefined || explicitSession === rootName)
89
- && needsManagedSession(parseArgvDescriptor(input.toolArgs), input.toolStdin);
88
+ && browserCommand;
90
89
  let identity = {};
91
- const browserCommand = needsManagedSession(parseArgvDescriptor(input.toolArgs), input.toolStdin);
92
90
  for (const path of paths)
93
91
  identity = { ...identity, ...await readNativeIdentity(path, cwd, signal, rootFallback, browserCommand) };
92
+ if (input.kind === "electron") {
93
+ const nativeConfirmActions = env.AGENT_BROWSER_CONFIRM_ACTIONS ?? (typeof identity.confirmActions === "string" ? identity.confirmActions : undefined);
94
+ return withAgentBrowserProcessEnvironment({
95
+ ...(input.compiledElectron.action === "launch" ? { AGENT_BROWSER_SESSION: undefined } : {}),
96
+ ...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
97
+ }, () => run({ ...input, nativeConfirmActions }));
98
+ }
94
99
  // Native ORs environment/config booleans; only CLI false overrides configured auto-connect.
95
100
  const autoConnect = getBooleanFlagValue(input.toolArgs, "--auto-connect")
96
101
  ?? (isUpstreamEnvFlagEnabled(env.AGENT_BROWSER_AUTO_CONNECT) || identity.autoConnect === true);
@@ -123,12 +128,22 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
123
128
  if (session !== undefined && extractExplicitSessionName(args) === undefined)
124
129
  args = ["--session", session, ...args];
125
130
  const idleTimeout = scanUpstreamGlobalFlagOccurrences(args, "--idle-timeout").at(-1)?.value;
131
+ const actionPolicy = scanUpstreamGlobalFlagOccurrences(args, "--action-policy").at(-1)?.value;
132
+ const confirmActions = scanUpstreamGlobalFlagOccurrences(args, "--confirm-actions").at(-1)?.value;
133
+ const nativeConfirmActions = confirmActions ?? env.AGENT_BROWSER_CONFIRM_ACTIONS
134
+ ?? (typeof identity.confirmActions === "string" ? identity.confirmActions : undefined);
135
+ const debug = getBooleanFlagValue(args, "--debug");
136
+ const noAutoDialog = getBooleanFlagValue(args, "--no-auto-dialog");
126
137
  return withAgentBrowserProcessEnvironment({
127
138
  ...(idleTimeout !== undefined ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: idleTimeout } : {}),
139
+ ...(actionPolicy !== undefined ? { AGENT_BROWSER_ACTION_POLICY: actionPolicy } : {}),
140
+ ...(confirmActions !== undefined ? { AGENT_BROWSER_CONFIRM_ACTIONS: confirmActions } : {}),
141
+ ...(debug !== undefined ? { AGENT_BROWSER_DEBUG: debug ? "1" : undefined } : {}),
142
+ ...(noAutoDialog !== undefined ? { AGENT_BROWSER_NO_AUTO_DIALOG: noAutoDialog ? "1" : "0" } : {}),
128
143
  ...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
129
144
  ...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
130
145
  ...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
131
- }, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun, launchSignal = signal) => {
146
+ }, () => run({ ...input, toolArgs: args, nativeConfirmActions, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun, launchSignal = signal) => {
132
147
  const daemon = await inspectManagedSessionDaemon({ cwd, signal: launchSignal, sessionName: rootDefault,
133
148
  namespace: scanUpstreamGlobalFlagOccurrences(args, "--namespace").at(-1)?.value ?? namespace, timeoutMs: 5_000 });
134
149
  if (daemon.status === "missing-binary")
@@ -156,5 +171,8 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
156
171
  ...(profile !== undefined ? { AGENT_BROWSER_PROFILE: profile } : {}),
157
172
  ...(executablePath !== undefined ? { AGENT_BROWSER_EXECUTABLE_PATH: executablePath } : {}),
158
173
  }, () => withChromeStartupArgs(bootstrap || configuredChromeLaunch ? chromeStartupArgs : undefined, () => browserRun(bootstrap)));
159
- }));
174
+ }), [
175
+ ...(debug !== undefined ? [["--debug", String(debug)]] : []),
176
+ ...(noAutoDialog !== undefined ? [["--no-auto-dialog", String(noAutoDialog)]] : []),
177
+ ]); // Native ORs env/config booleans; helpers need CLI false to override true config.
160
178
  }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, extname, isAbsolute, resolve } from "node:path";
3
3
  import { isRecord } from "../parsing.js";
4
+ import { getBooleanFlagValue } from "../argv-grammar.js";
4
5
  import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
5
6
  import { parseCommandInfo, redactSensitiveValue } from "../runtime.js";
6
7
  export function normalizeRequestedOutputPath(path) {
@@ -80,8 +81,9 @@ function serializeOutputPayload(value) {
80
81
  function appendOutputFileNotice(result, message, failed = false) {
81
82
  const content = [...(result.content ?? [])];
82
83
  if (content[0]?.type === "text") {
84
+ const nativeText = isRecord(result.details) && Array.isArray(result.details.args) && getBooleanFlagValue(result.details.args, "--json") === false;
83
85
  try {
84
- const json = JSON.parse(content[0].text);
86
+ const json = nativeText ? undefined : JSON.parse(content[0].text);
85
87
  if (isRecord(json) && typeof json.success === "boolean") {
86
88
  content[0] = { type: "text", text: JSON.stringify({ ...json, ...(failed ? { success: false, error: message } : {}), outputFileNotice: message }, null, 2) };
87
89
  return content;
@@ -2,6 +2,8 @@ import { parseArgvDescriptor } from "./argv-descriptor.js";
2
2
  import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, } from "./argv-grammar.js";
3
3
  import { isBrowserIndependentRead, needsManagedSession } from "./command-policy.js";
4
4
  import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
5
+ import { isRecord } from "./parsing.js";
6
+ import { detectConfirmationRequired } from "./results/confirmation.js";
5
7
  import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
6
8
  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
9
  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.`;
@@ -31,7 +33,7 @@ function getPositionalOperands(commandTokens) {
31
33
  }
32
34
  return values;
33
35
  }
34
- function getExplicitNavigationTarget(args) {
36
+ export function getExplicitNavigationTarget(args) {
35
37
  const descriptor = parseArgvDescriptor(args);
36
38
  const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
37
39
  if (EXPLICIT_NAVIGATION_COMMANDS.has(descriptor.commandInfo.command ?? ""))
@@ -85,6 +87,8 @@ function getBatchCommandSteps(args, stdin) {
85
87
  if (parsedStdin.error)
86
88
  return { error: parsedStdin.error, steps: [] };
87
89
  for (const step of parsedStdin.steps ?? []) {
90
+ if (step.length === 0)
91
+ continue;
88
92
  if (parseArgvDescriptor(step).commandInfo.command === "batch")
89
93
  return { error: NESTED_BATCH_ARGUMENT_MESSAGE, steps: [] };
90
94
  steps.push(step);
@@ -153,7 +157,10 @@ export function getResultingPageTargetState(options) {
153
157
  || isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand);
154
158
  return { ...getResultingPageState({ ...state, args: options.args, trustedBatchTabSelection: false }), pageTargetMayHaveChanged };
155
159
  }
156
- for (const step of options.executedBatchSteps) {
160
+ for (const [index, step] of options.executedBatchSteps.entries()) {
161
+ const row = Array.isArray(options.batchResults) ? options.batchResults[index] : undefined;
162
+ if (isRecord(row) && detectConfirmationRequired(row.result))
163
+ continue;
157
164
  const stepDescriptor = parseArgvDescriptor(step);
158
165
  pageTargetMayHaveChanged ||= getExplicitNavigationTarget(step) !== undefined
159
166
  || isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand);
@@ -211,7 +218,7 @@ export function getPageTargetValidationError(options) {
211
218
  let directError;
212
219
  let failedNavigationHazard = false;
213
220
  for (const state of possibleStates) {
214
- const error = getUnverifiedPageError({
221
+ const error = options.allowFirstBatchConfirmation === true && index === 0 ? undefined : getUnverifiedPageError({
215
222
  allowUnverifiedPageTransitions: options.allowUnverifiedPageTransitions,
216
223
  args: step,
217
224
  pageUrlUnknown: state.pageUrlUnknown,
@@ -5,6 +5,7 @@ import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup
5
5
  import { compileAgentBrowserSemanticAction } from "./input-modes/semantic-action.js";
6
6
  import { isRecord } from "./parsing.js";
7
7
  import { redactInvocationArgs } from "./runtime.js";
8
+ import { isBooleanFlagEnabled } from "./argv-grammar.js";
8
9
  const TUI_INVOCATION_PREVIEW_MAX_CHARS = 160;
9
10
  const TUI_COLLAPSED_OUTPUT_MAX_LINES = 12;
10
11
  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;
@@ -188,7 +189,7 @@ function agentBrowserToolResultRequestedJson(result, input) {
188
189
  const details = isRecord(result.details) ? result.details : undefined;
189
190
  const detailArgs = Array.isArray(details?.args) ? details.args : undefined;
190
191
  const inputArgs = isRecord(input) && Array.isArray(input.args) ? input.args : undefined;
191
- return detailArgs?.includes("--json") === true || inputArgs?.includes("--json") === true;
192
+ return isBooleanFlagEnabled(detailArgs ?? [], "--json") || isBooleanFlagEnabled(inputArgs ?? [], "--json");
192
193
  }
193
194
  function agentBrowserToolResultHasParseableJsonContent(content) {
194
195
  return content.some((item) => {
@@ -73,7 +73,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
73
73
  "For non-core families, pass current upstream commands through the native tool directly: network requests, network route <url>, network har start/stop [path], diff snapshot, diff screenshot --baseline <file>, diff url <u1> <u2>, trace start, trace stop [path], profiler start, profiler stop [path], record start <path>, record stop, console/errors [--clear], highlight <selector>, inspect, clipboard read, clipboard write <text>, clipboard copy/paste, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat <message>. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request agent_browser_network_source, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
74
74
  "For Electron desktop apps, enable agent_browser_electron through agent_browser_tools for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use agent_browser_electron probe or snapshot -i/agent_browser_qa attached:true for current-session state, and always cleanup the returned launchId when done. agent_browser_electron launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, run get url to verify the active target before page-content reads, inspect tab list, select the stable tab id such as tab t2, verify it again with get url, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
75
75
  "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox|derive-client through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Use derive-client when recording HAR traffic to generate a standalone API client; prefer network har start (text bodies by default) or network har start --content all|none before multi-step capture. For accessibility audits use a11y or a11y --tags wcag2a,wcag2aa (CDP browsers only). Hosted sandbox workflows should use upstream @agent-browser/sandbox helpers outside this wrapper. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
76
- "For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; dialog commands and eval snippets that look like alert/confirm/prompt/dialog triggers are shorter-bounded than normal browser calls, and timed-out dialog-like interactions may add inspect-dialog-after-timeout, dismiss-dialog-after-timeout, or recover-fresh-session-after-dialog-timeout nextActions. When --confirm-actions produces a pending confirmation, use details.nextActions or exact confirm <id> / deny <id> calls instead of inventing ids.",
76
+ "For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; dialog commands and eval snippets that look like alert/confirm/prompt/dialog triggers are shorter-bounded than normal browser calls, and timed-out dialog-like interactions may add inspect-dialog-after-timeout, dismiss-dialog-after-timeout, or recover-fresh-session-after-dialog-timeout nextActions. Set --confirm-actions on the session's first command; selected strings, including an explicit empty string, follow same-identity helpers and resumed follow-ups until successful close. Native unset keeps ordinary native defaults. Changing a running daemon's policy can discard pending confirmations. When it produces a pending confirmation, including a cold-reopen helper, follow the exact details.nextActions or confirm <id> / deny <id> calls without repeating the policy or inventing ids. Matching native guarded controls avoid page helpers that could replace the pending action; this is not native ID-validation proof or browser independence. In native batch, only a matching first effective decision gets that helper suppression; later page/ref checks remain. Nested decisions remain pending; failed inner actions fail, and completed browser actions reconcile their page state. A pending tab_new transport may show a validated after_confirmation open request and conditional guidance: follow it in the same session and new tab only after successful tab approval, never after denial. The extension does not execute that continuation. Confirmed full snapshots install returned refs; a final confirmed capture supplies the next same-identity call's freshness sample once, including semantic role click/check/fill and unique accessible select resolution, with target/membership/invalidation checks intact. On this one use, live verification compares the complete capture URL, including fragments, and rejects refs when it changes—including wrapper-owned managed sessions and warm sessions without resume. Ordinary ref/tab URL comparisons remain fragment-insensitive. Pending snapshots display confirmation required, not an empty page. Intervening calls or later rows consume it; DOM-only renames on the same page after that sample are not resampled on this one use.",
77
77
  "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For headed demos, put --headed on the first launch with sessionMode=fresh and verify with screenshot/tab/get-url evidence because tool success cannot prove the OS window is visible to the user. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or agent_browser_qa attached:true; for disappearance checks, use wait --fn predicates instead of stale upstream-help examples like wait <selector> --state hidden. Use agent_browser_electron probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort: their duration is positional (wait <ms>, not wait --time <ms>). Use explicit --timeout or top-level timeoutMs for legitimately slow waits, and treat a successful payload like \"waited\":\"timeout\" as elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
78
78
  "For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
79
79
  "For read-only browsing tasks, use read <url> for documentation or other unstructured text without requiring a Chrome page, or read with no URL for rendered active-tab DOM. Prefer the current snapshot, structured ref labels, getters, or scoped eval --stdin when you need interactive structure or targeted page state. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
@@ -81,7 +81,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
81
81
  "On dashboards with nested scroll containers, verify scroll with a screenshot or fresh snapshot -i; if the viewport did not move, details.data.scrolled may be false/noMovement true and you should prefer scrollintoview <@ref> or target the actual scrollable region with scroll <selector> <dir> [px|percent]. For native selects, use select <selector> <value...> (or agent_browser_action select) instead of clicking option refs; for custom comboboxes, a click/agent_browser_action may only focus the field, so re-snapshot and use keyboard type <text> for focused input, press ArrowDown or press Enter, or visible option refs. Raw type requires both <selector> and <text>.",
82
82
  "When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
83
83
  "When using eval --stdin for extraction, pass the JavaScript through the native tool stdin field, not as an extra args token after --stdin, and return the value you want instead of relying on console.log as the primary result channel. Prefer plain expressions like ({ title: document.title }) or explicitly invoked functions like (() => ({ title: document.title }))(); use outputPath when the eval/get/snapshot data should be saved as a durable local file, but never reuse a screenshot, download, recording, or other browser artifact destination as outputPath. If a function-shaped snippet returns {}, details.evalStdinHint may warn that the function was serialized instead of called. Local file pages and caller-selected output paths are supported when upstream allows them. If get text on a broad CSS selector surfaces details.selectorTextVisibility or selectorTextVisibilityAll, prefer a visible @ref, a more specific selector, or the inspect-visible-text-candidates nextAction over hidden tab content.",
84
- "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. Click-dispatch probes require native get attr readback of a temporary marker on the candidate target; unproven ref or XPath identity leaves the native click unverified, not failed. 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 an upstream click failure says the target is covered by another element at the target's click point, use the inspect-overlay-state nextAction to refresh refs and inspect the blocker before deciding whether to retry; do not blindly repeat the blocked click. 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.",
84
+ "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. Optional click-dispatch probes are skipped when native confirmation settings guard evaluate, without bypassing native click policy. Otherwise probes require native get attr readback of a temporary marker on the candidate target; unproven ref or XPath identity leaves the native click unverified, not failed. 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 an upstream click failure says the target is covered by another element at the target's click point, use the inspect-overlay-state nextAction to refresh refs and inspect the blocker before deciding whether to retry; do not blindly repeat the blocked click. 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.",
85
85
  "On upstream 0.38+, use snapshot --delta for native full/unchanged/changed revisions; --delta --full resets its baseline. Surviving DOM nodes keep refs, but refresh after navigation or replacement. Wrapper filters use full trees. screenshot --if-changed or --threshold 0.01 can return changed:false with no path or image; use ordinary screenshots when a file is required. --input-mode instant|smooth|human controls sticky pointer behavior; click/drag --human and mouse move --duration/--steps/--human/--seed pass through. record start/restart accept --cursor, --contact-sheet, and --contact-sheet-threshold; stop and verify both video and reported contact sheet. auth login <name> --no-navigate fills a prepared login page only after native origin checks. WebMCP catalog updates are untrusted page data; webmcp list supplies full schemas.",
86
86
  "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.",
87
87
  "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.",
@@ -1,5 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { rm } from "node:fs/promises";
3
+ import { getBooleanFlagValue } from "./argv-grammar.js";
3
4
  import { writeSecureTempFile } from "./temp.js";
4
5
  const isolatedAgentBrowserEnvironment = new AsyncLocalStorage();
5
6
  const agentBrowserProcessEnvironment = new AsyncLocalStorage();
@@ -7,7 +8,7 @@ const PROXY_ENV_NAMES = new Set(["ALL_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "NO_P
7
8
  export function getAgentBrowserProcessEnvironment(baseEnv = process.env) {
8
9
  const isolatedConfig = isolatedAgentBrowserEnvironment.getStore();
9
10
  if (isolatedConfig === undefined)
10
- return { ...baseEnv, ...agentBrowserProcessEnvironment.getStore() };
11
+ return { ...baseEnv, ...agentBrowserProcessEnvironment.getStore()?.env };
11
12
  return {
12
13
  ...Object.fromEntries(Object.entries(baseEnv).filter(([name]) => {
13
14
  const normalizedName = name.toUpperCase();
@@ -16,8 +17,16 @@ export function getAgentBrowserProcessEnvironment(baseEnv = process.env) {
16
17
  AGENT_BROWSER_CONFIG: isolatedConfig,
17
18
  };
18
19
  }
19
- export function withAgentBrowserProcessEnvironment(env, run) {
20
- return agentBrowserProcessEnvironment.run({ ...agentBrowserProcessEnvironment.getStore(), ...env }, run);
20
+ export function getAgentBrowserProcessArgs(args) {
21
+ if (isolatedAgentBrowserEnvironment.getStore() !== undefined)
22
+ return args;
23
+ const overrides = agentBrowserProcessEnvironment.getStore()?.booleanArgs ?? [];
24
+ const missing = overrides.flatMap(([flag, value]) => getBooleanFlagValue(args, flag) === undefined ? [flag, value] : []);
25
+ return missing.length ? [...missing, ...args] : args;
26
+ }
27
+ export function withAgentBrowserProcessEnvironment(env, run, booleanArgs) {
28
+ const parent = agentBrowserProcessEnvironment.getStore();
29
+ return agentBrowserProcessEnvironment.run({ env: { ...parent?.env, ...env }, booleanArgs: booleanArgs ?? parent?.booleanArgs ?? [] }, run);
21
30
  }
22
31
  export async function withIsolatedAgentBrowserEnvironment(run) {
23
32
  if (isolatedAgentBrowserEnvironment.getStore() !== undefined)
@@ -9,7 +9,7 @@ import { extractExplicitSessionName, resolveAgentBrowserNamespace, scanUpstreamG
9
9
  import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
10
10
  import { getPageTargetValidationError, } from "./page-target-validation.js";
11
11
  import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
12
- import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
12
+ import { getAgentBrowserProcessArgs, getAgentBrowserProcessEnvironment } from "./process-environment.js";
13
13
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
14
14
  import { resolveWindowsStockLauncher } from "./windows-stock-launcher.js";
15
15
  const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
@@ -328,11 +328,11 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
328
328
  clampUpstreamDefaultTimeout(childEnv);
329
329
  return childEnv;
330
330
  }
331
- function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, browserIndependentReadConfirmation = false) {
331
+ function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, nativeConfirmationDecision = false) {
332
332
  if (!validateManagedSessionRestoreContextForSpawn(options)) {
333
333
  return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
334
334
  }
335
- if (browserIndependentReadConfirmation)
335
+ if (nativeConfirmationDecision)
336
336
  return undefined;
337
337
  return getPageTargetValidationError({
338
338
  args: options.args,
@@ -361,7 +361,7 @@ export async function runAgentBrowserProcess(options) {
361
361
  restoreState: managedSessionRestoreState,
362
362
  stdin,
363
363
  };
364
- const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
364
+ const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.nativeConfirmationDecision);
365
365
  if (planningPolicyError) {
366
366
  return {
367
367
  aborted: false,
@@ -509,13 +509,13 @@ export async function runAgentBrowserProcess(options) {
509
509
  });
510
510
  });
511
511
  };
512
- const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
512
+ const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.nativeConfirmationDecision);
513
513
  if (spawnPolicyError) {
514
514
  resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
515
515
  return;
516
516
  }
517
517
  const spawnBrowser = processPlatform === "win32" && !stockLauncher ? crossSpawn : spawn;
518
- const child = spawnBrowser(stockLauncher ?? "agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
518
+ const child = spawnBrowser(stockLauncher ?? "agent-browser", prepareAgentBrowserSpawnArgs(getAgentBrowserProcessArgs(args), ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
519
519
  cwd,
520
520
  env: childEnv,
521
521
  stdio: ["pipe", "pipe", "pipe"],
@@ -2,21 +2,40 @@ import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { extractExplicitSessionName, getAgentBrowserSessionIdentityKey, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./argv-grammar.js";
3
3
  import { getExplicitReadUrl } from "./command-policy.js";
4
4
  import { isCloseCommand } from "./command-taxonomy.js";
5
+ import { getExplicitNavigationTarget } from "./page-target-validation.js";
5
6
  import { isRecord } from "./parsing.js";
7
+ import { getUpstreamEffectiveBatchSteps } from "./orchestration/batch-stdin.js";
6
8
  export function parseReadConfirmation(value) {
7
- if (!isRecord(value) || value.source !== "native-explicit-url-read" || (value.state !== "pending" && value.state !== "cleared"))
9
+ if (!isRecord(value) || (value.source !== "native-explicit-url-read" && value.source !== "native-guarded-action") || (value.state !== "pending" && value.state !== "cleared"))
8
10
  return undefined;
9
11
  if (typeof value.id !== "string" || !value.id || typeof value.sessionName !== "string" || !value.sessionName || (value.namespace !== undefined && typeof value.namespace !== "string"))
10
12
  return undefined;
11
- return { ...(isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: "native-explicit-url-read", state: value.state };
13
+ if (value.source === "native-guarded-action" && (typeof value.command !== "string" || !value.command || typeof value.action !== "string" || !value.action))
14
+ return undefined;
15
+ return { ...(value.source === "native-explicit-url-read" && isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: value.source, state: value.state,
16
+ ...(value.source === "native-guarded-action" ? { command: typeof value.command === "string" ? value.command : undefined, action: typeof value.action === "string" ? value.action : undefined } : {}),
17
+ ...(value.source === "native-guarded-action" && value.state === "cleared" && value.command === "snapshot" && value.action === "snapshot" && value.refSnapshotFresh === true ? { refSnapshotFresh: true } : {}) };
18
+ }
19
+ export function isBrowserIndependentConfirmation(value) {
20
+ return value?.source === "native-explicit-url-read" && value.capabilities?.readRequiresConfirmation === true;
21
+ }
22
+ export function suppressConfirmationPageHelpers(value) {
23
+ return value?.state === "pending" && (value.source === "native-guarded-action" || isBrowserIndependentConfirmation(value));
12
24
  }
13
- export function findReadConfirmation(args, confirmations, namespace) {
14
- const tokens = extractUpstreamCommandTokens(args);
25
+ export function isSuccessfulNativeConfirmedClose(commandTokens, data) {
26
+ return commandTokens[0] === "confirm" && commandTokens.length === 2 && isRecord(data) && data.confirmed === true
27
+ && data.action === "close" && isRecord(data.result) && data.result.success === true
28
+ && isRecord(data.result.data) && data.result.data.closed === true;
29
+ }
30
+ export function findReadConfirmation(args, confirmations, namespace, stdin) {
31
+ const command = extractUpstreamCommandTokens(args);
32
+ const tokens = command[0] === "batch" ? getUpstreamEffectiveBatchSteps(command, stdin)[0] ?? [] : command;
15
33
  if (tokens.length !== 2 || !["confirm", "deny"].includes(tokens[0]))
16
34
  return undefined;
17
35
  const sessionName = extractExplicitSessionName(args);
18
36
  const effectiveNamespace = resolveAgentBrowserNamespace(args, namespace);
19
37
  const matches = [...confirmations].filter(value => value.state === "pending" && value.id === tokens[1]
38
+ && (command[0] !== "batch" || value.source === "native-guarded-action")
20
39
  && (sessionName === undefined || getAgentBrowserSessionIdentityKey(sessionName, value.namespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace))
21
40
  && (effectiveNamespace === undefined || getAgentBrowserSessionIdentityKey(value.sessionName, effectiveNamespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace)));
22
41
  return matches.length === 1 ? matches[0] : undefined;
@@ -28,32 +47,53 @@ export function scopeReadConfirmationArgs(args, confirmation) {
28
47
  ...args,
29
48
  ];
30
49
  }
50
+ export function getNativeTabContinuationGuidance(commandTokens, data) {
51
+ if (commandTokens[0] !== "tab" || commandTokens[1] !== "new" || !isRecord(data)
52
+ || data.confirmation_required !== true || typeof data.confirmation_id !== "string" || !data.confirmation_id || data.action !== "tab_new"
53
+ || !Object.keys(data).every(key => ["confirmation_required", "confirmation_id", "action", "capabilities", "after_confirmation", "guidance"].includes(key))
54
+ || !Array.isArray(data.after_confirmation) || data.after_confirmation.length !== 2 || data.after_confirmation[0] !== "open"
55
+ || typeof data.after_confirmation[1] !== "string" || !data.after_confirmation[1]
56
+ || data.after_confirmation[1] !== getExplicitNavigationTarget(commandTokens)
57
+ || typeof data.guidance !== "string" || !data.guidance.trim())
58
+ return undefined;
59
+ // Transport prose only: approval creates the tab; the continuation is never stored or executed.
60
+ return data.guidance;
61
+ }
31
62
  export function nextReadConfirmation(options) {
32
63
  const { commandTokens: tokens, current } = options;
33
- const settlesRead = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
34
- const confirmedResult = settlesRead && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true && options.data.action === "read" && isRecord(options.data.result)
35
- ? options.data.result.data : options.data;
36
- // Native control fields only. Never parse response content, snapshot text or nested page JSON as provenance.
37
- if (isRecord(confirmedResult) && confirmedResult.confirmation_required === true && typeof confirmedResult.confirmation_id === "string" && confirmedResult.confirmation_id && !("content" in confirmedResult)) {
38
- if (confirmedResult.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || settlesRead)) {
39
- return { ...(isRecord(confirmedResult.capabilities) && confirmedResult.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: confirmedResult.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
64
+ const settles = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
65
+ const confirmed = settles && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true
66
+ && options.data.action === (current.source === "native-explicit-url-read" ? "read" : current.action) && isRecord(options.data.result);
67
+ const control = confirmed && isRecord(options.data) && isRecord(options.data.result) ? options.data.result.data : options.data;
68
+ // Only native control fields and a validated tab transport continuation. Page output is never provenance.
69
+ if (options.succeeded && isRecord(control) && !Array.isArray(control) && control.confirmation_required === true
70
+ && typeof control.confirmation_id === "string" && control.confirmation_id && typeof control.action === "string" && control.action
71
+ && (Object.keys(control).every(key => ["confirmation_required", "confirmation_id", "action", "capabilities"].includes(key))
72
+ || getNativeTabContinuationGuidance(tokens, control) !== undefined)) {
73
+ const explicitRead = control.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || confirmed && current.source === "native-explicit-url-read");
74
+ if (explicitRead)
75
+ return { ...(isRecord(control.capabilities) && control.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: control.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
76
+ if (tokens[0] && !["confirm", "deny"].includes(tokens[0]) || confirmed && current.source === "native-guarded-action") {
77
+ return { id: control.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-guarded-action", command: confirmed ? current.command : tokens[0], action: control.action, state: "pending" };
40
78
  }
41
79
  if (current?.state === "pending")
42
80
  return { ...current, state: "cleared" };
43
81
  }
44
- return options.succeeded && current?.state === "pending" && (settlesRead || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
82
+ return options.succeeded && current?.state === "pending" && (settles || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
45
83
  }
46
84
  export function buildReadConfirmationNextActions(confirmation, pendingResponse) {
47
85
  if (confirmation.state === "cleared")
48
86
  return [];
49
87
  const prefix = ["--namespace", confirmation.namespace ?? "", "--session", confirmation.sessionName];
50
88
  if (!pendingResponse)
51
- return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the read confirmation failed; rerun the original URL read if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
89
+ return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the confirmation failed; rerun the original command if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
52
90
  return ["confirm", "deny"].map(command => ({
53
91
  id: command === "confirm" ? "approve-confirmation" : "deny-confirmation", tool: "agent_browser", params: { args: [...prefix, command, confirmation.id] },
54
- reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for this explicit URL read.`,
55
- safety: confirmation.capabilities?.readRequiresConfirmation === true
92
+ reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for ${confirmation.source === "native-explicit-url-read" ? "this explicit URL read" : `${confirmation.command} (${confirmation.action})`}.`,
93
+ safety: isBrowserIndependentConfirmation(confirmation)
56
94
  ? "Review the requested read first. The native capability proves ID matching; no DOM confirmation is implied."
57
- : "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
95
+ : confirmation.source === "native-guarded-action"
96
+ ? "Review the original guarded action. Exact wrapper observation preserves its session and avoids overwriting helpers; native ID validation is not implied."
97
+ : "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
58
98
  }));
59
99
  }
@@ -34,6 +34,9 @@ function getNestedConfirmationRecord(data) {
34
34
  return undefined;
35
35
  }
36
36
  export function detectConfirmationRequired(data) {
37
+ // Native confirm can acknowledge one action while its original compound command remains pending.
38
+ if (isRecord(data) && data.confirmed === true && isRecord(data.result) && data.result.success === true)
39
+ data = data.result.data;
37
40
  if (!isRecord(data)) {
38
41
  return undefined;
39
42
  }
@@ -47,6 +47,10 @@ export async function parseAgentBrowserEnvelope(options) {
47
47
  catch (error) {
48
48
  return { parseError: error instanceof Error ? error.message : String(error) };
49
49
  }
50
+ // ponytail: native text has no machine receipts; use JSON mode for structured evidence.
51
+ // JSON-looking page text must never become an envelope, confirmation, or batch row.
52
+ if (typeof options !== "string" && options.textOutput)
53
+ return { envelope: { success: true, data: stdout } };
50
54
  const trimmed = stdout.trim();
51
55
  const plainText = typeof options !== "string" && options.plainText === true;
52
56
  if (trimmed.length === 0 && !plainText) {
@@ -150,7 +154,7 @@ export function getAgentBrowserErrorText(options) {
150
154
  if (spawnError)
151
155
  return spawnError.message;
152
156
  if (parseError)
153
- return parseError;
157
+ return exitCode !== 0 && stderr.trim() ? stderr.trim() : parseError;
154
158
  if (envelope?.success === false) {
155
159
  const explicitErrorText = extractEnvelopeErrorText(envelope.error);
156
160
  if ((hasStructuredBatchStepFailure(envelope.data) || detectConfirmationRequired(envelope.data)) && explicitErrorText === undefined) {
@@ -221,6 +221,9 @@ async function buildPreviousRestartRecordingArtifact(options) {
221
221
  const recording = getRecordingReceipt(options.data.previousRecording);
222
222
  return recording ? buildFileArtifactMetadata({ ...options, commandInfo: { command: "record", subcommand: "restart-previous" }, path: recording.path, recording }) : undefined;
223
223
  }
224
+ // A rejected or pending restart has not acknowledged replacement of the active take.
225
+ if (!isRecord(options.data) || options.data.restarted !== true)
226
+ return undefined;
224
227
  const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
225
228
  const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
226
229
  (entry.subcommand === "start" || entry.subcommand === "restart") &&