@phnx-labs/agents-cli 1.22.116 → 1.22.118

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 (254) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.d.ts +18 -0
  9. package/dist/commands/exec.js +104 -12
  10. package/dist/commands/repo.js +13 -49
  11. package/dist/commands/route.js +5 -5
  12. package/dist/commands/routines.js +39 -12
  13. package/dist/commands/sessions-render.d.ts +1 -1
  14. package/dist/commands/sessions-render.js +0 -1
  15. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  16. package/dist/commands/sessions.test-fixture.js +0 -37
  17. package/dist/commands/setup-browser.d.ts +17 -12
  18. package/dist/commands/setup-browser.js +52 -87
  19. package/dist/commands/setup-preferences.d.ts +0 -17
  20. package/dist/commands/setup-preferences.js +18 -49
  21. package/dist/commands/setup-term.d.ts +4 -3
  22. package/dist/commands/setup-term.js +5 -4
  23. package/dist/commands/setup.js +11 -11
  24. package/dist/commands/ssh.js +10 -1
  25. package/dist/commands/sync.js +6 -44
  26. package/dist/commands/teams.js +0 -1
  27. package/dist/commands/update.d.ts +19 -0
  28. package/dist/commands/update.js +34 -6
  29. package/dist/commands/view.d.ts +12 -1
  30. package/dist/commands/view.js +47 -2
  31. package/dist/index.js +107 -26
  32. package/dist/lib/account-capabilities.js +0 -2
  33. package/dist/lib/account-catalog.d.ts +38 -0
  34. package/dist/lib/account-catalog.js +110 -26
  35. package/dist/lib/account-provider-registry.js +1 -1
  36. package/dist/lib/account-registry.d.ts +1 -1
  37. package/dist/lib/account-registry.js +36 -2
  38. package/dist/lib/accounting/rotate.js +22 -8
  39. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  40. package/dist/lib/accounting/usage-ingest.js +115 -45
  41. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  42. package/dist/lib/accounting/usage-sync.js +227 -66
  43. package/dist/lib/accounting/usage.d.ts +16 -3
  44. package/dist/lib/accounting/usage.js +26 -8
  45. package/dist/lib/accounts/slots.d.ts +8 -0
  46. package/dist/lib/accounts/slots.js +17 -0
  47. package/dist/lib/add-dir.js +0 -1
  48. package/dist/lib/agent-cli-commands.js +0 -1
  49. package/dist/lib/agent-spec/agents.d.ts +6 -7
  50. package/dist/lib/agent-spec/agents.js +6 -60
  51. package/dist/lib/auth-health.d.ts +69 -1
  52. package/dist/lib/auth-health.js +133 -4
  53. package/dist/lib/browser/context.d.ts +82 -0
  54. package/dist/lib/browser/context.js +88 -0
  55. package/dist/lib/browser/paths.d.ts +25 -0
  56. package/dist/lib/browser/paths.js +51 -0
  57. package/dist/lib/browser/record.d.ts +38 -0
  58. package/dist/lib/browser/record.js +57 -0
  59. package/dist/lib/browser/sessions-list.js +1 -2
  60. package/dist/lib/browser-client.d.ts +158 -0
  61. package/dist/lib/browser-client.js +234 -0
  62. package/dist/lib/channels/owner-sink.d.ts +1 -18
  63. package/dist/lib/channels/owner-sink.js +29 -82
  64. package/dist/lib/channels/providers/rush.d.ts +7 -7
  65. package/dist/lib/channels/providers/rush.js +127 -57
  66. package/dist/lib/claude-statusline.d.ts +1 -1
  67. package/dist/lib/claude-statusline.js +15 -3
  68. package/dist/lib/commands.js +1 -1
  69. package/dist/lib/crabbox/lease.js +1 -1
  70. package/dist/lib/crabbox/runtimes.js +1 -3
  71. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  72. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  73. package/dist/lib/daemon/auth-sync-service.js +37 -43
  74. package/dist/lib/daemon/daemon.d.ts +25 -14
  75. package/dist/lib/daemon/daemon.js +92 -154
  76. package/dist/lib/daemon/runner.d.ts +2 -2
  77. package/dist/lib/daemon/runner.js +32 -9
  78. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  79. package/dist/lib/daemon/self-update-service.js +40 -36
  80. package/dist/lib/daemon/service.d.ts +12 -8
  81. package/dist/lib/daemon/supervisor.d.ts +65 -54
  82. package/dist/lib/daemon/supervisor.js +136 -171
  83. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  84. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  85. package/dist/lib/daemon/usage-sync-service.js +46 -78
  86. package/dist/lib/daemon-health.d.ts +22 -5
  87. package/dist/lib/daemon-health.js +47 -3
  88. package/dist/lib/daemon-services.d.ts +1 -1
  89. package/dist/lib/daemon-services.js +0 -10
  90. package/dist/lib/daemon-ticks.js +4 -2
  91. package/dist/lib/devices/doctor-findings.js +8 -11
  92. package/dist/lib/devices/harness-inventory.js +14 -5
  93. package/dist/lib/exec-account-home.js +5 -0
  94. package/dist/lib/exec.js +0 -11
  95. package/dist/lib/feed/events.js +0 -1
  96. package/dist/lib/feed/tool-activity.js +5 -2
  97. package/dist/lib/fleet-shared-state.d.ts +45 -0
  98. package/dist/lib/fleet-shared-state.js +78 -9
  99. package/dist/lib/fs-atomic.js +25 -0
  100. package/dist/lib/harness-auth-capabilities.js +0 -1
  101. package/dist/lib/helper-versions.js +1 -1
  102. package/dist/lib/hooks/cache.d.ts +7 -0
  103. package/dist/lib/hooks/cache.js +27 -2
  104. package/dist/lib/hooks/install.js +7 -1
  105. package/dist/lib/hosts/ready.d.ts +38 -8
  106. package/dist/lib/hosts/ready.js +62 -14
  107. package/dist/lib/hosts/remote-cmd.js +1 -0
  108. package/dist/lib/installations/active-check.d.ts +27 -0
  109. package/dist/lib/installations/active-check.js +61 -0
  110. package/dist/lib/installations/migrate.js +6 -3
  111. package/dist/lib/installations/shims.js +9 -7
  112. package/dist/lib/installations/update.js +21 -15
  113. package/dist/lib/installations/versions.js +12 -1
  114. package/dist/lib/manifest.js +1 -1
  115. package/dist/lib/models.js +0 -2
  116. package/dist/lib/open-url.js +28 -54
  117. package/dist/lib/placement.d.ts +2 -0
  118. package/dist/lib/placement.js +5 -0
  119. package/dist/lib/profiles.js +0 -2
  120. package/dist/lib/refresh.js +1 -1
  121. package/dist/lib/routine-readiness.d.ts +35 -0
  122. package/dist/lib/routine-readiness.js +74 -16
  123. package/dist/lib/rules/compile.js +6 -0
  124. package/dist/lib/sandbox.js +0 -2
  125. package/dist/lib/secrets-policy.d.ts +89 -11
  126. package/dist/lib/secrets-policy.js +175 -28
  127. package/dist/lib/session/db.d.ts +1 -1
  128. package/dist/lib/session/db.js +65 -18
  129. package/dist/lib/session/discover.js +1 -283
  130. package/dist/lib/session/mirror.d.ts +4 -3
  131. package/dist/lib/session/mirror.js +4 -3
  132. package/dist/lib/session/parse.d.ts +0 -2
  133. package/dist/lib/session/parse.js +1 -138
  134. package/dist/lib/session/throughput.d.ts +1 -3
  135. package/dist/lib/session/throughput.js +0 -20
  136. package/dist/lib/session/types.d.ts +2 -2
  137. package/dist/lib/session/types.js +2 -2
  138. package/dist/lib/sessions-client.d.ts +60 -1
  139. package/dist/lib/sessions-client.js +227 -4
  140. package/dist/lib/setup-tool-status.js +8 -12
  141. package/dist/lib/signin-badge.d.ts +3 -2
  142. package/dist/lib/signin-badge.js +6 -3
  143. package/dist/lib/smart-launch.js +4 -1
  144. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  147. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  148. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  149. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  150. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  151. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  152. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  153. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  154. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  155. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  156. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  157. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  158. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  159. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  160. package/dist/lib/staleness/writers/sources.js +5 -0
  161. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  162. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  163. package/dist/lib/state.d.ts +6 -4
  164. package/dist/lib/state.js +7 -17
  165. package/dist/lib/teams/parsers.d.ts +1 -1
  166. package/dist/lib/teams/parsers.js +1 -136
  167. package/dist/lib/teams/placement-probe.js +50 -15
  168. package/dist/lib/teams/scheduler.d.ts +7 -0
  169. package/dist/lib/types.d.ts +10 -1
  170. package/dist/lib/types.js +1 -1
  171. package/dist/lib/view-types.d.ts +32 -0
  172. package/dist/lib/watchdog/read.js +0 -1
  173. package/dist/lib/watchdog/rotate.js +1 -2
  174. package/dist/lib/watchdog/runner.js +2 -2
  175. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  176. package/dist/lib/watchdog/watchdogTail.js +0 -31
  177. package/package.json +1 -1
  178. package/scripts/postinstall.js +8 -8
  179. package/dist/browser.d.ts +0 -2
  180. package/dist/browser.js +0 -17
  181. package/dist/commands/browser-picker.d.ts +0 -18
  182. package/dist/commands/browser-picker.js +0 -97
  183. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  184. package/dist/lib/browser/arc-discovery.js +0 -197
  185. package/dist/lib/browser/arc-dom.d.ts +0 -14
  186. package/dist/lib/browser/arc-dom.js +0 -121
  187. package/dist/lib/browser/caller-identity.d.ts +0 -40
  188. package/dist/lib/browser/caller-identity.js +0 -175
  189. package/dist/lib/browser/cdp.d.ts +0 -51
  190. package/dist/lib/browser/cdp.js +0 -254
  191. package/dist/lib/browser/chrome.d.ts +0 -141
  192. package/dist/lib/browser/chrome.js +0 -704
  193. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  194. package/dist/lib/browser/chromium-discovery.js +0 -96
  195. package/dist/lib/browser/devices.d.ts +0 -34
  196. package/dist/lib/browser/devices.js +0 -61
  197. package/dist/lib/browser/domain-skills.d.ts +0 -71
  198. package/dist/lib/browser/domain-skills.js +0 -195
  199. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  200. package/dist/lib/browser/drivers/arc.js +0 -276
  201. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  202. package/dist/lib/browser/drivers/firefox.js +0 -377
  203. package/dist/lib/browser/drivers/local.d.ts +0 -55
  204. package/dist/lib/browser/drivers/local.js +0 -266
  205. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  206. package/dist/lib/browser/drivers/ssh.js +0 -467
  207. package/dist/lib/browser/editor.d.ts +0 -3
  208. package/dist/lib/browser/editor.js +0 -50
  209. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  210. package/dist/lib/browser/ffmpeg.js +0 -184
  211. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  212. package/dist/lib/browser/firefox-discovery.js +0 -162
  213. package/dist/lib/browser/har.d.ts +0 -85
  214. package/dist/lib/browser/har.js +0 -77
  215. package/dist/lib/browser/hygiene.d.ts +0 -97
  216. package/dist/lib/browser/hygiene.js +0 -153
  217. package/dist/lib/browser/index.d.ts +0 -5
  218. package/dist/lib/browser/index.js +0 -5
  219. package/dist/lib/browser/input.d.ts +0 -7
  220. package/dist/lib/browser/input.js +0 -92
  221. package/dist/lib/browser/ipc.d.ts +0 -204
  222. package/dist/lib/browser/ipc.js +0 -1453
  223. package/dist/lib/browser/login-detection.d.ts +0 -87
  224. package/dist/lib/browser/login-detection.js +0 -274
  225. package/dist/lib/browser/profiles.d.ts +0 -422
  226. package/dist/lib/browser/profiles.js +0 -1157
  227. package/dist/lib/browser/refs.d.ts +0 -89
  228. package/dist/lib/browser/refs.js +0 -191
  229. package/dist/lib/browser/registry.d.ts +0 -70
  230. package/dist/lib/browser/registry.js +0 -252
  231. package/dist/lib/browser/remote-control.d.ts +0 -59
  232. package/dist/lib/browser/remote-control.js +0 -85
  233. package/dist/lib/browser/resolve-target.d.ts +0 -66
  234. package/dist/lib/browser/resolve-target.js +0 -264
  235. package/dist/lib/browser/runtime-state.d.ts +0 -291
  236. package/dist/lib/browser/runtime-state.js +0 -584
  237. package/dist/lib/browser/secret-ref.d.ts +0 -10
  238. package/dist/lib/browser/secret-ref.js +0 -14
  239. package/dist/lib/browser/service.d.ts +0 -812
  240. package/dist/lib/browser/service.js +0 -4693
  241. package/dist/lib/browser/stream.d.ts +0 -17
  242. package/dist/lib/browser/stream.js +0 -72
  243. package/dist/lib/browser/task-index.d.ts +0 -78
  244. package/dist/lib/browser/task-index.js +0 -199
  245. package/dist/lib/browser/types.d.ts +0 -647
  246. package/dist/lib/browser/types.js +0 -61
  247. package/dist/lib/browser/upload.d.ts +0 -29
  248. package/dist/lib/browser/upload.js +0 -298
  249. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  250. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  251. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  252. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  253. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  254. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,467 +0,0 @@
