pi-agent-browser-native 0.9.4 → 0.9.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 (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +58 -813
  3. package/dist/extensions/agent-browser/index.js +927 -883
  4. package/dist/extensions/agent-browser/lib/browser-execution-claims.js +5 -5
  5. package/dist/extensions/agent-browser/lib/config-policy.js +1 -1
  6. package/dist/extensions/agent-browser/lib/config-validation.js +21 -8
  7. package/dist/extensions/agent-browser/lib/config.js +1 -1
  8. package/dist/extensions/agent-browser/lib/electron/cdp.js +5 -1
  9. package/dist/extensions/agent-browser/lib/electron/cleanup.js +13 -47
  10. package/dist/extensions/agent-browser/lib/electron/discovery-evidence.js +1 -0
  11. package/dist/extensions/agent-browser/lib/electron/launch.js +12 -1
  12. package/dist/extensions/agent-browser/lib/fs-utils.js +1 -9
  13. package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -4
  14. package/dist/extensions/agent-browser/lib/input-modes/lookup-network-evidence.js +15 -3
  15. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +3 -2
  16. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -23
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/electron-observation.js +9 -8
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result-electron.js +8 -0
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +2 -2
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +9 -0
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state-electron.js +4 -2
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state-refs.js +9 -11
  23. package/dist/extensions/agent-browser/lib/orchestration/electron-host/branch.js +1 -0
  24. package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-collection.js +5 -6
  25. package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-data.js +4 -2
  26. package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-presentation.js +20 -2
  27. package/dist/extensions/agent-browser/lib/orchestration/electron-host/status.js +4 -2
  28. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +1 -1
  29. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +7 -2
  30. package/dist/extensions/agent-browser/lib/temp-artifact-budget.js +4 -1
  31. package/dist/extensions/agent-browser/lib/temp-root.js +22 -18
  32. package/dist/extensions/agent-browser/lib/temp.js +1 -1
  33. package/dist/extensions/agent-browser/lib/web-search-providers.js +0 -36
  34. package/dist/extensions/agent-browser/lib/web-search-request.js +20 -26
  35. package/dist/extensions/agent-browser/lib/web-search-schema.js +5 -5
  36. package/dist/extensions/agent-browser/lib/web-search.js +22 -4
  37. package/docs/ARCHITECTURE.md +3 -2
  38. package/docs/CODE_QUALITY.md +28 -3
  39. package/docs/COMMAND_REFERENCE.md +20 -20
  40. package/docs/ELECTRON.md +24 -2
  41. package/docs/REFERENCE.md +869 -0
  42. package/docs/RELEASE.md +9 -9
  43. package/docs/REQUIREMENTS.md +1 -1
  44. package/docs/SUPPORT_MATRIX.md +9 -8
  45. package/docs/TOOL_CONTRACT.md +13 -9
  46. package/docs/platform-smoke.md +3 -3
  47. package/package.json +3 -2
  48. package/scripts/config.mjs +23 -19
  49. package/scripts/doctor.mjs +2 -2
  50. package/scripts/platform-smoke/lease-evidence.mjs +2 -4
  51. package/scripts/platform-smoke/linux-image/Dockerfile +8 -2
  52. package/scripts/platform-smoke/targets.mjs +1 -5
@@ -123,9 +123,8 @@ async function publishTicket(claim) {
123
123
  await rename(candidate, join(claim.path, LOCK_TICKET_FILE));
124
124
  return ticket.ticket;
125
125
  }
126
- function acquisitionExpired(budget) {
127
- return (budget.signal?.aborted === true ||
128
- (budget.deadline !== undefined && Date.now() >= budget.deadline));
126
+ function acquisitionCancelled(budget) {
127
+ return budget.signal?.aborted === true;
129
128
  }
130
129
  async function waitForTurn(claim, ticket, budget, deadline) {
131
130
  while (budget.signal?.aborted !== true) {
@@ -145,8 +144,9 @@ async function waitForTurn(claim, ticket, budget, deadline) {
145
144
  // Opportunistic GC must finish before the final cancellation check.
146
145
  // oxlint-disable-next-line no-await-in-loop
147
146
  await cleanDeadPolicyArtifacts(claim.directory, budget);
148
- // Re-read cancellation/deadline after the awaited filesystem/native probes.
149
- return !acquisitionExpired(budget);
147
+ // GC stays within the wait budget but cannot revoke a turn this claim already won;
148
+ // only cancellation can. Callers enforce their own operation deadlines.
149
+ return !acquisitionCancelled(budget);
150
150
  }
151
151
  if (Date.now() >= deadline) {
152
152
  return false;
@@ -5,7 +5,7 @@ import { join, resolve } from "node:path";
5
5
  import { DEFAULT_WEB_SEARCH_PROVIDER, WEB_SEARCH_PROVIDERS, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, } from "./config-providers.js";
6
6
  import { parseAgentBrowserConfigLayer, describeUnknownError } from "./config-validation.js";
7
7
  export * from "./config-providers.js";
8
- export { parseAgentBrowserConfigLayer, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-validation.js";
8
+ export { parseAgentBrowserConfigJson, parseAgentBrowserConfigLayer, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-validation.js";
9
9
  /** @typedef {import("./config-providers.js").WebSearchProvider} WebSearchProvider */
10
10
  /** @typedef {import("./config-providers.js").ExaSearchType} ExaSearchType */
11
11
  /** @typedef {import("./config-providers.js").WebSearchProviderConfigKey} WebSearchProviderConfigKey */
@@ -179,6 +179,23 @@ export function validateAgentBrowserConfig(value, path, errors, warnings) {
179
179
  ...(browser !== undefined ? { browser } : {}),
180
180
  };
181
181
  }
182
+ /** Parse config without retaining native token previews or source-bearing error causes.
183
+ * @param {string} raw
184
+ * @returns {{ readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly error: string }} */
185
+ export function parseAgentBrowserConfigJson(raw) {
186
+ try {
187
+ return { ok: true, value: JSON.parse(raw) };
188
+ }
189
+ catch (error) {
190
+ const location = error instanceof Error
191
+ ? error.message.match(/at position \d+(?: \(line \d+ column \d+\))?/)?.[0]
192
+ : undefined;
193
+ return {
194
+ ok: false,
195
+ error: location === undefined ? "Invalid JSON." : `Invalid JSON ${location}.`,
196
+ };
197
+ }
198
+ }
182
199
  /** @param {string} raw
183
200
  * @param {string} path
184
201
  * @param {ConfigLayerScope} scope
@@ -186,16 +203,12 @@ export function validateAgentBrowserConfig(value, path, errors, warnings) {
186
203
  * @param {ConfigDiagnostics} warnings
187
204
  * @returns {ConfigLayer | undefined} */
188
205
  export function parseAgentBrowserConfigLayer(raw, path, scope, errors, warnings) {
189
- /** @type {unknown} */
190
- let parsed;
191
- try {
192
- parsed = JSON.parse(raw);
193
- }
194
- catch (error) {
195
- errors.push(`Could not parse ${scope} config ${path}: ${error instanceof Error ? error.message : describeUnknownError(error)}`);
206
+ const parsed = parseAgentBrowserConfigJson(raw);
207
+ if (!parsed.ok) {
208
+ errors.push(`Could not parse ${scope} config ${path}: ${parsed.error}`);
196
209
  return;
197
210
  }
198
- const config = validateAgentBrowserConfig(parsed, path, errors, warnings);
211
+ const config = validateAgentBrowserConfig(parsed.value, path, errors, warnings);
199
212
  return config ? { config, path, scope } : undefined;
200
213
  }
201
214
  /** @param {unknown} value */
@@ -1,7 +1,7 @@
1
1
  import { exec as execCallback } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { SECRET_COMMAND_TIMEOUT_MS, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, resolveEnvInterpolations, } from "./config-policy.js";
4
- export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
4
+ export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderDescriptor, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigJson, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
5
5
  // Node's callback-oriented exec returns ChildProcess; promisify intentionally ignores that return.
6
6
  // oxlint-disable-next-line typescript/strict-void-return
7
7
  const exec = promisify(execCallback);
@@ -7,8 +7,12 @@ export function parseCdpVersion(value) {
7
7
  if (!isRecord(value)) {
8
8
  return undefined;
9
9
  }
10
+ const browser = asString(value.Browser) ?? asString(value.browser);
11
+ if (browser === undefined) {
12
+ return undefined;
13
+ }
10
14
  return {
11
- browser: asString(value.Browser) ?? asString(value.browser),
15
+ browser,
12
16
  protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
13
17
  userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
14
18
  v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
@@ -1,19 +1,13 @@
1
- import { execFile } from "node:child_process";
2
1
  import { lstat, rm } from "node:fs/promises";
3
- import { win32 } from "node:path";
4
- import { promisify } from "node:util";
5
2
  import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
6
3
  import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch-types.js";
7
4
  import { pathExists } from "../fs-utils.js";
8
5
  import { isRecord } from "../parsing.js";
6
+ import { readProcessStartIdentity, processStartIdentitiesMatch } from "../process-identity.js";
9
7
  import { stringifyUnknown } from "../results/text.js";
10
8
  import { getSecureTempChildDirectoryValidationError } from "../temp.js";
11
9
  const ELECTRON_CLEANUP_DEFAULT_TIMEOUT_MS = 5_000;
12
10
  const ELECTRON_CLEANUP_POLL_INTERVAL_MS = 100;
13
- const RESTORED_PROCESS_COMMAND_TIMEOUT_MS = 1_000;
14
- // Node's callback-style execFile returns a ChildProcess; promisify owns its completion callback, not that return value.
15
- // oxlint-disable-next-line typescript/strict-void-return
16
- const execFileAsync = promisify(execFile);
17
11
  function sleep(ms) {
18
12
  return new Promise((resolve) => {
19
13
  setTimeout(resolve, ms);
@@ -77,48 +71,16 @@ async function waitForProcessExit(child, pid, deadlineMs) {
77
71
  }
78
72
  return hasProcessExited(child, pid);
79
73
  }
80
- async function readPidCommandLine(pid) {
81
- if (!Number.isSafeInteger(pid) || pid <= 0) {
82
- return undefined;
83
- }
84
- try {
85
- // Win32_Process.CommandLine is the native ownership evidence; process
86
- // start time (used by daemon-policy locks) cannot prove profile ownership.
87
- const systemRoot = process.env.SystemRoot;
88
- const windowsRoot = systemRoot !== undefined && systemRoot.length > 0 && win32.isAbsolute(systemRoot)
89
- ? systemRoot
90
- : "C:\\Windows";
91
- const file = process.platform === "win32"
92
- ? win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
93
- : "ps";
94
- const args = process.platform === "win32"
95
- ? [
96
- "-NoProfile",
97
- "-NonInteractive",
98
- "-Command",
99
- `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $p = [wmi]"Win32_Process.Handle='${pid}'"; [Console]::WriteLine($p.CommandLine)`,
100
- ]
101
- : ["-ww", "-p", String(pid), "-o", "command="];
102
- const { stdout } = await execFileAsync(file, args, {
103
- timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
104
- });
105
- const commandLine = stdout.trim();
106
- return commandLine.length === 0 ? undefined : commandLine;
107
- }
108
- catch {
109
- return undefined;
110
- }
111
- }
112
- function restoredLaunchCommandMatchesRecord(record, commandLine) {
113
- return commandLine?.includes(`--user-data-dir=${record.userDataDir}`) === true;
114
- }
115
74
  async function getRestoredProcessVerificationError(record) {
116
- const commandLine = record.pid === undefined ? undefined : await readPidCommandLine(record.pid);
117
- if (commandLine === undefined || commandLine.length === 0) {
118
- return `PID ${String(record.pid)} is alive, but this session has no tracked child handle and its command line could not be inspected; refusing to signal a restored PID that may have been reused.`;
75
+ if (record.startIdentity === undefined || record.startIdentity.length === 0) {
76
+ return `PID ${String(record.pid)} is alive, but this session has no tracked child handle or recorded start identity; refusing to signal a restored PID that may have been reused.`;
119
77
  }
120
- if (!restoredLaunchCommandMatchesRecord(record, commandLine)) {
121
- return `PID ${String(record.pid)} is alive, but this session has no tracked child handle and its command line does not include wrapper-owned user data dir ${record.userDataDir}; refusing to signal a restored PID that may have been reused.`;
78
+ const currentIdentity = record.pid === undefined ? undefined : await readProcessStartIdentity(record.pid);
79
+ if (currentIdentity === undefined) {
80
+ return `PID ${String(record.pid)} is alive, but its current start identity could not be inspected; refusing to signal a restored PID that may have been reused.`;
81
+ }
82
+ if (!processStartIdentitiesMatch(record.startIdentity, currentIdentity)) {
83
+ return `PID ${String(record.pid)} is alive, but its start identity does not match the wrapper launch record; refusing to signal a restored PID that may have been reused.`;
122
84
  }
123
85
  return undefined;
124
86
  }
@@ -178,6 +140,10 @@ async function cleanupRestoredProcess(record, deadlineMs) {
178
140
  if (await waitForProcessExit(undefined, record.pid, deadlineMs)) {
179
141
  return { resource: "process", state: "removed" };
180
142
  }
143
+ const killVerificationError = await getRestoredProcessVerificationError(record);
144
+ if (killVerificationError !== undefined) {
145
+ return { error: killVerificationError, resource: "process", state: "failed" };
146
+ }
181
147
  signalRestoredLaunchProcess(record, "SIGKILL");
182
148
  if (await waitForProcessExit(undefined, record.pid, Date.now() + 1_000)) {
183
149
  return { resource: "process", state: "removed" };
@@ -49,6 +49,7 @@ export async function directoryContainsChromePak(directory) {
49
49
  }
50
50
  export async function hasElectronAppPayload(resourcesDirectory) {
51
51
  return ((await pathExists(join(resourcesDirectory, "app.asar"))) ||
52
+ (await pathExists(join(resourcesDirectory, "default_app.asar"))) ||
52
53
  (await isDirectory(join(resourcesDirectory, "app"))));
53
54
  }
54
55
  export async function hasLinuxElectronEvidence(executablePath) {
@@ -3,6 +3,7 @@ import { rm } from "node:fs/promises";
3
3
  import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
4
4
  import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
5
5
  import { stringifyUnknown } from "../results/text.js";
6
+ import { readProcessStartIdentity } from "../process-identity.js";
6
7
  import { isElectronLaunchAborted, pollCdpMetadata, pollDevToolsActivePort, } from "./launch-monitor.js";
7
8
  import { ElectronLaunchProcess } from "./launch-process.js";
8
9
  import { ELECTRON_LAUNCH_RECORD_VERSION, ELECTRON_PROFILE_DIR_PREFIX, } from "./launch-types.js";
@@ -93,7 +94,7 @@ function launchFailureMessage(reason, target, detail) {
93
94
  case "aborted":
94
95
  return `Electron launch was aborted${target ? ` before ${label} finished starting` : " before the app started"}.`;
95
96
  case "non-electron-target":
96
- return `Electron launch rejected: ${label} does not have Electron framework evidence.`;
97
+ return `Electron launch rejected: ${label} does not have the required Electron runtime and app payload evidence.`;
97
98
  case "policy-blocked":
98
99
  return detail ?? `Electron launch blocked by caller policy for ${label}.`;
99
100
  case "single-instance-conflict":
@@ -116,6 +117,7 @@ class ElectronLaunchAttempt {
116
117
  timeoutMs;
117
118
  deadlineMs;
118
119
  appArgs;
120
+ startIdentity;
119
121
  constructor(options, target, userDataDir, timing) {
120
122
  this.options = options;
121
123
  this.target = target;
@@ -194,6 +196,7 @@ class ElectronLaunchAttempt {
194
196
  platform: this.target.platform,
195
197
  port,
196
198
  processGroupId: process.platform === "win32" ? undefined : pid,
199
+ startIdentity: this.startIdentity,
197
200
  targetType: this.options.targetType,
198
201
  userDataDir: this.userDataDir,
199
202
  version: ELECTRON_LAUNCH_RECORD_VERSION,
@@ -211,6 +214,14 @@ class ElectronLaunchAttempt {
211
214
  if (this.process.child === undefined) {
212
215
  return this.fail(isElectronLaunchAborted(this.options.signal) ? "aborted" : "spawn-error", this.process.spawnError?.message);
213
216
  }
217
+ const pid = this.process.child.pid;
218
+ this.startIdentity =
219
+ pid === undefined
220
+ ? undefined
221
+ : await readProcessStartIdentity(pid, process.platform, {
222
+ deadline: this.deadlineMs,
223
+ signal: this.options.signal,
224
+ });
214
225
  const portResult = await pollDevToolsActivePort({
215
226
  deadlineMs: this.deadlineMs,
216
227
  getChildExit: this.process.childExit,
@@ -1,4 +1,4 @@
1
- import { access, stat } from "node:fs/promises";
1
+ import { access } from "node:fs/promises";
2
2
  export async function pathExists(path) {
3
3
  try {
4
4
  await access(path);
@@ -8,11 +8,3 @@ export async function pathExists(path) {
8
8
  return false;
9
9
  }
10
10
  }
11
- export async function directoryExists(path) {
12
- try {
13
- return (await stat(path)).isDirectory();
14
- }
15
- catch {
16
- return false;
17
- }
18
- }
@@ -19,9 +19,10 @@ function buildQaVisibleTextPredicate(text) {
19
19
  if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
20
20
  return element.getClientRects().length > 0;
21
21
  };
22
- const hasVisibleAncestors = (node) => {
22
+ const ancestorsAllowVisibility = (node) => {
23
23
  for (let element = node.parentElement; element; element = element.parentElement) {
24
- if (!isVisibleElement(element)) return false;
24
+ const style = window.getComputedStyle(element);
25
+ if (style.display === "none" || Number(style.opacity) === 0) return false;
25
26
  if (element === root) break;
26
27
  }
27
28
  return true;
@@ -29,14 +30,16 @@ function buildQaVisibleTextPredicate(text) {
29
30
  const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
30
31
  let visitedText = 0;
31
32
  for (let node = textWalker.nextNode(); node && visitedText < 6000; node = textWalker.nextNode(), visitedText += 1) {
32
- if (!hasVisibleAncestors(node)) continue;
33
+ const parent = node.parentElement;
34
+ if (!(parent instanceof HTMLElement) || skipTags.has(parent.tagName)) continue;
35
+ if (window.getComputedStyle(parent).visibility === "hidden" || !ancestorsAllowVisibility(node)) continue;
33
36
  if (normalize(node.nodeValue).includes(expected)) return true;
34
37
  }
35
38
  const elementWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
36
39
  let visitedElements = 0;
37
40
  for (let node = elementWalker.nextNode(); node && visitedElements < 3000; node = elementWalker.nextNode(), visitedElements += 1) {
38
41
  const element = node;
39
- if (!isVisibleElement(element) || !("value" in element)) continue;
42
+ if (!isVisibleElement(element) || !ancestorsAllowVisibility(element) || !("value" in element)) continue;
40
43
  if (normalize(element.value).includes(expected)) return true;
41
44
  }
42
45
  return false;
@@ -47,7 +47,17 @@ export function getFailedNetworkRequests(data, queryText) {
47
47
  });
48
48
  }
49
49
  }
50
- return failed;
50
+ const seenRequestIds = new Set();
51
+ return failed.filter(({ requestId: id }) => {
52
+ if (id === undefined) {
53
+ return true;
54
+ }
55
+ if (seenRequestIds.has(id)) {
56
+ return false;
57
+ }
58
+ seenRequestIds.add(id);
59
+ return true;
60
+ });
51
61
  }
52
62
  export function distinctNetworkCandidates(candidates) {
53
63
  const keys = new Set();
@@ -84,7 +94,7 @@ function correlatesWithFailure(value, failedIds, failedUrls) {
84
94
  const url = typeof value.url === "string" ? value.url : undefined;
85
95
  return (id !== undefined && failedIds.has(id)) || (url !== undefined && failedUrls.has(url));
86
96
  }
87
- export function observeInitiatorCandidates(data, failedRequests) {
97
+ export function observeInitiatorCandidates(data, failedRequests, queryText) {
88
98
  const failedIds = new Set(failedRequests.map((request) => request.requestId).filter((value) => value !== undefined));
89
99
  const failedUrls = new Set(failedRequests.map((request) => request.url).filter((value) => value !== undefined));
90
100
  const candidates = [];
@@ -95,7 +105,9 @@ export function observeInitiatorCandidates(data, failedRequests) {
95
105
  if (!isRecord(value)) {
96
106
  continue;
97
107
  }
98
- if (correlatesWithFailure(value, failedIds, failedUrls) || isFailedNetworkRecord(value)) {
108
+ const url = typeof value.url === "string" ? value.url : undefined;
109
+ if (networkRequestMatchesQuery(url, queryText) &&
110
+ (correlatesWithFailure(value, failedIds, failedUrls) || isFailedNetworkRecord(value))) {
99
111
  candidates.push(...initiatorCandidates(value));
100
112
  }
101
113
  }
@@ -64,8 +64,9 @@ function networkSummary(status, failedCount, candidateCount) {
64
64
  export async function analyzeNetworkSourceLookupResults(data, compiled, cwd) {
65
65
  const { distinctNetworkCandidates, getFailedNetworkRequests, observeInitiatorCandidates } = await import("./lookup-network-evidence.js");
66
66
  const { scanWorkspaceRequests } = await import("./lookup-workspace.js");
67
- const failedRequests = getFailedNetworkRequests(data, compiled.query.url ?? compiled.query.filter);
68
- const observed = observeInitiatorCandidates(data, failedRequests);
67
+ const queryText = compiled.query.filter ?? compiled.query.url;
68
+ const failedRequests = getFailedNetworkRequests(data, queryText);
69
+ const observed = observeInitiatorCandidates(data, failedRequests, queryText);
69
70
  const workspace = await scanWorkspaceRequests(compiled.query, failedRequests, cwd);
70
71
  const candidates = distinctNetworkCandidates([...observed, ...workspace.candidates]);
71
72
  const status = networkStatus(failedRequests.length, candidates.length);
@@ -1,9 +1,9 @@
1
- import { JsonSchema } from "../json-schema.js";
2
- import { StringEnum as localStringEnum } from "../string-enum-schema.js";
1
+ import { JsonSchema as Type } from "../json-schema.js";
2
+ import { StringEnum } from "../string-enum-schema.js";
3
3
  import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS, } from "../electron/discovery-types.js";
4
4
  import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
5
5
  const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
6
- function outputProperties(Type) {
6
+ function outputProperties() {
7
7
  return {
8
8
  outputPath: Type.Optional(Type.String({
9
9
  description: "Result-data path, separate from browser artifact paths.",
@@ -12,14 +12,14 @@ function outputProperties(Type) {
12
12
  timeoutMs: Type.Optional(Type.Integer({ description: "Timeout in ms; exceed explicit waits.", minimum: 1 })),
13
13
  };
14
14
  }
15
- function sessionModeProperty(Type, StringEnum) {
15
+ function sessionModeProperty() {
16
16
  return Type.Optional(StringEnum(["auto", "fresh"], {
17
17
  description: "Native configured sessions win; auto reuses the browser, fresh intentionally launches a separate managed browser.",
18
18
  default: DEFAULT_SESSION_MODE,
19
19
  }));
20
20
  }
21
21
  // Keep the always-on schemas small; specialized guidance belongs to each advanced tool.
22
- export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
22
+ export function createAgentBrowserParamsSchema() {
23
23
  return Type.Unsafe(Type.Object({
24
24
  args: Type.Array(Type.String(), {
25
25
  description: "Native agent-browser argv without the binary or shell syntax. Fixed sequences: batch --bail with JSON-array stdin.",
@@ -28,11 +28,11 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
28
28
  stdin: Type.Optional(Type.String({
29
29
  description: "Native batch JSON, eval --stdin source, or auth save --password-stdin text.",
30
30
  })),
31
- ...outputProperties(Type),
32
- sessionMode: sessionModeProperty(Type, StringEnum),
31
+ ...outputProperties(),
32
+ sessionMode: sessionModeProperty(),
33
33
  }, { additionalProperties: false }));
34
34
  }
35
- export function createAgentBrowserCodeParamsSchema(Type = JsonSchema) {
35
+ export function createAgentBrowserCodeParamsSchema() {
36
36
  return Type.Unsafe(Type.Object({
37
37
  code: Type.String({
38
38
  description: "JavaScript with await browser({args, stdin?, timeoutMs?}), emit(JSON), and emitImage(image handle). Fresh JS context; persistent browser.",
@@ -46,11 +46,11 @@ export function createAgentBrowserCodeParamsSchema(Type = JsonSchema) {
46
46
  namespace: Type.Optional(Type.String({
47
47
  description: "Native namespace; empty string selects the default namespace.",
48
48
  })),
49
- ...outputProperties(Type),
49
+ ...outputProperties(),
50
50
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: AGENT_BROWSER_CODE_MAX_TIMEOUT_MS })),
51
51
  }, { additionalProperties: false }));
52
52
  }
53
- export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
53
+ export function createAgentBrowserActionParamsSchema() {
54
54
  return Type.Unsafe(Type.Object({
55
55
  action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
56
56
  locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
@@ -68,7 +68,7 @@ export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEn
68
68
  role: Type.Optional(Type.String({ description: "Role locator; select needs combobox or listbox." })),
69
69
  name: Type.Optional(Type.String({ description: "Accessible name." })),
70
70
  session: Type.Optional(Type.String({ description: "Upstream session name." })),
71
- ...outputProperties(Type),
71
+ ...outputProperties(),
72
72
  }, {
73
73
  additionalProperties: false,
74
74
  // Top-level properties let Pi normalize optional nulls before validating branches.
@@ -81,7 +81,7 @@ export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEn
81
81
  ],
82
82
  }));
83
83
  }
84
- export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
84
+ export function createAgentBrowserQaParamsSchema() {
85
85
  return Type.Unsafe(Type.Object({
86
86
  url: Type.Optional(Type.String()),
87
87
  attached: Type.Optional(Type.Boolean()),
@@ -92,8 +92,8 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
92
92
  checkErrors: Type.Optional(Type.Boolean()),
93
93
  checkNetwork: Type.Optional(Type.Boolean()),
94
94
  loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES)),
95
- ...outputProperties(Type),
96
- sessionMode: sessionModeProperty(Type, StringEnum),
95
+ ...outputProperties(),
96
+ sessionMode: sessionModeProperty(),
97
97
  }, {
98
98
  additionalProperties: false,
99
99
  anyOf: [
@@ -105,8 +105,8 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
105
105
  ],
106
106
  }));
107
107
  }
108
- export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
109
- const common = { outputPath: outputProperties(Type).outputPath };
108
+ export function createAgentBrowserElectronParamsSchema() {
109
+ const common = { outputPath: outputProperties().outputPath };
110
110
  const timeoutMs = Type.Optional(Type.Integer({ minimum: 1 }));
111
111
  const list = {
112
112
  query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
@@ -147,7 +147,7 @@ export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, String
147
147
  ],
148
148
  }));
149
149
  }
150
- export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
150
+ export function createAgentBrowserSourceParamsSchema() {
151
151
  return Type.Unsafe(Type.Object({
152
152
  selector: Type.Optional(Type.String({ description: "Visible selector or @ref." })),
153
153
  reactFiberId: Type.Optional(Type.String({ description: "React fiber id; requires --enable react-devtools." })),
@@ -158,11 +158,11 @@ export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEn
158
158
  minimum: 1,
159
159
  maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES,
160
160
  })),
161
- ...outputProperties(Type),
162
- sessionMode: sessionModeProperty(Type, StringEnum),
161
+ ...outputProperties(),
162
+ sessionMode: sessionModeProperty(),
163
163
  }, { additionalProperties: false }));
164
164
  }
165
- export function createAgentBrowserNetworkSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
165
+ export function createAgentBrowserNetworkSourceParamsSchema() {
166
166
  return Type.Unsafe(Type.Object({
167
167
  filter: Type.Optional(Type.String({ description: "Network request filter." })),
168
168
  namespace: Type.Optional(Type.String()),
@@ -174,11 +174,11 @@ export function createAgentBrowserNetworkSourceParamsSchema(Type = JsonSchema, S
174
174
  minimum: 1,
175
175
  maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES,
176
176
  })),
177
- ...outputProperties(Type),
178
- sessionMode: sessionModeProperty(Type, StringEnum),
177
+ ...outputProperties(),
178
+ sessionMode: sessionModeProperty(),
179
179
  }, { additionalProperties: false }));
180
180
  }
181
- export function createAgentBrowserToolsParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
181
+ export function createAgentBrowserToolsParamsSchema() {
182
182
  return Type.Unsafe(Type.Object({
183
183
  enable: Type.Optional(Type.Array(StringEnum(AGENT_BROWSER_ADVANCED_TOOLS), {
184
184
  description: "Add capabilities to active tools; omit to list inventory.",
@@ -4,8 +4,9 @@ import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next
4
4
  import { extractRefSnapshotFromData } from "../../session-page-state.js";
5
5
  import { extractStringResultField, runSessionCommandData } from "./session-state.js";
6
6
  import { diagnosticErrorText } from "./diagnostic-values.js";
7
- function boundedSessionResult(data, field, limit) {
8
- return boundElectronProbeString(extractStringResultField(data, "result") ?? extractStringResultField(data, field), limit);
7
+ import { presentElectronManagedSessionTarget } from "./final-result-electron.js";
8
+ function sessionResult(data, field) {
9
+ return extractStringResultField(data, "result") ?? extractStringResultField(data, field);
9
10
  }
10
11
  async function collectManagedSessionCommandData(options) {
11
12
  try {
@@ -17,8 +18,7 @@ async function collectManagedSessionCommandData(options) {
17
18
  }
18
19
  async function collectElectronManagedSessionUrl(options) {
19
20
  const urlResult = await collectManagedSessionCommandData({ ...options, args: ["get", "url"] });
20
- const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ??
21
- extractStringResultField(urlResult.data, "url"), 300);
21
+ const url = sessionResult(urlResult.data, "url");
22
22
  return urlResult.error !== undefined && urlResult.error !== ""
23
23
  ? { error: urlResult.error }
24
24
  : { url };
@@ -29,8 +29,8 @@ export async function collectElectronManagedSessionTarget(options) {
29
29
  }
30
30
  const request = { ...options, sessionName: options.sessionName };
31
31
  const urlResult = await collectManagedSessionCommandData({ ...request, args: ["get", "url"] });
32
- const url = boundedSessionResult(urlResult.data, "url", 300);
33
- if ((urlResult.error ?? "") !== "" || url === undefined || url === "") {
32
+ const url = sessionResult(urlResult.data, "url");
33
+ if ((urlResult.error ?? "") !== "" || url === undefined || url.trim() === "") {
34
34
  return {
35
35
  error: urlResult.error ?? "get url returned no active page URL.",
36
36
  sessionName: options.sessionName,
@@ -40,7 +40,7 @@ export async function collectElectronManagedSessionTarget(options) {
40
40
  ...request,
41
41
  args: ["get", "title"],
42
42
  });
43
- const title = boundedSessionResult(titleResult.data, "title", 160);
43
+ const title = boundElectronProbeString(sessionResult(titleResult.data, "title"), 160);
44
44
  return {
45
45
  sessionName: options.sessionName,
46
46
  title,
@@ -62,12 +62,13 @@ export async function collectQaAttachedTarget(options) {
62
62
  url: target.url,
63
63
  };
64
64
  }
65
- return collectElectronManagedSessionTarget({
65
+ const observed = await collectElectronManagedSessionTarget({
66
66
  cwd: options.cwd,
67
67
  namespace: options.namespace,
68
68
  sessionName: options.sessionName,
69
69
  signal: options.signal,
70
70
  });
71
+ return observed ? presentElectronManagedSessionTarget(observed) : undefined;
71
72
  }
72
73
  export function formatQaAttachedTargetText(target) {
73
74
  return target
@@ -1,6 +1,14 @@
1
+ import { boundElectronProbeString } from "../../electron/cdp.js";
1
2
  import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
2
3
  import { redactSensitiveText } from "../../runtime-redaction.js";
3
4
  import { redactToolDetails } from "./final-result-redaction.js";
5
+ export function presentElectronManagedSessionTarget(target) {
6
+ return {
7
+ ...target,
8
+ title: boundElectronProbeString(target.title, 160),
9
+ url: boundElectronProbeString(target.url, 300),
10
+ };
11
+ }
4
12
  export function getElectronLaunchFailureCategory(failure) {
5
13
  switch (failure.reason) {
6
14
  case "aborted":
@@ -5,7 +5,7 @@ import { buildScrollNoopNextActions } from "../interaction-diagnostics.js";
5
5
  import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
6
6
  const SCROLL_CONTAINER_DIRECTIONS = new Set(["down", "left", "right", "up"]);
7
7
  function getContainerScrollRequest(commandTokens) {
8
- if (commandTokens[0] !== "scroll" || commandTokens.length < 3) {
8
+ if (commandTokens[0] !== "scroll" || commandTokens.length < 3 || commandTokens.length > 4) {
9
9
  return undefined;
10
10
  }
11
11
  const selector = commandTokens[1];
@@ -138,7 +138,7 @@ function getPageScrollToRequest(commandTokens) {
138
138
  return undefined;
139
139
  }
140
140
  if (commandTokens[1]?.toLowerCase() === "to") {
141
- return getPageScrollTarget(commandTokens.at(2));
141
+ return commandTokens.length === 3 ? getPageScrollTarget(commandTokens.at(2)) : undefined;
142
142
  }
143
143
  const direction = commandTokens[1]?.toLowerCase();
144
144
  if (!isPageScrollDirection(direction) || commandTokens.length > 3) {
@@ -51,6 +51,13 @@ async function collectSnapshotEvidence(options, request) {
51
51
  ? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot)
52
52
  : undefined;
53
53
  if (fullSnapshot) {
54
+ if (fullSnapshot.target) {
55
+ options.sessionPageState.applyTabTarget({
56
+ sessionName: options.sessionStateKey ?? options.sessionName,
57
+ target: fullSnapshot.target,
58
+ update: options.sessionPageStateUpdate,
59
+ });
60
+ }
54
61
  options.sessionPageState.applyRefSnapshot({
55
62
  sessionName: options.sessionStateKey ?? options.sessionName,
56
63
  snapshot: fullSnapshot,
@@ -133,6 +140,8 @@ function snapshotToolResult(options, request, evidence, presentation) {
133
140
  fullOutputPaths: presentation.fullOutputPaths,
134
141
  refSnapshot: evidence.fullSnapshot,
135
142
  sessionMode: options.sessionMode,
143
+ sessionTabTarget: options.sessionPageState.get(options.sessionStateKey ?? options.sessionName)
144
+ .tabTarget,
136
145
  snapshotDiff: evidence.diff,
137
146
  snapshotFilter: snapshotFilterDetails(request, evidence),
138
147
  snapshotViewport: evidence.viewport,
@@ -3,6 +3,7 @@ import { isElectronPostCommandHealthCommand } from "../../command-taxonomy.js";
3
3
  import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
4
4
  import { isAboutBlankUrl, normalizeComparableUrl, } from "../../session-page-state.js";
5
5
  import { getGuardedRefUsage } from "./session-state-refs.js";
6
+ import { presentElectronManagedSessionTarget } from "./final-result-electron.js";
6
7
  export function isLiveElectronRendererTarget(target) {
7
8
  const normalizedUrl = normalizeComparableUrl(target.url);
8
9
  if (normalizedUrl === undefined ||
@@ -105,15 +106,16 @@ export function buildElectronSessionMismatch(options) {
105
106
  return;
106
107
  }
107
108
  const nextActions = buildElectronMismatchNextActions(options.record, liveTarget);
109
+ const managedSession = presentElectronManagedSessionTarget(options.managedSession);
108
110
  return {
109
111
  launchId: options.record.launchId,
110
112
  liveTarget,
111
- managedSession: options.managedSession,
113
+ managedSession,
112
114
  nextActionIds: nextActions.map((action) => action.id),
113
115
  reason,
114
116
  sessionName: options.record.sessionName,
115
117
  statusTargets: options.statusTargets,
116
- summary: describeElectronMismatch(options.managedSession, options.record, liveTarget, reason),
118
+ summary: describeElectronMismatch(managedSession, options.record, liveTarget, reason),
117
119
  };
118
120
  }
119
121
  export function formatElectronSessionMismatchText(mismatch) {