@phnx-labs/agents-cli 1.22.117 → 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 (248) hide show
  1. package/CHANGELOG.md +235 -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.js +53 -1
  9. package/dist/commands/repo.js +13 -49
  10. package/dist/commands/route.js +5 -5
  11. package/dist/commands/routines.js +39 -12
  12. package/dist/commands/sessions-render.d.ts +1 -1
  13. package/dist/commands/sessions-render.js +0 -1
  14. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  15. package/dist/commands/sessions.test-fixture.js +0 -37
  16. package/dist/commands/setup-browser.d.ts +17 -12
  17. package/dist/commands/setup-browser.js +52 -87
  18. package/dist/commands/setup-preferences.d.ts +0 -17
  19. package/dist/commands/setup-preferences.js +18 -49
  20. package/dist/commands/setup.js +9 -9
  21. package/dist/commands/ssh.js +10 -1
  22. package/dist/commands/sync.js +6 -44
  23. package/dist/commands/teams.js +0 -1
  24. package/dist/commands/update.d.ts +19 -0
  25. package/dist/commands/update.js +34 -6
  26. package/dist/commands/view.d.ts +12 -1
  27. package/dist/commands/view.js +47 -2
  28. package/dist/index.js +107 -26
  29. package/dist/lib/account-capabilities.js +0 -2
  30. package/dist/lib/account-catalog.d.ts +38 -0
  31. package/dist/lib/account-catalog.js +110 -26
  32. package/dist/lib/account-provider-registry.js +1 -1
  33. package/dist/lib/account-registry.d.ts +1 -1
  34. package/dist/lib/account-registry.js +36 -2
  35. package/dist/lib/accounting/rotate.js +22 -8
  36. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  37. package/dist/lib/accounting/usage-ingest.js +115 -45
  38. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  39. package/dist/lib/accounting/usage-sync.js +227 -66
  40. package/dist/lib/accounting/usage.d.ts +16 -3
  41. package/dist/lib/accounting/usage.js +26 -8
  42. package/dist/lib/accounts/slots.d.ts +8 -0
  43. package/dist/lib/accounts/slots.js +17 -0
  44. package/dist/lib/add-dir.js +0 -1
  45. package/dist/lib/agent-cli-commands.js +0 -1
  46. package/dist/lib/agent-spec/agents.d.ts +6 -7
  47. package/dist/lib/agent-spec/agents.js +6 -60
  48. package/dist/lib/auth-health.d.ts +69 -1
  49. package/dist/lib/auth-health.js +133 -4
  50. package/dist/lib/browser/context.d.ts +82 -0
  51. package/dist/lib/browser/context.js +88 -0
  52. package/dist/lib/browser/paths.d.ts +25 -0
  53. package/dist/lib/browser/paths.js +51 -0
  54. package/dist/lib/browser/record.d.ts +38 -0
  55. package/dist/lib/browser/record.js +57 -0
  56. package/dist/lib/browser/sessions-list.js +1 -2
  57. package/dist/lib/browser-client.d.ts +158 -0
  58. package/dist/lib/browser-client.js +234 -0
  59. package/dist/lib/channels/owner-sink.d.ts +1 -18
  60. package/dist/lib/channels/owner-sink.js +29 -82
  61. package/dist/lib/channels/providers/rush.d.ts +7 -7
  62. package/dist/lib/channels/providers/rush.js +127 -57
  63. package/dist/lib/claude-statusline.d.ts +1 -1
  64. package/dist/lib/claude-statusline.js +15 -3
  65. package/dist/lib/commands.js +1 -1
  66. package/dist/lib/crabbox/lease.js +1 -1
  67. package/dist/lib/crabbox/runtimes.js +1 -3
  68. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  69. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  70. package/dist/lib/daemon/auth-sync-service.js +37 -43
  71. package/dist/lib/daemon/daemon.d.ts +25 -14
  72. package/dist/lib/daemon/daemon.js +92 -154
  73. package/dist/lib/daemon/runner.d.ts +2 -2
  74. package/dist/lib/daemon/runner.js +32 -9
  75. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  76. package/dist/lib/daemon/self-update-service.js +40 -36
  77. package/dist/lib/daemon/service.d.ts +12 -8
  78. package/dist/lib/daemon/supervisor.d.ts +65 -54
  79. package/dist/lib/daemon/supervisor.js +136 -171
  80. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  81. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  82. package/dist/lib/daemon/usage-sync-service.js +46 -78
  83. package/dist/lib/daemon-health.d.ts +22 -5
  84. package/dist/lib/daemon-health.js +47 -3
  85. package/dist/lib/daemon-services.d.ts +1 -1
  86. package/dist/lib/daemon-services.js +0 -10
  87. package/dist/lib/daemon-ticks.js +4 -2
  88. package/dist/lib/devices/doctor-findings.js +8 -11
  89. package/dist/lib/devices/harness-inventory.js +14 -5
  90. package/dist/lib/exec-account-home.js +5 -0
  91. package/dist/lib/exec.js +0 -11
  92. package/dist/lib/feed/events.js +0 -1
  93. package/dist/lib/feed/tool-activity.js +5 -2
  94. package/dist/lib/fleet-shared-state.d.ts +45 -0
  95. package/dist/lib/fleet-shared-state.js +78 -9
  96. package/dist/lib/fs-atomic.js +25 -0
  97. package/dist/lib/harness-auth-capabilities.js +0 -1
  98. package/dist/lib/helper-versions.js +1 -1
  99. package/dist/lib/hooks/cache.d.ts +7 -0
  100. package/dist/lib/hooks/cache.js +27 -2
  101. package/dist/lib/hooks/install.js +7 -1
  102. package/dist/lib/hosts/ready.d.ts +38 -8
  103. package/dist/lib/hosts/ready.js +62 -14
  104. package/dist/lib/installations/active-check.d.ts +27 -0
  105. package/dist/lib/installations/active-check.js +61 -0
  106. package/dist/lib/installations/migrate.js +6 -3
  107. package/dist/lib/installations/shims.js +9 -7
  108. package/dist/lib/installations/update.js +21 -15
  109. package/dist/lib/installations/versions.js +12 -1
  110. package/dist/lib/manifest.js +1 -1
  111. package/dist/lib/models.js +0 -2
  112. package/dist/lib/open-url.js +28 -54
  113. package/dist/lib/profiles.js +0 -2
  114. package/dist/lib/refresh.js +1 -1
  115. package/dist/lib/routine-readiness.d.ts +35 -0
  116. package/dist/lib/routine-readiness.js +74 -16
  117. package/dist/lib/rules/compile.js +6 -0
  118. package/dist/lib/sandbox.js +0 -2
  119. package/dist/lib/secrets-policy.d.ts +89 -11
  120. package/dist/lib/secrets-policy.js +175 -28
  121. package/dist/lib/session/db.d.ts +1 -1
  122. package/dist/lib/session/db.js +65 -18
  123. package/dist/lib/session/discover.js +1 -283
  124. package/dist/lib/session/mirror.d.ts +4 -3
  125. package/dist/lib/session/mirror.js +4 -3
  126. package/dist/lib/session/parse.d.ts +0 -2
  127. package/dist/lib/session/parse.js +1 -138
  128. package/dist/lib/session/throughput.d.ts +1 -3
  129. package/dist/lib/session/throughput.js +0 -20
  130. package/dist/lib/session/types.d.ts +2 -2
  131. package/dist/lib/session/types.js +2 -2
  132. package/dist/lib/sessions-client.d.ts +60 -1
  133. package/dist/lib/sessions-client.js +227 -4
  134. package/dist/lib/setup-tool-status.js +5 -10
  135. package/dist/lib/signin-badge.d.ts +3 -2
  136. package/dist/lib/signin-badge.js +6 -3
  137. package/dist/lib/smart-launch.js +4 -1
  138. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  139. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  140. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  141. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  142. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  143. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  144. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  147. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  148. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  149. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  150. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  151. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  152. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  153. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  154. package/dist/lib/staleness/writers/sources.js +5 -0
  155. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  156. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  157. package/dist/lib/state.d.ts +6 -4
  158. package/dist/lib/state.js +7 -17
  159. package/dist/lib/teams/parsers.d.ts +1 -1
  160. package/dist/lib/teams/parsers.js +1 -136
  161. package/dist/lib/teams/placement-probe.js +50 -15
  162. package/dist/lib/teams/scheduler.d.ts +7 -0
  163. package/dist/lib/types.d.ts +10 -1
  164. package/dist/lib/types.js +1 -1
  165. package/dist/lib/view-types.d.ts +32 -0
  166. package/dist/lib/watchdog/read.js +0 -1
  167. package/dist/lib/watchdog/rotate.js +1 -2
  168. package/dist/lib/watchdog/runner.js +2 -2
  169. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  170. package/dist/lib/watchdog/watchdogTail.js +0 -31
  171. package/package.json +1 -1
  172. package/scripts/postinstall.js +8 -8
  173. package/dist/browser.d.ts +0 -2
  174. package/dist/browser.js +0 -17
  175. package/dist/commands/browser-picker.d.ts +0 -18
  176. package/dist/commands/browser-picker.js +0 -97
  177. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  178. package/dist/lib/browser/arc-discovery.js +0 -197
  179. package/dist/lib/browser/arc-dom.d.ts +0 -14
  180. package/dist/lib/browser/arc-dom.js +0 -121
  181. package/dist/lib/browser/caller-identity.d.ts +0 -40
  182. package/dist/lib/browser/caller-identity.js +0 -175
  183. package/dist/lib/browser/cdp.d.ts +0 -51
  184. package/dist/lib/browser/cdp.js +0 -254
  185. package/dist/lib/browser/chrome.d.ts +0 -141
  186. package/dist/lib/browser/chrome.js +0 -704
  187. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  188. package/dist/lib/browser/chromium-discovery.js +0 -96
  189. package/dist/lib/browser/devices.d.ts +0 -34
  190. package/dist/lib/browser/devices.js +0 -61
  191. package/dist/lib/browser/domain-skills.d.ts +0 -71
  192. package/dist/lib/browser/domain-skills.js +0 -195
  193. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  194. package/dist/lib/browser/drivers/arc.js +0 -276
  195. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  196. package/dist/lib/browser/drivers/firefox.js +0 -377
  197. package/dist/lib/browser/drivers/local.d.ts +0 -55
  198. package/dist/lib/browser/drivers/local.js +0 -266
  199. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  200. package/dist/lib/browser/drivers/ssh.js +0 -467
  201. package/dist/lib/browser/editor.d.ts +0 -3
  202. package/dist/lib/browser/editor.js +0 -50
  203. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  204. package/dist/lib/browser/ffmpeg.js +0 -184
  205. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  206. package/dist/lib/browser/firefox-discovery.js +0 -162
  207. package/dist/lib/browser/har.d.ts +0 -85
  208. package/dist/lib/browser/har.js +0 -77
  209. package/dist/lib/browser/hygiene.d.ts +0 -97
  210. package/dist/lib/browser/hygiene.js +0 -153
  211. package/dist/lib/browser/index.d.ts +0 -5
  212. package/dist/lib/browser/index.js +0 -5
  213. package/dist/lib/browser/input.d.ts +0 -7
  214. package/dist/lib/browser/input.js +0 -92
  215. package/dist/lib/browser/ipc.d.ts +0 -204
  216. package/dist/lib/browser/ipc.js +0 -1453
  217. package/dist/lib/browser/login-detection.d.ts +0 -87
  218. package/dist/lib/browser/login-detection.js +0 -274
  219. package/dist/lib/browser/profiles.d.ts +0 -422
  220. package/dist/lib/browser/profiles.js +0 -1157
  221. package/dist/lib/browser/refs.d.ts +0 -89
  222. package/dist/lib/browser/refs.js +0 -191
  223. package/dist/lib/browser/registry.d.ts +0 -70
  224. package/dist/lib/browser/registry.js +0 -252
  225. package/dist/lib/browser/remote-control.d.ts +0 -59
  226. package/dist/lib/browser/remote-control.js +0 -85
  227. package/dist/lib/browser/resolve-target.d.ts +0 -66
  228. package/dist/lib/browser/resolve-target.js +0 -264
  229. package/dist/lib/browser/runtime-state.d.ts +0 -291
  230. package/dist/lib/browser/runtime-state.js +0 -584
  231. package/dist/lib/browser/secret-ref.d.ts +0 -10
  232. package/dist/lib/browser/secret-ref.js +0 -14
  233. package/dist/lib/browser/service.d.ts +0 -812
  234. package/dist/lib/browser/service.js +0 -4693
  235. package/dist/lib/browser/stream.d.ts +0 -17
  236. package/dist/lib/browser/stream.js +0 -72
  237. package/dist/lib/browser/task-index.d.ts +0 -78
  238. package/dist/lib/browser/task-index.js +0 -199
  239. package/dist/lib/browser/types.d.ts +0 -647
  240. package/dist/lib/browser/types.js +0 -61
  241. package/dist/lib/browser/upload.d.ts +0 -29
  242. package/dist/lib/browser/upload.js +0 -298
  243. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  244. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  245. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  246. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  247. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  248. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,266 +0,0 @@
