pi-agent-browser-native 0.2.48 → 0.2.50

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 (189) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +21 -11
  3. package/dist/extensions/agent-browser/index.js +808 -0
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
  6. package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
  9. package/dist/extensions/agent-browser/lib/config-policy.js +669 -0
  10. package/dist/extensions/agent-browser/lib/config.js +122 -0
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
  15. package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
  16. package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
  17. package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
  18. package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
  19. package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
  21. package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
  22. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
  23. package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
  24. package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
  25. package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
  26. package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
  27. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
  28. package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
  29. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
  49. package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
  50. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
  51. package/dist/extensions/agent-browser/lib/playbook.js +121 -0
  52. package/dist/extensions/agent-browser/lib/process.js +363 -0
  53. package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
  54. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
  55. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
  56. package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
  57. package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
  58. package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
  59. package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
  60. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
  61. package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
  62. package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
  63. package/dist/extensions/agent-browser/lib/results/network.js +73 -0
  64. package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
  65. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
  66. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
  67. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
  68. package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
  69. package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
  70. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +956 -0
  71. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
  72. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
  73. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
  74. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
  75. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
  76. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
  77. package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
  78. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
  79. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
  80. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
  81. package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
  82. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
  83. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
  84. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
  85. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
  86. package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
  87. package/dist/extensions/agent-browser/lib/results/text.js +40 -0
  88. package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
  89. package/dist/extensions/agent-browser/lib/runtime.js +855 -0
  90. package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
  91. package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
  92. package/dist/extensions/agent-browser/lib/temp.js +498 -0
  93. package/dist/extensions/agent-browser/lib/web-search.js +562 -0
  94. package/docs/ARCHITECTURE.md +5 -5
  95. package/docs/COMMAND_REFERENCE.md +4 -4
  96. package/docs/RELEASE.md +22 -11
  97. package/docs/REQUIREMENTS.md +1 -1
  98. package/docs/SUPPORT_MATRIX.md +5 -4
  99. package/docs/TOOL_CONTRACT.md +1 -1
  100. package/package.json +9 -5
  101. package/scripts/config.mjs +14 -20
  102. package/scripts/doctor.mjs +8 -7
  103. package/extensions/agent-browser/index.ts +0 -961
  104. package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
  105. package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
  106. package/extensions/agent-browser/lib/bash-guard.ts +0 -205
  107. package/extensions/agent-browser/lib/command-policy.ts +0 -71
  108. package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
  109. package/extensions/agent-browser/lib/config-policy.js +0 -690
  110. package/extensions/agent-browser/lib/config.ts +0 -211
  111. package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
  112. package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
  113. package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
  114. package/extensions/agent-browser/lib/electron/launch.ts +0 -499
  115. package/extensions/agent-browser/lib/executable-path.ts +0 -19
  116. package/extensions/agent-browser/lib/fs-utils.ts +0 -18
  117. package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
  118. package/extensions/agent-browser/lib/input-modes/job.ts +0 -527
  119. package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
  120. package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
  121. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
  122. package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
  123. package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
  124. package/extensions/agent-browser/lib/input-modes.ts +0 -45
  125. package/extensions/agent-browser/lib/json-schema.ts +0 -73
  126. package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
  127. package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
  128. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
  129. package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +0 -44
  130. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -280
  131. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -914
  132. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -521
  133. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
  134. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +0 -158
  135. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +0 -116
  136. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +0 -147
  137. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +0 -183
  138. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +0 -58
  139. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -847
  140. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -559
  141. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
  142. package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +0 -8
  143. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
  144. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -565
  145. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
  146. package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
  147. package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
  148. package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -267
  149. package/extensions/agent-browser/lib/playbook.ts +0 -142
  150. package/extensions/agent-browser/lib/process.ts +0 -516
  151. package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
  152. package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
  153. package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
  154. package/extensions/agent-browser/lib/results/categories.ts +0 -106
  155. package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
  156. package/extensions/agent-browser/lib/results/contracts.ts +0 -241
  157. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
  158. package/extensions/agent-browser/lib/results/envelope.ts +0 -195
  159. package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
  160. package/extensions/agent-browser/lib/results/network.ts +0 -78
  161. package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
  162. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
  163. package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
  164. package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
  165. package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
  166. package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
  167. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
  168. package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
  169. package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
  170. package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
  171. package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
  172. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
  173. package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
  174. package/extensions/agent-browser/lib/results/presentation.ts +0 -257
  175. package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
  176. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
  177. package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
  178. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
  179. package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
  180. package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
  181. package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
  182. package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
  183. package/extensions/agent-browser/lib/results/text.ts +0 -40
  184. package/extensions/agent-browser/lib/runtime.ts +0 -988
  185. package/extensions/agent-browser/lib/session-page-state.ts +0 -512
  186. package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
  187. package/extensions/agent-browser/lib/temp.ts +0 -577
  188. package/extensions/agent-browser/lib/web-search.ts +0 -728
  189. /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Purpose: Launch wrapper-owned Electron applications and discover their CDP endpoint.