1
- import { spawn, execFileSync } from 'child_process';
2
- import * as net from 'net';
3
- import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
4
- import { getPortOccupant } from '../chrome.js';
5
- import { parseEndpointUrl } from '../profiles.js';
6
- import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
7
- // shellQuote lives in the shared ssh-exec helper (single choke point); re-export
8
- // so existing importers of `shellQuote` from this module keep working.
9
- import { shellQuote, assertValidSshTarget, controlOpts, sshConnectOpts } from '../../ssh-exec.js';
10
- export { shellQuote };
11
- // The `ssh -L` tunnel spawn is shared with `agents computer --device`; it lives in
12
- // the single ssh-tunnel helper. Calling it with no options preserves this
13
- // driver's original foreground, stderr-captured behavior exactly.
14
- import { startSSHTunnel } from '../../ssh-tunnel.js';
15
- import { encodePwshBase64 } from '../../pwsh.js';
16
- /** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
17
- export function persistRemoteLifecycle(persistRemote) {
18
- return persistRemote
19
- ? { launchRemote: false, killOnCleanup: false }
20
- : { launchRemote: true, killOnCleanup: true };
21
- }
22
- export async function connectSSH(endpoint, profile,
23
- /**
24
- * Runtime key (`<profile>@<device>`) this connection is stored under. It
25
- * keys the tunnel's runtime record; `profile.name` is the bare, user-facing
26
- * name and appears only in messages (RUSH-2709).
27
- */
28
- key, opts = {}) {
29
- const url = new URL(endpoint);
30
- if (url.protocol !== 'ssh:') {
31
- throw new Error(`Invalid SSH endpoint: ${endpoint}`);
32
- }
33
- const user = url.username || process.env.USER || 'root';
34
- // Use the shared parser so the documented `ssh://host?port=N` form works
35
- // identically to `ssh://host:N`. Previously `url.port` alone meant every
36
- // `?port=`-style profile silently fell back to 9222.
37
- const parsed = parseEndpointUrl(endpoint);
38
- if (!parsed) {
39
- throw new Error(`Could not extract host:port from SSH endpoint: ${endpoint}`);
40
- }
41
- const host = parsed.host;
42
- const remotePort = parsed.port;
43
- // `&os=windows` switches the remote-command dialect (cmd.exe launch via
44
- // `start`, taskkill teardown). Anything else — including absent — is posix.
45
- // The query param is the single source of truth so the driver never has to
46
- // be threaded a separate per-profile field.
47
- const remoteOs = (url.searchParams.get('os') || '').toLowerCase() === 'windows' ? 'windows' : 'posix';
48
- // Bind the tunnel to the SAME local port the user configured. Using an
49
- // allocated port instead made `status` print confusing rows like
50
- // `port 9200 (configured 10005)` and made it impossible to predict which
51
- // local port a profile would land on. Now `ssh://host?port=N` => local N.
52
- const localPort = remotePort;
53
- // Preflight: if the local port is busy with something that isn't our
54
- // own SSH tunnel for this very target, bail with a clear error. Letting
55
- // ssh -L race ahead would either silently succeed (binding to a second
56
- // port via fail-safe) or fail with cryptic stderr.
57
- const occupant = getPortOccupant(localPort);
58
- if (occupant && !isOwnTunnel(occupant.pid, host, remotePort)) {
59
- throw new Error(`Local port ${localPort} (needed for SSH tunnel to ${host}:${remotePort}) ` +
60
- `is already in use by ${occupant.command} (pid ${occupant.pid}). ` +
61
- `Either kill that process (\`kill ${occupant.pid}\`) or change the profile's port.`);
62
- }
63
- // A non-zero remote exit (missing exe, auth failure, bad launch) now rejects
64
- // with the captured stderr and propagates. An already-running browser exits 0
65
- // (the launch backgrounds/WMI-spawns regardless), so this does not spuriously
66
- // fail the reconnect path — only genuine launch failures reach the caller
67
- // instead of being swallowed and mis-reported later as a tunnel timeout.
68
- //
69
- // persistRemote skips the launch: launching would mint a logged-out
70
- // `--user-data-dir=/tmp/agents-browser-N` under a name that means a
71
- // credentialed browser on the declaring device.
72
- const lifecycle = persistRemoteLifecycle(Boolean(opts.persistRemote));
73
- if (lifecycle.launchRemote) {
74
- await ensureRemoteBrowser(user, host, profile.browser, remotePort, remoteOs, profile.binary);
75
- }
76
- let tunnel;
77
- if (occupant) {
78
- // Reuse the existing tunnel rather than spawning a duplicate.
79
- tunnel = { pid: occupant.pid, kill: () => { try {
80
- process.kill(occupant.pid);
81
- }
82
- catch { /* gone */ } } };
83
- }
84
- else {
85
- tunnel = await startSSHTunnel(user, host, localPort, remotePort);
86
- }
87
- try {
88
- await waitForPort(localPort, 8000);
89
- }
90
- catch {
91
- tunnel.kill();
92
- throw new Error(`SSH tunnel failed to establish to ${host}`);
93
- }
94
- let wsUrl;
95
- let browser;
96
- try {
97
- ({ wsUrl, browser } = await discoverBrowserWsUrl(localPort, 'localhost', profile.name));
98
- }
99
- catch (err) {
100
- tunnel.kill();
101
- if (opts.persistRemote) {
102
- const why = err instanceof Error ? err.message : String(err);
103
- throw new Error(`Browser profile "${profile.name}" is declared on ${host}, but nothing is speaking CDP on port ${remotePort}: ${why}`);
104
- }
105
- throw err;
106
- }
107
- try {
108
- verifyBrowserIdentity(browser, profile.browser, remotePort, host);
109
- }
110
- catch (err) {
111
- tunnel.kill();
112
- throw err;
113
- }
114
- const cdp = new CDPClient();
115
- await cdp.connect(wsUrl);
116
- // Record the tunnel in the profile's runtime so a future daemon — or
117
- // the orphan reaper after a crash — can find and clean it up. The
118
- // `kind: 'tunnel'` flag distinguishes it from a locally-launched
119
- // browser process.
120
- const tunnelPid = tunnel.pid ?? 0;
121
- if (tunnelPid > 0) {
122
- writeProfileRuntime(key, {
123
- pid: 0,
124
- port: localPort,
125
- command: 'ssh',
126
- kind: 'tunnel',
127
- tunnelPid,
128
- });
129
- }
130
- return {
131
- cdp,
132
- port: localPort,
133
- pid: tunnelPid,
134
- cleanup: () => {
135
- cdp.close();
136
- // Kill the remote browser BEFORE tearing down the tunnel — but only
137
- // when we launched it. persistRemote is an attach to someone else's
138
- // declared browser; killing it would drop the logins the caller came
139
- // for. Fire-and-forget — cleanup stays synchronous.
140
- if (lifecycle.killOnCleanup) {
141
- killRemoteBrowser(user, host, remoteOs, remotePort).catch(() => { });
142
- }
143
- tunnel.kill();
144
- clearProfileRuntime(key);
145
- },
146
- };
147
- }
148
- async function waitForPort(port, timeoutMs) {
149
- const start = Date.now();
150
- while (Date.now() - start < timeoutMs) {
151
- try {
152
- await tryConnect(port);
153
- return;
154
- }
155
- catch {
156
- await sleep(200);
157
- }
158
- }
159
- throw new Error(`Port ${port} not ready after ${timeoutMs}ms`);
160
- }
161
- function tryConnect(port) {
162
- return new Promise((resolve, reject) => {
163
- const socket = net.createConnection({ port, host: '127.0.0.1' });
164
- socket.on('connect', () => {
165
- socket.destroy();
166
- resolve();
167
- });
168
- socket.on('error', reject);
169
- });
170
- }
171
- // macOS .app launchers — the historical POSIX table.
172
- const POSIX_BROWSER_PATHS = {
173
- chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
174
- comet: '/Applications/Comet.app/Contents/MacOS/Comet',
175
- chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
176
- brave: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
177
- edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
178
- arc: '/Applications/Arc.app/Contents/MacOS/Arc',
179
- };
180
- // Windows App Paths registry keys per browser. CreateProcess (used by WMI
181
- // Win32_Process.Create) does not honor App Paths the way ShellExecute/`start`
182
- // does, so the launch script resolves the real `.exe` path from this registry
183
- // key at runtime — covering both Program Files and Program Files (x86)
184
- // installs without hardcoding (or guessing) the location.
185
- const WIN_BROWSER_APPPATH = {
186
- chrome: 'chrome.exe',
187
- chromium: 'chrome.exe',
188
- brave: 'brave.exe',
189
- edge: 'msedge.exe',
190
- };
191
- /** Single-quote a string for embedding inside a PowerShell literal. */
192
- function psSingleQuote(s) {
193
- return "'" + s.replace(/'/g, "''") + "'";
194
- }
195
- /**
196
- * Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
197
- * UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
198
- * → Windows sshd → cmd.exe with zero escaping hazards (hand-quoted
199
- * `powershell -Command "…"` is fragile the moment a path or URL is involved).
200
- */
201
- export function encodePowerShell(script) {
202
- return `powershell -NoProfile -EncodedCommand ${encodePwshBase64(script)}`;
203
- }
204
- /**
205
- * The PowerShell that launches the browser on a Windows remote. Three hard
206
- * requirements shaped this:
207
- * 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
208
- * the session's job tree on disconnect, which reaps both `start /B` and
209
- * `Start-Process` children (verified against a real box). A scheduled
210
- * task runs under the Task Scheduler service, so the process survives
211
- * after we drop the ssh connection and reconnect over the CDP tunnel.
212
- * 2. The browser must run in the user's INTERACTIVE session. The previous
213
- * WMI `Win32_Process.Create` launch also survived disconnect, but it
214
- * spawned Edge in session 0 (the services session): the CDP socket bound
215
- * and accepted, yet the DevTools server never initialized — every
216
- * /json/version probe hung forever and DevToolsActivePort was never
217
- * written (verified against a real box, Edg/150). A scheduled task
218
- * registered and started by the logged-on user runs in that user's
219
- * interactive session, where DevTools comes up normally.
220
- * 3. A distinct `--user-data-dir` so a fresh instance bound to the debugging
221
- * port comes up even when the user already has Edge open.
222
- * CreateProcess ignores App Paths, so we resolve the real `.exe` from the
223
- * registry at runtime rather than relying on a bare `msedge` name. The task is
224
- * unregistered immediately after start — the browser keeps running; nothing
225
- * lingers in the scheduler.
226
- */
227
- export function buildWindowsLaunchScript(browserType, port, customBinary) {
228
- let exeStmts;
229
- if (customBinary) {
230
- exeStmts = [`$exe = ${psSingleQuote(customBinary)}`];
231
- }
232
- else if (browserType === 'custom') {
233
- throw new Error('browser: custom requires a binary path in the profile');
234
- }
235
- else {
236
- const exeKey = WIN_BROWSER_APPPATH[browserType];
237
- if (!exeKey)
238
- throw new Error(`Unknown browser type for windows remote: ${browserType}`);
239
- const appPath = `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeKey}`;
240
- // Per-user installs — the DEFAULT for Edge and Chrome — register App Paths
241
- // under HKCU, not HKLM, so an HKLM-only lookup silently missed them and the
242
- // launch failed. Resolve from HKLM first, then fall through to HKCU.
243
- // Windows PowerShell 5.1 (what `powershell.exe` is) has no `??`, so the
244
- // fallback is an explicit `if`. A missing exe `throw`s so the failure rides
245
- // out as a non-zero ssh exit (surfaced by ensureRemoteBrowser) instead of a
246
- // silent empty launch.
247
- exeStmts = [
248
- `$exe = (Get-ItemProperty 'HKLM:\\${appPath}' -EA SilentlyContinue).'(default)'`,
249
- `if (-not $exe) { $exe = (Get-ItemProperty 'HKCU:\\${appPath}' -EA SilentlyContinue).'(default)' }`,
250
- `if (-not $exe) { throw 'browser exe not found in HKLM/HKCU App Paths: ${exeKey}' }`,
251
- ];
252
- }
253
- // Keep the `--remote-allow-origins=http://127.0.0.1:${port}` literal in
254
- // source — a test asserts CDP is never opened to `*`.
255
- return [
256
- ...exeStmts,
257
- // First-run / default-browser modals block automation (same rationale as
258
- // the local launcher in chrome.ts), and the crash-restore bubble is worse
259
- // here: `browser stop` hard-kills the remote process, so the NEXT launch
260
- // against the same --user-data-dir marks the profile crashed and session
261
- // restore swaps the initial page target — closing its CDP websocket while
262
- // a command is pending (reproduced live: Page.captureScreenshot rejected
263
- // with "CDP connection closed" on every reused-profile launch).
264
- `$args = '--remote-debugging-port=${port}` +
265
- ` --remote-allow-origins=http://127.0.0.1:${port}` +
266
- ` --no-first-run --no-default-browser-check` +
267
- ` --hide-crash-restore-bubble --disable-session-crashed-bubble` +
268
- ` --disable-background-timer-throttling --user-data-dir="' + $env:TEMP + '\\agents-browser-${port}"'`,
269
- `$action = New-ScheduledTaskAction -Execute $exe -Argument $args`,
270
- `Register-ScheduledTask -TaskName 'agents-browser-${port}' -Action $action -Force | Out-Null`,
271
- `Start-ScheduledTask -TaskName 'agents-browser-${port}'`,
272
- `Unregister-ScheduledTask -TaskName 'agents-browser-${port}' -Confirm:$false`,
273
- ].join('; ');
274
- }
275
- /**
276
- * The PowerShell that kills whatever holds the CDP port on a Windows remote.
277
- * Tree-kill (`taskkill /T`), not `Stop-Process`: Chromium is multi-process, and
278
- * killing only the port-owning main process orphans its children. The orphans
279
- * keep the profile's SingletonLock, so every later launch against the same
280
- * `--user-data-dir` delegates to the zombie tree, exits, and never binds the
281
- * CDP port — `browser start --device` then fails until someone hand-cleans the
282
- * box (found live on win-mini by the #561 e2e suite).
283
- */
284
- export function buildWindowsKillScript(port) {
285
- return (`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue ` +
286
- `| ForEach-Object { taskkill /PID $_.OwningProcess /T /F 2>$null } | Out-Null`);
287
- }
288
- /**
289
- * Build the remote command that launches the browser detached with a CDP port.
290
- * POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
291
- * starts it via an interactive one-shot scheduled task (encoded PowerShell)
292
- * so it survives the ssh session AND serves CDP (session 0 never does).
293
- */
294
- export function buildLaunchCmd(remoteOs, browserType, port, customBinary) {
295
- if (remoteOs === 'windows') {
296
- return encodePowerShell(buildWindowsLaunchScript(browserType, port, customBinary));
297
- }
298
- const browserPath = customBinary ?? POSIX_BROWSER_PATHS[browserType];
299
- if (!browserPath) {
300
- if (browserType === 'custom') {
301
- throw new Error('browser: custom requires a binary path in the profile');
302
- }
303
- throw new Error(`Unknown browser type for posix remote: ${browserType}`);
304
- }
305
- return [
306
- shellQuote(browserPath),
307
- `--remote-debugging-port=${port}`,
308
- shellQuote(`--remote-allow-origins=http://127.0.0.1:${port}`),
309
- '--disable-background-timer-throttling',
310
- `--user-data-dir=/tmp/agents-browser-${port}`,
311
- '</dev/null >/dev/null 2>&1 &',
312
- ].join(' ');
313
- }
314
- /**
315
- * Build the remote command that kills whatever holds the CDP port.
316
- * POSIX uses `lsof`+`kill`; Windows uses encoded PowerShell
317
- * (Get-NetTCPConnection → Stop-Process).
318
- */
319
- export function buildKillCmd(remoteOs, port) {
320
- if (remoteOs === 'windows') {
321
- return encodePowerShell(buildWindowsKillScript(port));
322
- }
323
- return `pids=$(lsof -ti ${shellQuote(`:${port}`)} 2>/dev/null); [ -z "$pids" ] || kill -9 $pids 2>/dev/null || true`;
324
- }
325
- export async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
326
- // `user`/`host` derive from an ssh:// profile endpoint (git-tracked user
327
- // config). Validate the composed target before it reaches the raw `ssh` spawn:
328
- // an endpoint like `ssh://-Fattack@victim` would otherwise place `-Fattack` at
329
- // the target position, where OpenSSH parses it as `-F <file>` and an
330
- // attacker-supplied ssh config's ProxyCommand yields local code execution.
331
- assertValidSshTarget(`${user}@${host}`);
332
- const remoteCmd = buildLaunchCmd(remoteOs, browserType, port, customBinary);
333
- return new Promise((resolve, reject) => {
334
- // Capture stderr so a real launch failure (missing exe, auth denied, bad
335
- // CreateProcess) surfaces here as a clear error. Previously stdio was
336
- // ignored and this resolved on close regardless of exit code, so failures
337
- // were swallowed and only re-emerged 8s later as a generic
338
- // "SSH tunnel failed to establish".
339
- // Options BEFORE the target (matching `sshExec`): OpenSSH's BSD getopt on
340
- // macOS stops at the first non-option, so any `-o` after `user@host` would
341
- // be swallowed into the remote command instead of applied.
342
- const child = spawn('ssh', [
343
- ...sshConnectOpts(controlOpts()),
344
- `${user}@${host}`,
345
- remoteCmd,
346
- ], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
347
- let stderr = '';
348
- child.stderr?.on('data', (d) => {
349
- stderr += d.toString();
350
- });
351
- let settled = false;
352
- const timer = setTimeout(() => {
353
- if (settled)
354
- return;
355
- settled = true;
356
- // ssh is still running the backgrounding launch command past our window.
357
- // Killing the local ssh client does NOT reap the WMI/detached remote
358
- // browser, so treat this as launched and let waitForPort confirm.
359
- child.kill();
360
- resolve();
361
- }, 2000);
362
- child.on('close', (code) => {
363
- if (settled)
364
- return;
365
- settled = true;
366
- clearTimeout(timer);
367
- if (code && code !== 0) {
368
- const detail = stderr.trim();
369
- reject(new Error(`Remote browser launch failed on ${host} (ssh exit ${code})` +
370
- (detail ? `: ${detail}` : '')));
371
- }
372
- else {
373
- resolve();
374
- }
375
- });
376
- child.on('error', (err) => {
377
- if (settled)
378
- return;
379
- settled = true;
380
- clearTimeout(timer);
381
- reject(err);
382
- });
383
- });
384
- }
385
- /**
386
- * Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
387
- * task-launched (Windows) / detached (posix) browser process is not orphaned when
388
- * the ssh tunnel is torn down — killing only the tunnel left it running on
389
- * win-mini after every `browser stop`. Best-effort: never rejects.
390
- */
391
- export function killRemoteBrowser(user, host, remoteOs, port) {
392
- return runSSHCommand(user, host, buildKillCmd(remoteOs, port));
393
- }
394
- function runSSHCommand(user, host, cmd) {
395
- return new Promise((resolve, reject) => {
396
- // Reject an option-injecting target (e.g. a `-`-leading user from a crafted
397
- // ssh:// profile) before it reaches the raw `ssh` spawn. Reject rather than
398
- // throw synchronously so `killRemoteBrowser(...).catch()` stays best-effort.
399
- // See ensureRemoteBrowser.
400
- try {
401
- assertValidSshTarget(`${user}@${host}`);
402
- }
403
- catch (err) {
404
- reject(err);
405
- return;
406
- }
407
- // Reuse the shared hardened baseline so a hung TCP SYN to an unreachable
408
- // host is bounded by ConnectTimeout (the local 3s kill below only bounds a
409
- // connected-but-slow command, not the connect itself). Options precede the
410
- // target — see ensureRemoteBrowser.
411
- const child = spawn('ssh', [...sshConnectOpts(controlOpts()), `${user}@${host}`, cmd], {
412
- stdio: 'ignore',
413
- windowsHide: true,
414
- });
415
- child.on('close', () => resolve());
416
- child.on('error', () => resolve());
417
- setTimeout(() => {
418
- child.kill();
419
- resolve();
420
- }, 3000);
421
- });
422
- }
423
- function sleep(ms) {
424
- return new Promise((resolve) => setTimeout(resolve, ms));
425
- }
426
- /**
427
- * Identify whether a pid listening on our target local port is an SSH
428
- * tunnel WE would have spawned for `host:remotePort`. Used so that two
429
- * agents-browser invocations of the same SSH profile share a tunnel
430
- * rather than failing the second one with "port in use".
431
- *
432
- * Best-effort match against the ssh -L command line via `ps`. If we
433
- * can't read the cmd or the args don't look like ours, treat as not-ours.
434
- *
435
- * Windows has no `ps`, so the POSIX branch below always throws there and every
436
- * reuse check returned false — the second invocation to the same host:port then
437
- * failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
438
- * occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
439
- * (localPort === remotePort in the caller), so the pid holding that port is our
440
- * ssh.exe. tasklist only exposes the image name, not the full command line, so
441
- * we can't match host/remotePort as tightly as the POSIX branch — an ssh
442
- * process on our exact local port is our tunnel in practice.
443
- */
444
- export function isOwnTunnel(pid, host, remotePort) {
445
- if (process.platform === 'win32') {
446
- const occupant = getPortOccupant(remotePort);
447
- if (!occupant || occupant.pid !== pid)
448
- return false;
449
- return occupant.command.toLowerCase().startsWith('ssh');
450
- }
451
- try {
452
- const out = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
453
- encoding: 'utf-8',
454
- stdio: ['ignore', 'pipe', 'ignore'],
455
- }).toString().trim();
456
- if (!out.startsWith('ssh'))
457
- return false;
458
- if (!out.includes(host))
459
- return false;
460
- if (!out.includes(`:${remotePort}`) && !out.includes(`:127.0.0.1:${remotePort}`))
461
- return false;
462
- return true;
463
- }
464
- catch {
465
- return false;
466
- }
467
- }
@@ -1,3 +0,0 @@
1
- import type { CDPClient } from './cdp.js';
2
- import type { RefNode } from './refs.js';
3
- export declare function typeEditorText(cdp: CDPClient, sessionId: string, node: RefNode, text: string, clear?: boolean): Promise<void>;
@@ -1,50 +0,0 @@
1
- const BEFOREINPUT_INSERT_FN = `(function(text) {
2
- this.focus();
3
- var sel = window.getSelection();
4
- var range = document.createRange();
5
- range.selectNodeContents(this);
6
- range.collapse(false);
7
- sel.removeAllRanges();
8
- sel.addRange(range);
9
- this.dispatchEvent(new InputEvent('beforeinput', {
10
- inputType: 'insertText',
11
- data: text,
12
- bubbles: true,
13
- cancelable: true,
14
- composed: true,
15
- }));
16
- })`;
17
- const BEFOREINPUT_CLEAR_FN = `(function() {
18
- this.focus();
19
- var sel = window.getSelection();
20
- var range = document.createRange();
21
- range.selectNodeContents(this);
22
- sel.removeAllRanges();
23
- sel.addRange(range);
24
- this.dispatchEvent(new InputEvent('beforeinput', {
25
- inputType: 'deleteContentBackward',
26
- bubbles: true,
27
- cancelable: true,
28
- composed: true,
29
- }));
30
- })`;
31
- const TRIX_INSERT_FN = `(function(text) { this.editor.insertString(text); })`;
32
- export async function typeEditorText(cdp, sessionId, node, text, clear = false) {
33
- const { object } = await cdp.send('DOM.resolveNode', { backendNodeId: node.backendNodeId }, sessionId);
34
- if (!object.objectId)
35
- throw new Error(`Could not resolve DOM node for ref ${node.ref}`);
36
- const objectId = object.objectId;
37
- try {
38
- if (node.editor === 'trix') {
39
- await cdp.send('Runtime.callFunctionOn', { objectId, functionDeclaration: TRIX_INSERT_FN, arguments: [{ value: text }], returnByValue: true }, sessionId);
40
- return;
41
- }
42
- if (clear) {
43
- await cdp.send('Runtime.callFunctionOn', { objectId, functionDeclaration: BEFOREINPUT_CLEAR_FN, arguments: [], returnByValue: true }, sessionId);
44
- }
45
- await cdp.send('Runtime.callFunctionOn', { objectId, functionDeclaration: BEFOREINPUT_INSERT_FN, arguments: [{ value: text }], returnByValue: true }, sessionId);
46
- }
47
- finally {
48
- await cdp.send('Runtime.releaseObject', { objectId }, sessionId);
49
- }
50
- }
@@ -1,12 +0,0 @@
1
- type Platform = NodeJS.Platform;
2
- export declare function ffmpegManualCommand(platform: Platform, osRelease?: string): string;
3
- export declare function resolveFfmpeg(options?: {
4
- env?: NodeJS.ProcessEnv;
5
- home?: string;
6
- platform?: Platform;
7
- osRelease?: string;
8
- /** Override canonical executable directories, primarily for isolated verification. */
9
- executableDirs?: string[];
10
- }): Promise<string>;
11
- export declare function resetFfmpegResolverForTest(): void;
12
- export {};