1
- import * as net from 'net';
2
- import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
3
- import { launchBrowser, getPortOccupant, getProcessUserDataDir, storeOccupant, storeRelaunchCommand } from '../chrome.js';
4
- import { parseEndpointUrl, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir } from '../profiles.js';
5
- /**
6
- * Cheap TCP-level "is something bound here?" probe. Used as a fallback when
7
- * `getPortOccupant()` (lsof-based) misses the process — Comet and other
8
- * Electron apps sometimes hold a TCP socket that lsof's `-sTCP:LISTEN` filter
9
- * doesn't report. If anything ACKs the connection, we treat the port as taken
10
- * and surface a friendly error instead of silently auto-launching a fresh
11
- * browser that would then conflict.
12
- */
13
- async function probeTcpBound(port, host = '127.0.0.1', timeoutMs = 500) {
14
- return new Promise((resolve) => {
15
- const sock = net.createConnection({ port, host });
16
- const cleanup = () => {
17
- sock.removeAllListeners();
18
- sock.destroy();
19
- };
20
- const timer = setTimeout(() => { cleanup(); resolve(false); }, timeoutMs);
21
- sock.once('connect', () => { clearTimeout(timer); cleanup(); resolve(true); });
22
- sock.once('error', () => { clearTimeout(timer); cleanup(); resolve(false); });
23
- });
24
- }
25
- /**
26
- * Local-port listeners we refuse to attach through. These forward CDP traffic
27
- * to a remote host — silently using them would let a `cdp://127.0.0.1:N`
28
- * profile drive a browser on a different machine without the caller realizing.
29
- */
30
- const TUNNEL_PROCESS_NAMES = new Set(['ssh', 'autossh', 'mosh-client', 'socat']);
31
- function isTunnelProcess(command) {
32
- return TUNNEL_PROCESS_NAMES.has(command.toLowerCase());
33
- }
34
- /**
35
- * Arc is single-instance: relaunching the Arc binary with a fresh
36
- * `--user-data-dir` does not create a second debuggable process — macOS routes
37
- * the launch to the running Arc, which was started with no debug port, so the
38
- * spawn produces a stray window and no CDP endpoint (issue #2779). agents
39
- * browser therefore ATTACHES to the user's running Arc and never launches its
40
- * own. When the running Arc exposes no CDP endpoint on the profile's port, we
41
- * fail loud with the one relaunch that fixes it rather than silently spawning a
42
- * duplicate. Exported so the contract is unit-testable without a real Arc.
43
- */
44
- export function arcAttachRequiredError(profileName, port) {
45
- return new Error(`Arc is not exposing a CDP endpoint on cdp://127.0.0.1:${port} for profile ` +
46
- `"${profileName}". Arc is single-instance — agents browser attaches to your ` +
47
- `RUNNING Arc and never launches a second one, so it will not start an isolated ` +
48
- `Arc for you. Quit Arc, then relaunch it with remote debugging on this port:\n` +
49
- ` open -a Arc --args --remote-debugging-port=${port}\n` +
50
- `and retry. The port must match the profile's endpoint (\`agents browser profiles list\`).`);
51
- }
52
- /**
53
- * The loud error an ATTACH-ONLY profile raises when nothing debuggable is on its
54
- * port (PHNX-3967). Same contract as {@link arcAttachRequiredError}, generalized
55
- * to any browser: agents attach to a browser the user already started and NEVER
56
- * spawn a rival window, so instead of falling through to `launchBrowser` (which
57
- * would produce the second, logged-out dock tile this ticket set out to end) we
58
- * fail loud with the exact relaunch that makes the canonical instance
59
- * attachable — pinned to the profile's DURABLE `--user-data-dir` so the one-time
60
- * sign-in persists. Exported so the contract is unit-testable without a browser.
61
- */
62
- export function attachOnlyRequiredError(profile, port) {
63
- if (profile.browser === 'arc')
64
- return arcAttachRequiredError(profile.name, port);
65
- const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
66
- const dataDir = resolveProfileDataDir(profile);
67
- return new Error(`Profile "${profile.name}" is attach-only and nothing is serving the Chrome ` +
68
- `DevTools Protocol on cdp://127.0.0.1:${port}. agents browser attaches to the ` +
69
- `${app} you already started and never launches a second one, so it will not spawn ` +
70
- `a rival window (that is the duplicate, logged-out tile this profile exists to ` +
71
- `prevent). Launch the canonical ${app} with remote debugging on its durable data dir:\n` +
72
- ` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${dataDir}\n` +
73
- `and retry. Keep signing in once in that window — the data dir is durable, so the ` +
74
- `login survives quit+relaunch.`);
75
- }
76
- /**
77
- * The loud error an attach-only profile raises when a browser IS serving CDP on
78
- * its port but is a FOREIGN instance — its `--user-data-dir` is not the profile's
79
- * durable dir (PHNX-3967). This closes the port-squat: a logged-out `/tmp/...`
80
- * Comet answering CDP on the canonical port passes the browser-FAMILY identity
81
- * check, so without an ownership check agents would drive it as if it were the
82
- * credentialed browser. Rather than adopt the squatter we reject it and name the
83
- * fix. Exported for unit testing.
84
- */
85
- export function foreignInstanceError(profile, port, runningDataDir, pid) {
86
- const expected = resolveProfileDataDir(profile);
87
- const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
88
- return new Error(`Attach-only ownership check failed for profile "${profile.name}" on ` +
89
- `cdp://127.0.0.1:${port}: a ${app} is serving CDP there (pid ${pid}) but it is ` +
90
- `running a FOREIGN user-data-dir\n` +
91
- ` running: ${runningDataDir}\n` +
92
- ` expected: ${expected}\n` +
93
- `so it is not this profile's credentialed browser. agents will not drive it. Close ` +
94
- `that instance (\`kill ${pid}\`), then relaunch the canonical ${app}:\n` +
95
- ` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${expected}`);
96
- }
97
- /**
98
- * The loud error a profile pinned to the owner's own store (PHNX-4042) raises
99
- * when a browser already holds that store without a debug port. Launching on it
100
- * would not open a second browser: Chromium hands the arguments to the running
101
- * instance and exits, the port never binds, and the agent would wait out a
102
- * timeout while the owner sees a stray window. Exported for unit testing.
103
- */
104
- export function storeInUseError(profile, port, pid) {
105
- const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
106
- const dataDir = resolveProfileDataDir(profile);
107
- return new Error(`Profile "${profile.name}" is pinned to ${dataDir}, and a ${app} (pid ${pid}) already has that ` +
108
- `store open without remote debugging, so nothing serves the Chrome DevTools Protocol on ` +
109
- `cdp://127.0.0.1:${port}. agents browser will not launch a second ${app} on the same store. ` +
110
- `Quit that ${app}, then relaunch it with remote debugging:\n` +
111
- ` ${storeRelaunchCommand(profile.browser, port, dataDir, profile.profileDirectory)}\n` +
112
- `and retry. Your logins stay: the store is the one you already use.`);
113
- }
114
- /**
115
- * Prefix every ownership-rejection message carries, so `connectLocal`'s catch can
116
- * re-throw it verbatim instead of misreading it as "attach failed, launch fresh".
117
- */
118
- const OWNERSHIP_REJECTION_PREFIX = 'Attach-only ownership check failed';
119
- /**
120
- * Verify the browser serving CDP on `port` belongs to this attach-only profile,
121
- * by comparing its live `--user-data-dir` to the profile's durable dir
122
- * (PHNX-3967). No-op for a `launch`-policy profile. When an occupant IS present
123
- * but its data dir can't be read, fail LOUD rather than open: a canonical Comet
124
- * this profile is meant to attach to was launched by the user with the durable
125
- * `--user-data-dir` and is readable via `ps`, so an unreadable occupant is the
126
- * suspicious case, not a legitimate one — driving it would be the exact
127
- * port-squat this guard exists to stop.
128
- */
129
- function verifyEndpointOwnership(profile, port) {
130
- // Attach-only profiles and profiles pinned to the owner's own store
131
- // (PHNX-4042) both name the exact user-data dir a running instance must have.
132
- if (!isAttachOnlyProfile(profile) && !profile.userDataDir)
133
- return;
134
- // Arc is the user's single running instance under its own default data dir; it
135
- // has no managed durable dir to compare against and no /tmp-squat vector.
136
- if (profile.browser === 'arc')
137
- return;
138
- const occupant = getPortOccupant(port);
139
- if (!occupant)
140
- return;
141
- const expected = resolveProfileDataDir(profile);
142
- const runningDataDir = getProcessUserDataDir(occupant.pid);
143
- if (!runningDataDir) {
144
- throw new Error(`${OWNERSHIP_REJECTION_PREFIX} for profile "${profile.name}" on cdp://127.0.0.1:${port}: ` +
145
- `a process (pid ${occupant.pid}) is serving CDP there but its --user-data-dir could not be ` +
146
- `read, so ownership can't be confirmed. Refusing to attach to an unverified instance. If ` +
147
- `this is your canonical browser, relaunch it with --user-data-dir=${expected} so the ` +
148
- `attach-only guard can verify it.`);
149
- }
150
- if (normalizeDataDir(runningDataDir) !== normalizeDataDir(expected)) {
151
- throw foreignInstanceError(profile, port, runningDataDir, occupant.pid);
152
- }
153
- }
154
- export async function connectLocal(endpoint, profile,
155
- /**
156
- * Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
157
- * pid, and port files are stored under. Separate from `profile.name`, which
158
- * stays the bare user-facing name and appears only in messages (RUSH-2709).
159
- */
160
- key) {
161
- const url = new URL(endpoint);
162
- if (url.protocol !== 'cdp:') {
163
- throw new Error(`Invalid local endpoint: ${endpoint}`);
164
- }
165
- // Share the parser with the SSH driver and the collision-detection code
166
- // path so `cdp://host:N` and `cdp://host?port=N` behave identically.
167
- const parsed = parseEndpointUrl(endpoint);
168
- const port = parsed?.port ?? 9222;
169
- // Refuse to attach through an SSH tunnel before we even try to speak CDP.
170
- // `verifyBrowserIdentity` only inspects what comes back over the wire — it
171
- // can't tell whether the browser actually lives on this machine or on the
172
- // far end of an `ssh -L` tunnel. A stale tunnel from a prior session
173
- // (common when an SSH-driven profile is deleted before the daemon exits)
174
- // will silently hijack any "local" profile bound to the same port.
175
- const preOccupant = getPortOccupant(port);
176
- if (preOccupant && isTunnelProcess(preOccupant.command)) {
177
- throw new Error(`Port ${port} is held by ${preOccupant.command} (pid ${preOccupant.pid}), an SSH ` +
178
- `tunnel forwarding to a remote host. Profile "${profile.name}" is configured as ` +
179
- `local (${endpoint}) but traffic would round-trip to another machine. Either kill ` +
180
- `the tunnel (\`kill ${preOccupant.pid}\`) and retry, or switch the profile to an ` +
181
- `ssh:// endpoint to drive the remote browser explicitly.`);
182
- }
183
- try {
184
- const { wsUrl, browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
185
- verifyBrowserIdentity(browser, profile.browser, port);
186
- // Ownership beyond browser FAMILY (PHNX-3967): a foreign /tmp Comet answers
187
- // CDP on the canonical port and passes verifyBrowserIdentity, so before we
188
- // adopt the endpoint confirm its --user-data-dir is this profile's durable
189
- // dir. Rejects the port-squatter loudly instead of driving it.
190
- verifyEndpointOwnership(profile, port);
191
- const cdp = new CDPClient();
192
- await cdp.connect(wsUrl);
193
- return { cdp, port, pid: 0 };
194
- }
195
- catch (err) {
196
- if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
197
- throw err;
198
- }
199
- // An ownership rejection is a definitive answer — never fall through to a
200
- // fresh launch or a generic port message. Re-throw it verbatim.
201
- if (err instanceof Error && err.message.startsWith(OWNERSHIP_REJECTION_PREFIX)) {
202
- throw err;
203
- }
204
- // An attach-only profile reached the catch, so the attach above failed:
205
- // nothing debuggable is on this port. Never fall through to launchBrowser —
206
- // that would spawn the duplicate/stray-window this ticket set out to end
207
- // (Arc: #2779, PHNX-2399; Comet: PHNX-3967). Fail loud with the relaunch that
208
- // makes the canonical instance attachable.
209
- if (isAttachOnlyProfile(profile)) {
210
- throw attachOnlyRequiredError(profile, port);
211
- }
212
- // Distinguish "nothing listening on this port" (fine to launch fresh) from
213
- // "something is listening but it's not a debuggable browser" (bail loudly —
214
- // silently launching on a different port leads to confusing `pid 0` and
215
- // `CDP connection not open` errors downstream).
216
- const occupant = getPortOccupant(port);
217
- if (occupant) {
218
- throw new Error(`Port ${port} is occupied by ${occupant.command} (pid ${occupant.pid}) but is ` +
219
- `not serving the Chrome DevTools Protocol. Either stop that process ` +
220
- `(\`kill ${occupant.pid}\`) or restart it with \`--remote-debugging-port=${port}\` ` +
221
- `so profile "${profile.name}" can attach.`);
222
- }
223
- // lsof-based detection misses some Electron-family processes (Comet, custom
224
- // chrome wrappers). Cheap TCP probe as a safety net: if something ACKs a
225
- // connect, the port is bound — bail loudly with the profile name + endpoint
226
- // rather than silently launching a duplicate browser.
227
- if (await probeTcpBound(port)) {
228
- throw new Error(`Profile "${profile.name}" is configured for cdp://127.0.0.1:${port}, ` +
229
- `but something is already bound to that port without serving the Chrome ` +
230
- `DevTools Protocol. If that's your browser running without remote debugging, ` +
231
- `quit it and relaunch with \`--remote-debugging-port=${port}\`. Otherwise, ` +
232
- `update the profile to a free port (\`agents browser profiles list\`).`);
233
- }
234
- // A store-pinned profile launches on the owner's store only while nothing
235
- // holds it; a browser already open there without a port must be relaunched
236
- // by the owner, never doubled (PHNX-4042).
237
- if (profile.userDataDir) {
238
- const holder = storeOccupant(profile.userDataDir);
239
- if (holder)
240
- throw storeInUseError(profile, port, holder.pid);
241
- }
242
- const newPort = port;
243
- const chromeOpts = {
244
- ...profile.chrome,
245
- viewport: profile.viewport,
246
- // A discovered native profile launches the owner's browser on its own
247
- // store: one window for the owner and agents, logins intact (PHNX-4042).
248
- ...(profile.userDataDir
249
- ? { userDataDir: profile.userDataDir, profileDirectory: profile.profileDirectory }
250
- : {}),
251
- };
252
- let launched;
253
- try {
254
- launched = await launchBrowser(key, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
255
- }
256
- catch (launchErr) {
257
- const reason = launchErr instanceof Error ? launchErr.message : String(launchErr);
258
- throw new Error(`Could not start ${profile.browser} for profile "${profile.name}" on port ${newPort}: ${reason}. ` +
259
- `Check that the browser binary is installed (\`agents browser profiles list\`) and ` +
260
- `that no other process is holding the port.`);
261
- }
262
- const cdp = new CDPClient();
263
- await cdp.connect(launched.wsUrl);
264
- return { cdp, port: newPort, pid: launched.pid };
265
- }
266
- }
@@ -1,120 +0,0 @@
1
- import { CDPClient } from '../cdp.js';
2
- import type { BrowserProfile, ConnectionKey } from '../types.js';
3
- import { shellQuote } from '../../ssh-exec.js';
4
- export { shellQuote };
5
- export interface SSHConnection {
6
- cdp: CDPClient;
7
- port: number;
8
- pid: number;
9
- cleanup: () => void;
10
- }
11
- /**
12
- * Which shell dialect the *remote* host speaks. Selected per-endpoint via the
13
- * `&os=windows` query param on an `ssh://` target. POSIX is the default — the
14
- * historical behavior for macOS/Linux remotes. Windows remotes run OpenSSH
15
- * Server with cmd.exe as the default shell, so the launch/teardown command
16
- * strings differ (no `&` backgrounding, no `lsof`, `.exe` instead of `.app`).
17
- */
18
- type RemoteOs = 'windows' | 'posix';
19
- interface SSHConnectOptions {
20
- /**
21
- * Attach to a browser another device already declared (identity-bearing or
22
- * a fungible name we don't declare here). Do not launch a fresh remote
23
- * chrome-data, and do not kill that browser when the tunnel closes — it
24
- * holds the logins the caller is trying to reach.
25
- */
26
- persistRemote?: boolean;
27
- }
28
- /** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
29
- export declare function persistRemoteLifecycle(persistRemote: boolean): {
30
- launchRemote: boolean;
31
- killOnCleanup: boolean;
32
- };
33
- export declare function connectSSH(endpoint: string, profile: BrowserProfile,
34
- /**
35
- * Runtime key (`<profile>@<device>`) this connection is stored under. It
36
- * keys the tunnel's runtime record; `profile.name` is the bare, user-facing
37
- * name and appears only in messages (RUSH-2709).
38
- */
39
- key: ConnectionKey, opts?: SSHConnectOptions): Promise<SSHConnection>;
40
- /**
41
- * Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
42
- * UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
43
- * → Windows sshd → cmd.exe with zero escaping hazards (hand-quoted
44
- * `powershell -Command "…"` is fragile the moment a path or URL is involved).
45
- */
46
- export declare function encodePowerShell(script: string): string;
47
- /**
48
- * The PowerShell that launches the browser on a Windows remote. Three hard
49
- * requirements shaped this:
50
- * 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
51
- * the session's job tree on disconnect, which reaps both `start /B` and
52
- * `Start-Process` children (verified against a real box). A scheduled
53
- * task runs under the Task Scheduler service, so the process survives
54
- * after we drop the ssh connection and reconnect over the CDP tunnel.
55
- * 2. The browser must run in the user's INTERACTIVE session. The previous
56
- * WMI `Win32_Process.Create` launch also survived disconnect, but it
57
- * spawned Edge in session 0 (the services session): the CDP socket bound
58
- * and accepted, yet the DevTools server never initialized — every
59
- * /json/version probe hung forever and DevToolsActivePort was never
60
- * written (verified against a real box, Edg/150). A scheduled task
61
- * registered and started by the logged-on user runs in that user's
62
- * interactive session, where DevTools comes up normally.
63
- * 3. A distinct `--user-data-dir` so a fresh instance bound to the debugging
64
- * port comes up even when the user already has Edge open.
65
- * CreateProcess ignores App Paths, so we resolve the real `.exe` from the
66
- * registry at runtime rather than relying on a bare `msedge` name. The task is
67
- * unregistered immediately after start — the browser keeps running; nothing
68
- * lingers in the scheduler.
69
- */
70
- export declare function buildWindowsLaunchScript(browserType: string, port: number, customBinary?: string): string;
71
- /**
72
- * The PowerShell that kills whatever holds the CDP port on a Windows remote.
73
- * Tree-kill (`taskkill /T`), not `Stop-Process`: Chromium is multi-process, and
74
- * killing only the port-owning main process orphans its children. The orphans
75
- * keep the profile's SingletonLock, so every later launch against the same
76
- * `--user-data-dir` delegates to the zombie tree, exits, and never binds the
77
- * CDP port — `browser start --device` then fails until someone hand-cleans the
78
- * box (found live on win-mini by the #561 e2e suite).
79
- */
80
- export declare function buildWindowsKillScript(port: number): string;
81
- /**
82
- * Build the remote command that launches the browser detached with a CDP port.
83
- * POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
84
- * starts it via an interactive one-shot scheduled task (encoded PowerShell)
85
- * so it survives the ssh session AND serves CDP (session 0 never does).
86
- */
87
- export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string, port: number, customBinary?: string): string;
88
- /**
89
- * Build the remote command that kills whatever holds the CDP port.
90
- * POSIX uses `lsof`+`kill`; Windows uses encoded PowerShell
91
- * (Get-NetTCPConnection → Stop-Process).
92
- */
93
- export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
94
- export declare function ensureRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
95
- /**
96
- * Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
97
- * task-launched (Windows) / detached (posix) browser process is not orphaned when
98
- * the ssh tunnel is torn down — killing only the tunnel left it running on
99
- * win-mini after every `browser stop`. Best-effort: never rejects.
100
- */
101
- export declare function killRemoteBrowser(user: string, host: string, remoteOs: RemoteOs, port: number): Promise<void>;
102
- /**
103
- * Identify whether a pid listening on our target local port is an SSH
104
- * tunnel WE would have spawned for `host:remotePort`. Used so that two
105
- * agents-browser invocations of the same SSH profile share a tunnel
106
- * rather than failing the second one with "port in use".
107
- *
108
- * Best-effort match against the ssh -L command line via `ps`. If we
109
- * can't read the cmd or the args don't look like ours, treat as not-ours.
110
- *
111
- * Windows has no `ps`, so the POSIX branch below always throws there and every
112
- * reuse check returned false — the second invocation to the same host:port then
113
- * failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
114
- * occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
115
- * (localPort === remotePort in the caller), so the pid holding that port is our
116
- * ssh.exe. tasklist only exposes the image name, not the full command line, so
117
- * we can't match host/remotePort as tightly as the POSIX branch — an ssh
118
- * process on our exact local port is our tunnel in practice.
119
- */
120
- export declare function isOwnTunnel(pid: number, host: string, remotePort: number): boolean;