3
+ * Responsibilities: Resolve Electron targets, enforce caller-owned allow/deny policy, create isolated userDataDir profiles, launch with remote debugging on an OS-chosen port, poll DevToolsActivePort, and read bounded CDP version/target metadata.
4
+ * Scope: Host-side Electron lifecycle setup only; upstream agent-browser attach/presentation stays in the extension entrypoint.
5
+ * Usage: Called by the agent_browser electron.launch shorthand before routing through upstream `connect`.
6
+ * Invariants/Assumptions: The wrapper only launches targets with Electron framework evidence, always uses an isolated temp profile, and never accepts a caller-supplied remote debugging port.
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import { randomUUID } from "node:crypto";
10
+ import { readFile, rm } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+ import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
13
+ import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
14
+ import { createSecureTempDirectory } from "../temp.js";
15
+ export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
16
+ export const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
17
+ export const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
18
+ const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
19
+ export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
20
+ const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"];
21
+ const ELECTRON_DEVTOOLS_POLL_INTERVAL_MS = 100;
22
+ function normalizeTimeoutMs(timeoutMs) {
23
+ if (!Number.isSafeInteger(timeoutMs) || (timeoutMs ?? 0) <= 0)
24
+ return ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS;
25
+ return Math.min(timeoutMs, ELECTRON_LAUNCH_MAX_TIMEOUT_MS);
26
+ }
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+ function normalizeIdentifier(value) {
31
+ const trimmed = value?.trim().toLowerCase();
32
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
33
+ }
34
+ function appIdentifiers(app) {
35
+ return [app.name, app.bundleId, app.desktopId, app.appPath, app.executablePath]
36
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
37
+ }
38
+ function policyEntryMatchesApp(entry, app) {
39
+ const normalizedEntry = normalizeIdentifier(entry);
40
+ if (!normalizedEntry)
41
+ return false;
42
+ return appIdentifiers(app).some((identifier) => identifier.toLowerCase().includes(normalizedEntry));
43
+ }
44
+ export function evaluateElectronLaunchPolicy(options) {
45
+ const denyEntry = options.deny?.find((entry) => policyEntryMatchesApp(entry, options.target));
46
+ if (denyEntry) {
47
+ return {
48
+ entry: denyEntry,
49
+ list: "deny",
50
+ message: `Electron launch blocked by caller deny policy: ${denyEntry}`,
51
+ };
52
+ }
53
+ if (options.allow && options.allow.length > 0) {
54
+ const allowEntry = options.allow.find((entry) => policyEntryMatchesApp(entry, options.target));
55
+ if (!allowEntry) {
56
+ return {
57
+ list: "allow",
58
+ message: "Electron launch blocked because the resolved app did not match caller allow policy.",
59
+ };
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ export async function resolveElectronLaunchTarget(options) {
65
+ if (options.appPath)
66
+ return inspectElectronAppPath(options.appPath);
67
+ if (options.executablePath)
68
+ return inspectElectronExecutablePath(options.executablePath);
69
+ const query = options.bundleId ?? options.appName;
70
+ const discovery = await discoverElectronApps({ maxResults: 200, query });
71
+ if (options.bundleId) {
72
+ const normalizedBundleId = normalizeIdentifier(options.bundleId);
73
+ return discovery.apps.find((app) => normalizeIdentifier(app.bundleId) === normalizedBundleId);
74
+ }
75
+ if (options.appName) {
76
+ const normalizedName = normalizeIdentifier(options.appName);
77
+ return discovery.apps.find((app) => normalizeIdentifier(app.name) === normalizedName) ?? discovery.apps[0];
78
+ }
79
+ return undefined;
80
+ }
81
+ function targetMatchesType(target, targetType) {
82
+ return targetType === undefined || targetType === "any" || target.type === targetType;
83
+ }
84
+ function selectElectronConnectArg(options) {
85
+ const targetWebSocket = options.targets.find((target) => targetMatchesType(target, options.targetType) && target.webSocketDebuggerUrl)?.webSocketDebuggerUrl;
86
+ return targetWebSocket ?? options.version.webSocketDebuggerUrl ?? String(options.port);
87
+ }
88
+ async function readDevToolsActivePort(userDataDir) {
89
+ const path = `${userDataDir}/${DEVTOOLS_ACTIVE_PORT_FILE}`;
90
+ try {
91
+ const text = await readFile(path, "utf8");
92
+ const [portLine] = text.split(/\r?\n/);
93
+ const port = Number(portLine?.trim());
94
+ return {
95
+ found: true,
96
+ path,
97
+ port: Number.isSafeInteger(port) && port > 0 && port <= 65_535 ? port : undefined,
98
+ ...(Number.isSafeInteger(port) && port > 0 && port <= 65_535 ? {} : { error: "DevToolsActivePort did not contain a valid TCP port." }),
99
+ };
100
+ }
101
+ catch (error) {
102
+ const code = error.code;
103
+ return {
104
+ error: code && code !== "ENOENT" ? `${code}: ${error instanceof Error ? error.message : String(error)}` : undefined,
105
+ found: false,
106
+ path,
107
+ };
108
+ }
109
+ }
110
+ async function pollDevToolsActivePort(options) {
111
+ let devToolsActivePort;
112
+ while (Date.now() <= options.deadlineMs) {
113
+ const spawnError = options.getSpawnError();
114
+ if (spawnError)
115
+ return { devToolsActivePort, failure: "spawn-error", spawnError };
116
+ devToolsActivePort = await readDevToolsActivePort(options.userDataDir);
117
+ if (devToolsActivePort.port)
118
+ return { devToolsActivePort, port: devToolsActivePort.port };
119
+ const exit = options.getChildExit();
120
+ if (exit.code !== null || exit.signal !== null) {
121
+ return { devToolsActivePort, failure: exit.code === 0 ? "single-instance-conflict" : "spawn-error" };
122
+ }
123
+ await sleep(ELECTRON_DEVTOOLS_POLL_INTERVAL_MS);
124
+ }
125
+ return { devToolsActivePort, failure: "timeout" };
126
+ }
127
+ async function pollCdpMetadata(port, deadlineMs) {
128
+ while (Date.now() <= deadlineMs) {
129
+ const version = parseCdpVersion(await fetchCdpJson(`http://127.0.0.1:${port}/json/version`));
130
+ if (version) {
131
+ const targets = parseCdpTargets(await fetchCdpJson(`http://127.0.0.1:${port}/json/list`));
132
+ return { targets, version };
133
+ }
134
+ await sleep(ELECTRON_DEVTOOLS_POLL_INTERVAL_MS);
135
+ }
136
+ return undefined;
137
+ }
138
+ function buildLaunchArgs(userDataDir, appArgs) {
139
+ return [
140
+ ...appArgs,
141
+ `--user-data-dir=${userDataDir}`,
142
+ "--remote-debugging-port=0",
143
+ ...ELECTRON_DEFAULT_APP_ARGS,
144
+ ];
145
+ }
146
+ async function waitForLaunchChildExit(child, deadlineMs) {
147
+ while (Date.now() <= deadlineMs) {
148
+ if (child.exitCode !== null || child.signalCode !== null)
149
+ return true;
150
+ await sleep(50);
151
+ }
152
+ return child.exitCode !== null || child.signalCode !== null;
153
+ }
154
+ function isLaunchChildPidAlive(child) {
155
+ if (!child.pid)
156
+ return undefined;
157
+ if (child.exitCode !== null || child.signalCode !== null)
158
+ return false;
159
+ try {
160
+ process.kill(child.pid, 0);
161
+ return true;
162
+ }
163
+ catch (error) {
164
+ return error.code === "EPERM";
165
+ }
166
+ }
167
+ async function terminateLaunchChild(child) {
168
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null)
169
+ return undefined;
170
+ try {
171
+ child.kill("SIGTERM");
172
+ }
173
+ catch (error) {
174
+ return error instanceof Error ? error.message : String(error);
175
+ }
176
+ if (await waitForLaunchChildExit(child, Date.now() + 1_000))
177
+ return undefined;
178
+ try {
179
+ child.kill("SIGKILL");
180
+ }
181
+ catch (error) {
182
+ return error instanceof Error ? error.message : String(error);
183
+ }
184
+ if (await waitForLaunchChildExit(child, Date.now() + 1_000))
185
+ return undefined;
186
+ return `PID ${child.pid} remained alive after failed Electron launch cleanup.`;
187
+ }
188
+ function buildLaunchRecord(options) {
189
+ return {
190
+ appName: options.target.name,
191
+ appPath: options.target.appPath,
192
+ bundleId: options.target.bundleId,
193
+ cleanupState: "active",
194
+ createdAtMs: options.createdAtMs,
195
+ desktopId: options.target.desktopId,
196
+ executablePath: options.target.executablePath,
197
+ launchId: `electron-${randomUUID()}`,
198
+ launchedByWrapper: true,
199
+ packageSource: options.target.packageSource,
200
+ pid: options.pid,
201
+ platform: options.target.platform,
202
+ port: options.port,
203
+ processGroupId: process.platform === "win32" ? undefined : options.pid,
204
+ targetType: options.targetType,
205
+ userDataDir: options.userDataDir,
206
+ version: ELECTRON_LAUNCH_RECORD_VERSION,
207
+ webSocketDebuggerUrl: options.version.webSocketDebuggerUrl,
208
+ };
209
+ }
210
+ function launchFailureMessage(reason, target, detail) {
211
+ const label = target ? `${target.name} (${target.appPath ?? target.executablePath})` : "target";
212
+ switch (reason) {
213
+ case "non-electron-target":
214
+ return `Electron launch rejected: ${label} does not have Electron framework evidence.`;
215
+ case "policy-blocked":
216
+ return detail ?? `Electron launch blocked by caller policy for ${label}.`;
217
+ case "single-instance-conflict":
218
+ return `Electron launch did not expose a debug port for ${label}; the app may already be running as a single-instance Electron app. Quit the running app and retry.`;
219
+ case "port-not-found":
220
+ return `Electron launch found a DevToolsActivePort for ${label}, but /json/version never returned a valid CDP payload.`;
221
+ case "spawn-error":
222
+ return `Electron launch failed while starting ${label}${detail ? `: ${detail}` : "."}`;
223
+ case "timeout":
224
+ return `Electron launch timed out waiting for DevToolsActivePort for ${label}.`;
225
+ }
226
+ }
227
+ export async function launchElectronApp(options) {
228
+ const appArgs = options.appArgs ?? [];
229
+ const target = await resolveElectronLaunchTarget(options);
230
+ if (!target) {
231
+ return {
232
+ ok: false,
233
+ failure: {
234
+ appArgs,
235
+ error: launchFailureMessage("non-electron-target", undefined),
236
+ reason: "non-electron-target",
237
+ },
238
+ };
239
+ }
240
+ const policy = evaluateElectronLaunchPolicy({ allow: options.allow, deny: options.deny, target });
241
+ if (policy) {
242
+ return {
243
+ ok: false,
244
+ failure: {
245
+ appArgs,
246
+ error: launchFailureMessage("policy-blocked", target, policy.message),
247
+ policy,
248
+ reason: "policy-blocked",
249
+ target,
250
+ },
251
+ };
252
+ }
253
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
254
+ const startedAtMs = Date.now();
255
+ const deadlineMs = startedAtMs + timeoutMs;
256
+ const userDataDir = await createSecureTempDirectory(ELECTRON_PROFILE_DIR_PREFIX);
257
+ let cleanupError;
258
+ let spawnError;
259
+ let exitCode = null;
260
+ let exitSignal = null;
261
+ const args = buildLaunchArgs(userDataDir, appArgs);
262
+ const child = spawn(target.executablePath, args, {
263
+ cwd: dirname(target.executablePath),
264
+ detached: process.platform !== "win32",
265
+ stdio: "ignore",
266
+ });
267
+ child.once("error", (error) => {
268
+ spawnError = error;
269
+ });
270
+ child.once("exit", (code, signal) => {
271
+ exitCode = code;
272
+ exitSignal = signal;
273
+ });
274
+ child.unref();
275
+ const buildFailureDiagnostics = (options = {}) => ({
276
+ cdpVersionReached: options.cdpVersionReached,
277
+ devToolsActivePort: options.devToolsActivePort,
278
+ elapsedMs: Math.max(0, Date.now() - startedAtMs),
279
+ exitCode,
280
+ exitSignal,
281
+ outputCaptured: false,
282
+ pid: child.pid,
283
+ pidAlive: isLaunchChildPidAlive(child),
284
+ port: options.port ?? options.devToolsActivePort?.port,
285
+ timeoutMs,
286
+ userDataDir,
287
+ });
288
+ const fail = async (reason, detail, diagnosticOptions) => {
289
+ const diagnostics = buildFailureDiagnostics(diagnosticOptions);
290
+ const processCleanupError = await terminateLaunchChild(child);
291
+ try {
292
+ await rm(userDataDir, { force: true, recursive: true });
293
+ }
294
+ catch (error) {
295
+ cleanupError = error instanceof Error ? error.message : String(error);
296
+ }
297
+ cleanupError = [processCleanupError, cleanupError].filter((value) => value !== undefined).join("; ") || undefined;
298
+ return {
299
+ ok: false,
300
+ failure: {
301
+ appArgs,
302
+ cleanupError,
303
+ diagnostics,
304
+ error: launchFailureMessage(reason, target, detail),
305
+ reason,
306
+ target,
307
+ userDataDir,
308
+ },
309
+ };
310
+ };
311
+ const portResult = await pollDevToolsActivePort({
312
+ deadlineMs,
313
+ getChildExit: () => ({ code: exitCode, signal: exitSignal }),
314
+ getSpawnError: () => spawnError,
315
+ userDataDir,
316
+ });
317
+ if (!portResult.port) {
318
+ return fail(portResult.failure ?? "timeout", portResult.spawnError?.message, { devToolsActivePort: portResult.devToolsActivePort });
319
+ }
320
+ const metadata = await pollCdpMetadata(portResult.port, deadlineMs);
321
+ if (!metadata) {
322
+ return fail("port-not-found", undefined, { cdpVersionReached: false, devToolsActivePort: portResult.devToolsActivePort, port: portResult.port });
323
+ }
324
+ const record = buildLaunchRecord({
325
+ createdAtMs: Date.now(),
326
+ pid: child.pid,
327
+ port: portResult.port,
328
+ target,
329
+ targetType: options.targetType,
330
+ userDataDir,
331
+ version: metadata.version,
332
+ });
333
+ const connectArg = selectElectronConnectArg({
334
+ port: portResult.port,
335
+ targets: metadata.targets,
336
+ targetType: options.targetType,
337
+ version: metadata.version,
338
+ });
339
+ return {
340
+ ok: true,
341
+ value: {
342
+ appArgs,
343
+ child,
344
+ connectArg,
345
+ record,
346
+ target,
347
+ targets: metadata.targets,
348
+ version: metadata.version,
349
+ },
350
+ };
351
+ }
@@ -5,9 +5,9 @@
5
5
  * Usage: Imported by Electron host probes and browser-run Electron diagnostics.
6
6
  * Invariants/Assumptions: Empty or whitespace-only strings stay omitted, and truncation keeps the prior ellipsis behavior.
7
7
  */
8
-
9
- export function boundElectronProbeString(value: string | undefined, maxLength = 240): string | undefined {
10
- const trimmed = value?.trim();
11
- if (!trimmed) return undefined;
12
- return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
8
+ export function boundElectronProbeString(value, maxLength = 240) {
9
+ const trimmed = value?.trim();
10
+ if (!trimmed)
11
+ return undefined;
12
+ return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
13
13
  }
@@ -0,0 +1,20 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { access, stat } from "node:fs/promises";
3
+ import { delimiter, join } from "node:path";
4
+ export async function executableExistsOnPath(command) {
5
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
6
+ for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
7
+ for (const extension of extensions) {
8
+ try {
9
+ const candidate = join(directory, `${command}${extension}`);
10
+ await access(candidate, fsConstants.X_OK);
11
+ if ((await stat(candidate)).isFile())
12
+ return true;
13
+ }
14
+ catch {
15
+ // Try the next PATH candidate.
16
+ }
17
+ }
18
+ }
19
+ return false;
20
+ }
@@ -0,0 +1,18 @@
1
+ import { access, stat } from "node:fs/promises";
2
+ export async function pathExists(path) {
3
+ try {
4
+ await access(path);
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export async function directoryExists(path) {
12
+ try {
13
+ return (await stat(path)).isDirectory();
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Purpose: Compile top-level Electron wrapper inputs into validated Electron actions.
3
+ * Responsibilities: Enforce action-specific fields, launch-target rules, and wrapper-owned flag safety.
4
+ * Scope: Electron input-mode validation only; launch/probe/cleanup execution stays in the extension entrypoint.
5
+ */
6
+ import { isRecord } from "../parsing.js";
7
+ import { AGENT_BROWSER_ELECTRON_ACTIONS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_LIST_FIELDS, AGENT_BROWSER_ELECTRON_PROBE_FIELDS, AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, } from "./types.js";
8
+ function validateOptionalNonEmptyString(input, fieldName) {
9
+ const value = input[fieldName];
10
+ if (value === undefined)
11
+ return {};
12
+ if (typeof value !== "string" || value.trim().length === 0) {
13
+ return { error: `electron.${fieldName} must be a non-empty string when provided.` };
14
+ }
15
+ return { value: value.trim() };
16
+ }
17
+ function validateOptionalElectronStringArray(input, fieldName) {
18
+ const value = input[fieldName];
19
+ if (value === undefined)
20
+ return undefined;
21
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
22
+ return `electron.${fieldName} must be an array of non-empty strings when provided.`;
23
+ }
24
+ return undefined;
25
+ }
26
+ function validateOptionalElectronEnum(input, fieldName, values) {
27
+ const value = input[fieldName];
28
+ if (value === undefined)
29
+ return undefined;
30
+ if (typeof value !== "string" || !values.includes(value)) {
31
+ return `electron.${fieldName} must be one of: ${values.join(", ")}.`;
32
+ }
33
+ return undefined;
34
+ }
35
+ function getReservedElectronAppArg(appArgs) {
36
+ return appArgs?.find((arg) => {
37
+ const trimmed = arg.trim();
38
+ return trimmed === "--" || AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS.some((reserved) => trimmed === reserved || trimmed.startsWith(`${reserved}=`));
39
+ });
40
+ }
41
+ function validateElectronLaunchAppArgs(appArgs) {
42
+ const reservedArg = getReservedElectronAppArg(appArgs);
43
+ return reservedArg
44
+ ? `electron.appArgs must not include wrapper-owned launch flag ${reservedArg}.`
45
+ : undefined;
46
+ }
47
+ function validateOptionalElectronPositiveInteger(input, fieldName) {
48
+ const value = input[fieldName];
49
+ if (value === undefined)
50
+ return {};
51
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
52
+ return { error: `electron.${fieldName} must be a positive integer when provided.` };
53
+ }
54
+ return { value };
55
+ }
56
+ function onlyAllowedElectronFields(input, action, allowedFields) {
57
+ return Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))
58
+ ? `electron.${action} does not support electron.${Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))}.`
59
+ : undefined;
60
+ }
61
+ export function compileAgentBrowserElectron(input) {
62
+ if (!isRecord(input))
63
+ return { error: "electron must be an object." };
64
+ const action = input.action;
65
+ if (typeof action !== "string" || !AGENT_BROWSER_ELECTRON_ACTIONS.includes(action)) {
66
+ return { error: `electron.action must be one of: ${AGENT_BROWSER_ELECTRON_ACTIONS.join(", ")}.` };
67
+ }
68
+ for (const fieldName of ["query", "appPath", "appName", "bundleId", "executablePath", "launchId"]) {
69
+ const validation = validateOptionalNonEmptyString(input, fieldName);
70
+ if (validation.error)
71
+ return { error: validation.error };
72
+ }
73
+ for (const fieldName of ["appArgs", "allow", "deny"]) {
74
+ const error = validateOptionalElectronStringArray(input, fieldName);
75
+ if (error)
76
+ return { error };
77
+ }
78
+ const handoffError = validateOptionalElectronEnum(input, "handoff", AGENT_BROWSER_ELECTRON_HANDOFFS);
79
+ if (handoffError)
80
+ return { error: handoffError };
81
+ const targetTypeError = validateOptionalElectronEnum(input, "targetType", AGENT_BROWSER_ELECTRON_TARGET_TYPES);
82
+ if (targetTypeError)
83
+ return { error: targetTypeError };
84
+ for (const fieldName of ["maxResults", "timeoutMs"]) {
85
+ const validation = validateOptionalElectronPositiveInteger(input, fieldName);
86
+ if (validation.error)
87
+ return { error: validation.error };
88
+ }
89
+ if (input.all !== undefined && input.all !== true) {
90
+ return { error: "electron.all must be true when provided." };
91
+ }
92
+ if (action === "list") {
93
+ const unsupportedListField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_LIST_FIELDS.has(fieldName));
94
+ if (unsupportedListField) {
95
+ return { error: `electron.list only supports query and maxResults; remove electron.${unsupportedListField}.` };
96
+ }
97
+ return {
98
+ compiled: {
99
+ action: "list",
100
+ maxResults: validateOptionalElectronPositiveInteger(input, "maxResults").value,
101
+ query: validateOptionalNonEmptyString(input, "query").value,
102
+ },
103
+ };
104
+ }
105
+ if (action === "probe") {
106
+ const unsupportedProbeField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_PROBE_FIELDS.has(fieldName));
107
+ if (unsupportedProbeField) {
108
+ return { error: `electron.probe only supports action, launchId, and timeoutMs; remove electron.${unsupportedProbeField}.` };
109
+ }
110
+ const launchId = validateOptionalNonEmptyString(input, "launchId").value;
111
+ const timeoutMs = validateOptionalElectronPositiveInteger(input, "timeoutMs").value;
112
+ return {
113
+ compiled: {
114
+ action: "probe",
115
+ ...(launchId ? { launchId } : {}),
116
+ ...(timeoutMs ? { timeoutMs } : {}),
117
+ },
118
+ };
119
+ }
120
+ if (action === "launch") {
121
+ const allowedFields = new Set(["action", "allow", "appArgs", "appName", "appPath", "bundleId", "deny", "executablePath", "handoff", "targetType", "timeoutMs"]);
122
+ const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
123
+ if (unsupportedFieldError)
124
+ return { error: unsupportedFieldError };
125
+ const appArgs = input.appArgs?.map((item) => item.trim());
126
+ const appArgsError = validateElectronLaunchAppArgs(appArgs);
127
+ if (appArgsError)
128
+ return { error: appArgsError };
129
+ const targetFields = ["appPath", "appName", "bundleId", "executablePath"];
130
+ const providedTargets = targetFields.filter((fieldName) => input[fieldName] !== undefined);
131
+ if (providedTargets.length !== 1) {
132
+ return { error: "electron.launch requires exactly one of appPath, appName, bundleId, or executablePath." };
133
+ }
134
+ return {
135
+ compiled: {
136
+ action: "launch",
137
+ allow: input.allow?.map((item) => item.trim()),
138
+ appArgs,
139
+ deny: input.deny?.map((item) => item.trim()),
140
+ appName: validateOptionalNonEmptyString(input, "appName").value,
141
+ appPath: validateOptionalNonEmptyString(input, "appPath").value,
142
+ bundleId: validateOptionalNonEmptyString(input, "bundleId").value,
143
+ executablePath: validateOptionalNonEmptyString(input, "executablePath").value,
144
+ handoff: input.handoff ?? "snapshot",
145
+ targetType: input.targetType ?? "page",
146
+ timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
147
+ },
148
+ };
149
+ }
150
+ const allowedFields = new Set(["action", "all", "launchId", "timeoutMs"]);
151
+ const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
152
+ if (unsupportedFieldError)
153
+ return { error: unsupportedFieldError };
154
+ if (input.all === true && input.launchId !== undefined) {
155
+ return { error: `electron.${action} accepts launchId or all, not both.` };
156
+ }
157
+ return {
158
+ compiled: {
159
+ action: action,
160
+ all: input.all === true || undefined,
161
+ launchId: validateOptionalNonEmptyString(input, "launchId").value,
162
+ timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
163
+ },
164
+ };
165
+ }