@phnx-labs/agents-cli 1.22.46 → 1.22.49

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 (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -0,0 +1,149 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import * as yaml from 'yaml';
5
+ import { getUserAgentsDir, readMeta, updateMeta } from '../state.js';
6
+ function isRecord(value) {
7
+ return !!value && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ /** Leftover central `browser:` map from before per-device declarations. Empty when none remain. */
10
+ export function centralBrowserProfiles() {
11
+ const central = readMeta().browser;
12
+ if (!central || Object.keys(central).length === 0)
13
+ return {};
14
+ return { ...central };
15
+ }
16
+ /**
17
+ * Fold leftover central `browser:` entries into THIS device's declaration file.
18
+ *
19
+ * Never called implicitly, and that is the point. Every device can read the
20
+ * central map, so an implicit claim races: whichever box happens to read first
21
+ * claims the name, {@link profileKind} then reports it `identity`, and the
22
+ * daemon tunnels to that box — which for a fleet-wide `cdp://localhost:*`
23
+ * profile is a logged-out headless browser wearing a credentialed browser's
24
+ * name. That is the exact bug this module exists to remove, and an implicit
25
+ * migration would write it to disk as a stored fact.
26
+ *
27
+ * The claim is an explicit operator action (`agents browser profiles claim`),
28
+ * run on the machine that actually owns the browser. `canHostHere` is supplied
29
+ * by the command layer so this module stays a leaf — it must not import
30
+ * `isProfileLaunchableHere` (that would cycle through chrome.ts → profiles.ts).
31
+ * Only profiles this machine can host are claimed; the rest stay central,
32
+ * undeclared, and fail loudly on resolve.
33
+ */
34
+ export function migrateCentralBrowserProfiles(canHostHere, name) {
35
+ const meta = readMeta();
36
+ const central = meta.browser;
37
+ if (!central || Object.keys(central).length === 0) {
38
+ if (name) {
39
+ throw new Error(`No leftover central browser profile named "${name}".`);
40
+ }
41
+ return { claimed: [], skipped: [] };
42
+ }
43
+ const local = meta.deviceBrowser ?? {};
44
+ if (name) {
45
+ const config = central[name];
46
+ if (!config) {
47
+ throw new Error(local[name]
48
+ ? `Browser profile "${name}" is already declared on this device.`
49
+ : `No leftover central browser profile named "${name}".`);
50
+ }
51
+ if (!canHostHere(config)) {
52
+ throw new Error(`Cannot claim browser profile "${name}": this machine cannot host it ` +
53
+ `(its browser/binary isn't installed here). Run this command on the machine that has that browser.`);
54
+ }
55
+ }
56
+ const toClaim = {};
57
+ const skipped = [];
58
+ for (const [profileName, config] of Object.entries(central)) {
59
+ if (name && profileName !== name)
60
+ continue;
61
+ if (!canHostHere(config)) {
62
+ skipped.push(profileName);
63
+ continue;
64
+ }
65
+ const existing = local[profileName];
66
+ if (existing && !isDeepStrictEqual(existing, config)) {
67
+ throw new Error(`Cannot migrate browser profile "${profileName}": central agents.yaml and this device's ` +
68
+ `agents.yaml declare different configurations. Resolve the duplicate before retrying.`);
69
+ }
70
+ toClaim[profileName] = config;
71
+ }
72
+ const claimed = Object.keys(toClaim).sort();
73
+ skipped.sort();
74
+ if (claimed.length === 0)
75
+ return { claimed, skipped };
76
+ updateMeta((current) => {
77
+ const legacy = current;
78
+ const remaining = {};
79
+ for (const [profileName, config] of Object.entries(legacy.browser ?? {})) {
80
+ if (!(profileName in toClaim))
81
+ remaining[profileName] = config;
82
+ }
83
+ const { browser: _removed, ...withoutCentralBrowser } = legacy;
84
+ return {
85
+ ...withoutCentralBrowser,
86
+ ...(Object.keys(remaining).length > 0 ? { browser: remaining } : {}),
87
+ deviceBrowser: { ...current.deviceBrowser, ...toClaim },
88
+ };
89
+ });
90
+ return { claimed, skipped };
91
+ }
92
+ /**
93
+ * Every profile any device declares, keyed by name to all declaring devices.
94
+ * Reads every `devices/<name>/agents.yaml`; declarations never overwrite.
95
+ */
96
+ export function profileRegistry() {
97
+ const registry = new Map();
98
+ const devicesDir = path.join(getUserAgentsDir(), 'devices');
99
+ if (!fs.existsSync(devicesDir))
100
+ return registry;
101
+ const devices = fs
102
+ .readdirSync(devicesDir, { withFileTypes: true })
103
+ .filter((entry) => entry.isDirectory())
104
+ .map((entry) => entry.name)
105
+ .sort();
106
+ for (const device of devices) {
107
+ const file = path.join(devicesDir, device, 'agents.yaml');
108
+ if (!fs.existsSync(file))
109
+ continue;
110
+ let parsed;
111
+ try {
112
+ parsed = yaml.parse(fs.readFileSync(file, 'utf8'));
113
+ }
114
+ catch (error) {
115
+ throw new Error(`Cannot read browser declarations from ${file}: ${error instanceof Error ? error.message : String(error)}`);
116
+ }
117
+ if (parsed == null)
118
+ continue;
119
+ if (!isRecord(parsed)) {
120
+ throw new Error(`Cannot read browser declarations from ${file}: document root must be a map.`);
121
+ }
122
+ const browser = parsed.browser;
123
+ if (browser == null)
124
+ continue;
125
+ if (!isRecord(browser)) {
126
+ throw new Error(`Cannot read browser declarations from ${file}: browser must be a map.`);
127
+ }
128
+ for (const [name, config] of Object.entries(browser)) {
129
+ if (!isRecord(config)) {
130
+ throw new Error(`Cannot read browser declaration "${name}" from ${file}: profile must be a map.`);
131
+ }
132
+ const declarations = registry.get(name) ?? [];
133
+ declarations.push({ device, config: config });
134
+ registry.set(name, declarations);
135
+ }
136
+ }
137
+ return registry;
138
+ }
139
+ /** Devices declaring `name`, empty when nobody does. */
140
+ export function declaringDevices(name) {
141
+ return (profileRegistry().get(name) ?? []).map((declaration) => declaration.device);
142
+ }
143
+ /** Exactly one declaring device is identity-bearing; several are fungible. */
144
+ export function profileKind(name) {
145
+ const count = profileRegistry().get(name)?.length ?? 0;
146
+ if (count === 0)
147
+ return null;
148
+ return count === 1 ? 'identity' : 'fungible';
149
+ }
@@ -9,8 +9,17 @@
9
9
  * `browser.remote-control` config key, never synced).
10
10
  *
11
11
  * Local invocations (no marker) are never gated — this only governs cross-machine
12
- * drives. Read-only queries are not gated here; the gate sits at the drive entry
13
- * point (`browser start`), the one command that opens/attaches a browser.
12
+ * drives. Read-only queries are not gated.
13
+ *
14
+ * The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
15
+ * inside the browser daemon at the top of `resolveOrCreateTask` — the one
16
+ * chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
17
+ * for the task-less `browser start` command. It has to live there because ~18
18
+ * page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
19
+ * to a browser implicitly, and gating only the `browser start` command left every
20
+ * one of them ungated; gating only the create branch of `resolveOrCreateTask` left
21
+ * the attach paths ungated (RUSH-3064). {@link assertRemoteControlAllowed} remains
22
+ * as a fast-fail CLI-side check so a refused `start` never auto-creates a profile.
14
23
  */
15
24
  /** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
16
25
  export declare const FLEET_REMOTE_ENV = "AGENTS_FLEET_REMOTE";
@@ -33,3 +42,20 @@ export declare function assertRemoteControlAllowed(opts?: {
33
42
  env?: NodeJS.ProcessEnv;
34
43
  enabled?: boolean;
35
44
  }): void;
45
+ /**
46
+ * The daemon-side consent gate.
47
+ *
48
+ * Reads ONLY the per-request marker, never `process.env`: the browser daemon is
49
+ * shared and long-lived, and it may have been auto-started by a fleet-remote CLI
50
+ * that leaked `AGENTS_FLEET_REMOTE=1` into its environment permanently. Reading
51
+ * the daemon's env would then refuse every subsequent LOCAL drive on this
52
+ * machine until the daemon restarted.
53
+ *
54
+ * `actor` is likewise the caller's forwarded identity, not the daemon's.
55
+ */
56
+ export declare function assertRemoteControlAllowedForRequest(fleetRemote: boolean | undefined, opts?: {
57
+ actor?: string;
58
+ enabled?: boolean;
59
+ }): void;
60
+ /** The refusal text, shared by the CLI-side and daemon-side gates. */
61
+ export declare function remoteControlRefusal(who: string): string;
@@ -9,8 +9,17 @@
9
9
  * `browser.remote-control` config key, never synced).
10
10
  *
11
11
  * Local invocations (no marker) are never gated — this only governs cross-machine
12
- * drives. Read-only queries are not gated here; the gate sits at the drive entry
13
- * point (`browser start`), the one command that opens/attaches a browser.
12
+ * drives. Read-only queries are not gated.
13
+ *
14
+ * The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
15
+ * inside the browser daemon at the top of `resolveOrCreateTask` — the one
16
+ * chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
17
+ * for the task-less `browser start` command. It has to live there because ~18
18
+ * page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
19
+ * to a browser implicitly, and gating only the `browser start` command left every
20
+ * one of them ungated; gating only the create branch of `resolveOrCreateTask` left
21
+ * the attach paths ungated (RUSH-3064). {@link assertRemoteControlAllowed} remains
22
+ * as a fast-fail CLI-side check so a refused `start` never auto-creates a profile.
14
23
  */
15
24
  import { getConfigValue } from '../device-config.js';
16
25
  /** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
@@ -42,7 +51,30 @@ export function assertRemoteControlAllowed(opts) {
42
51
  if (enabled)
43
52
  return;
44
53
  const who = env.AGENTS_ACTOR_HOST || env.AGENTS_ACTOR || 'A fleet machine';
45
- throw new Error(`${who} tried to drive this machine's browser over \`browser --device\`, but remote ` +
54
+ throw new Error(remoteControlRefusal(who));
55
+ }
56
+ /**
57
+ * The daemon-side consent gate.
58
+ *
59
+ * Reads ONLY the per-request marker, never `process.env`: the browser daemon is
60
+ * shared and long-lived, and it may have been auto-started by a fleet-remote CLI
61
+ * that leaked `AGENTS_FLEET_REMOTE=1` into its environment permanently. Reading
62
+ * the daemon's env would then refuse every subsequent LOCAL drive on this
63
+ * machine until the daemon restarted.
64
+ *
65
+ * `actor` is likewise the caller's forwarded identity, not the daemon's.
66
+ */
67
+ export function assertRemoteControlAllowedForRequest(fleetRemote, opts = {}) {
68
+ if (!fleetRemote)
69
+ return;
70
+ const enabled = opts.enabled ?? remoteControlEnabled();
71
+ if (enabled)
72
+ return;
73
+ throw new Error(remoteControlRefusal(opts.actor || 'A fleet machine'));
74
+ }
75
+ /** The refusal text, shared by the CLI-side and daemon-side gates. */
76
+ export function remoteControlRefusal(who) {
77
+ return (`${who} tried to drive this machine's browser over \`browser --device\`, but remote ` +
46
78
  `browser control is off here. To allow it, run on THIS machine:\n` +
47
79
  ` agents browser remote-control on`);
48
80
  }
@@ -0,0 +1,68 @@
1
+ import { type ProfileDeclaration } from './registry.js';
2
+ import { type BrowserProfile, type ConnectionKey } from './types.js';
3
+ import type { BrowserProfileConfig } from '../types.js';
4
+ export type DeviceProbeResult = {
5
+ reachable: true;
6
+ os?: string;
7
+ } | {
8
+ reachable: false;
9
+ reason: string;
10
+ };
11
+ export type DeviceProbe = (device: string) => DeviceProbeResult;
12
+ export interface ResolvedBrowserTarget {
13
+ name: string;
14
+ kind: 'identity' | 'fungible';
15
+ /** Device the daemon will talk to. */
16
+ device: string;
17
+ local: boolean;
18
+ key: ConnectionKey;
19
+ /** `cdp:` locally, `ssh:` when tunnelling to a declaring device. */
20
+ target: string;
21
+ profile: BrowserProfile;
22
+ /**
23
+ * Human line when the daemon picked a remote device. Command output
24
+ * surfaces this so the caller knows WHERE the browser actually lives.
25
+ */
26
+ picked?: string;
27
+ }
28
+ /** Fork only fungible Electron profiles; identity-bearing ones share one connection. */
29
+ export declare function shouldForkProfile(kind: 'identity' | 'fungible', conn: {
30
+ electron?: boolean;
31
+ tasks: {
32
+ size: number;
33
+ };
34
+ }): boolean;
35
+ export declare function profileFromDeclaration(name: string, declaration: ProfileDeclaration): BrowserProfile;
36
+ /** Runtime key: profile name plus the resolved device. Drops `@endpoint-N`. */
37
+ export declare function profileConnectionKey(profileName: string, device: string): ConnectionKey;
38
+ /** Declared names that look like `query` — used in the undeclared error. */
39
+ export declare function similarProfileNames(query: string, names: string[]): string[];
40
+ export declare function undeclaredProfileError(name: string): Error;
41
+ export declare function unreachableDeclaringDevicesError(name: string, devices: string[], failures: string[]): Error;
42
+ export declare function sshEndpointForDeclaration(device: string, config: BrowserProfileConfig, endpointName?: string, os?: string): string;
43
+ /**
44
+ * Decide where `name` connects. Never claims a central leftover. Never
45
+ * falls back to a local browser when the name is declared elsewhere.
46
+ */
47
+ export declare function resolveBrowserTarget(name: string, opts?: {
48
+ endpointName?: string;
49
+ here?: string;
50
+ probe?: DeviceProbe;
51
+ }): ResolvedBrowserTarget;
52
+ /**
53
+ * Rename a leftover `<name>@endpoint-N` runtime dir to the new
54
+ * `<name>@<device>` key so logins in chrome-data are not orphaned.
55
+ *
56
+ * Only when exactly one legacy dir exists and the destination does not —
57
+ * several endpoint dirs stay put so a multi-endpoint profile is not merged.
58
+ */
59
+ /**
60
+ * Adopt a leftover `@endpoint-N` runtime dir onto the new key ONLY for a
61
+ * local connect. Doing this for a tunnel key would rename this machine's
62
+ * pre-T2 logged-out chrome-data onto `comet-local@zion`, and connectProfile
63
+ * would then attach localhost CDP instead of tunnelling.
64
+ */
65
+ export declare function adoptLegacyRuntimeIfLocal(local: boolean, profileName: string, newKey: ConnectionKey, runtimeRoot: string): void;
66
+ export declare function migrateLegacyRuntimeDir(profileName: string, newKey: ConnectionKey, runtimeRoot: string): void;
67
+ /** True when a live key is a leftover `@endpoint-N` key of `profileName`. */
68
+ export declare function isLegacyEndpointKey(key: string, profileName: string): boolean;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Registry-driven browser target resolution.
3
+ *
4
+ * The profile KIND is not a field: it falls out of how many devices declare
5
+ * the name ({@link profileKind}). This module answers where a name should
6
+ * connect, and never claims a leftover central profile (that race is T1).
7
+ *
8
+ * Three outcomes, no fourth:
9
+ * 1. THIS device declares the name → connect locally.
10
+ * 2. Only OTHER devices declare it → tunnel to a reachable declaring device.
11
+ * 3. Nobody declares it → fail loud. Never auto-create a local browser
12
+ * under a name that means a logged-in browser somewhere else.
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { machineId } from '../machine-id.js';
17
+ import { centralBrowserProfiles, declaringDevices, profileKind, profileRegistry, } from './registry.js';
18
+ import { parseEndpointUrl, resolveEndpoint } from './profiles.js';
19
+ import { connectionKey, parseConnectionKey, } from './types.js';
20
+ import { probeHost } from '../hosts/ready.js';
21
+ /** Fork only fungible Electron profiles; identity-bearing ones share one connection. */
22
+ export function shouldForkProfile(kind, conn) {
23
+ if (kind === 'identity')
24
+ return false;
25
+ return Boolean(conn.electron && conn.tasks.size > 0);
26
+ }
27
+ export function profileFromDeclaration(name, declaration) {
28
+ const config = declaration.config;
29
+ return {
30
+ name,
31
+ description: config.description,
32
+ browser: config.browser,
33
+ binary: config.binary,
34
+ electron: config.electron,
35
+ targetFilter: config.targetFilter,
36
+ endpoints: config.endpoints,
37
+ defaultEndpoint: config.defaultEndpoint,
38
+ chrome: config.chrome,
39
+ secrets: config.secrets,
40
+ viewport: config.viewport,
41
+ logDir: config.logDir,
42
+ logHost: config.logHost,
43
+ };
44
+ }
45
+ /** Runtime key: profile name plus the resolved device. Drops `@endpoint-N`. */
46
+ export function profileConnectionKey(profileName, device) {
47
+ return connectionKey(profileName, device);
48
+ }
49
+ function levenshtein(a, b) {
50
+ if (a === b)
51
+ return 0;
52
+ if (a.length === 0)
53
+ return b.length;
54
+ if (b.length === 0)
55
+ return a.length;
56
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
57
+ for (let i = 0; i < a.length; i++) {
58
+ let prev = row[0];
59
+ row[0] = i + 1;
60
+ for (let j = 0; j < b.length; j++) {
61
+ const cur = row[j + 1];
62
+ const cost = a[i] === b[j] ? 0 : 1;
63
+ row[j + 1] = Math.min(row[j] + 1, row[j + 1] + 1, prev + cost);
64
+ prev = cur;
65
+ }
66
+ }
67
+ return row[b.length];
68
+ }
69
+ /** Declared names that look like `query` — used in the undeclared error. */
70
+ export function similarProfileNames(query, names) {
71
+ const q = query.toLowerCase();
72
+ const maxDist = Math.max(2, Math.floor(q.length / 4));
73
+ return names
74
+ .filter((name) => {
75
+ const x = name.toLowerCase();
76
+ if (x === q)
77
+ return false;
78
+ if (x.includes(q) || q.includes(x))
79
+ return Math.min(x.length, q.length) >= 3;
80
+ return levenshtein(x, q) <= maxDist;
81
+ })
82
+ .sort();
83
+ }
84
+ function lines(...parts) {
85
+ return parts.filter((part) => part != null && part !== '').join('\n');
86
+ }
87
+ export function undeclaredProfileError(name) {
88
+ const central = centralBrowserProfiles();
89
+ if (central[name]) {
90
+ return new Error(lines(`Browser profile "${name}" is not declared by any device.`, `It still lives in the central agents.yaml browser: map.`, `Claim it on the machine that hosts that browser with: agents browser profiles claim ${name}`));
91
+ }
92
+ const registry = profileRegistry();
93
+ const similar = similarProfileNames(name, [...registry.keys()]);
94
+ const similarLines = similar.map((candidate) => {
95
+ const devices = (registry.get(candidate) ?? []).map((entry) => entry.device);
96
+ return devices.length > 0
97
+ ? ` ${candidate} (declared on ${devices.join(', ')})`
98
+ : ` ${candidate}`;
99
+ });
100
+ return new Error(lines(`Browser profile "${name}" is not declared by any device.`, ...(similarLines.length > 0
101
+ ? ['Similar names:', ...similarLines]
102
+ : ['No device declares a similar name.']), `Create it on the machine that hosts the browser: agents browser profiles create ${name} --browser <chrome|comet|chromium|brave|edge|arc|custom>`, `Or, if it is a leftover central profile, claim it: agents browser profiles claim ${name}`));
103
+ }
104
+ export function unreachableDeclaringDevicesError(name, devices, failures) {
105
+ return new Error(lines(`Browser profile "${name}" is declared on ${devices.join(', ')}, but none of those devices are reachable.`, ...failures.map((line) => ` ${line}`), `A local browser will not be launched — that would be a logged-out copy of an identity-bearing profile.`));
106
+ }
107
+ /**
108
+ * Build the SSH endpoint used to tunnel to a declaring device's own browser.
109
+ *
110
+ * A `cdp://localhost:N` declaration on that device is THIS machine's loopback
111
+ * if used as-is — that is the original bug. Rewrite it to `ssh://<device>?port=N`.
112
+ * An already-`ssh://` declaration is used as written.
113
+ */
114
+ function isWindowsOs(os) {
115
+ if (!os)
116
+ return false;
117
+ const normalized = os.toLowerCase();
118
+ return (normalized.includes('windows') ||
119
+ normalized === 'win32' ||
120
+ normalized === 'win64' ||
121
+ normalized === 'windows_nt');
122
+ }
123
+ export function sshEndpointForDeclaration(device, config, endpointName, os) {
124
+ const profile = profileFromDeclaration('_', { device, config });
125
+ const resolved = resolveEndpoint(profile, endpointName);
126
+ if (resolved.target.startsWith('ssh:'))
127
+ return resolved.target;
128
+ const parsed = parseEndpointUrl(resolved.target);
129
+ const port = parsed?.port ?? 9222;
130
+ const osQuery = isWindowsOs(os) ? '&os=windows' : '';
131
+ return `ssh://${device}?port=${port}${osQuery}`;
132
+ }
133
+ function defaultProbe(device) {
134
+ try {
135
+ const result = probeHost(device);
136
+ if (result.reachable)
137
+ return { reachable: true, os: result.os };
138
+ return { reachable: false, reason: `ssh to ${device} failed` };
139
+ }
140
+ catch (error) {
141
+ return {
142
+ reachable: false,
143
+ reason: error instanceof Error ? error.message : String(error),
144
+ };
145
+ }
146
+ }
147
+ /**
148
+ * Decide where `name` connects. Never claims a central leftover. Never
149
+ * falls back to a local browser when the name is declared elsewhere.
150
+ */
151
+ export function resolveBrowserTarget(name, opts = {}) {
152
+ const here = opts.here ?? machineId();
153
+ const kind = profileKind(name);
154
+ const declarations = profileRegistry().get(name) ?? [];
155
+ if (kind === null || declarations.length === 0) {
156
+ throw undeclaredProfileError(name);
157
+ }
158
+ const devices = declaringDevices(name);
159
+ const local = declarations.find((entry) => entry.device === here);
160
+ if (local) {
161
+ const profile = profileFromDeclaration(name, local);
162
+ const resolved = resolveEndpoint(profile, opts.endpointName);
163
+ return {
164
+ name,
165
+ kind,
166
+ device: here,
167
+ local: true,
168
+ key: profileConnectionKey(name, here),
169
+ target: resolved.target,
170
+ profile: {
171
+ ...profile,
172
+ binary: resolved.binary,
173
+ targetFilter: resolved.targetFilter,
174
+ },
175
+ };
176
+ }
177
+ const probe = opts.probe ?? defaultProbe;
178
+ const candidates = [...declarations].sort((a, b) => a.device.localeCompare(b.device));
179
+ const failures = [];
180
+ for (const declaration of candidates) {
181
+ const result = probe(declaration.device);
182
+ if (!result.reachable) {
183
+ failures.push(`${declaration.device}: ${result.reason}`);
184
+ continue;
185
+ }
186
+ const profile = profileFromDeclaration(name, declaration);
187
+ const resolved = resolveEndpoint(profile, opts.endpointName);
188
+ const target = sshEndpointForDeclaration(declaration.device, declaration.config, opts.endpointName, result.os);
189
+ const picked = candidates.length > 1
190
+ ? `Using ${name} on ${declaration.device} (declared on ${devices.join(', ')})`
191
+ : `Using ${name} on ${declaration.device}`;
192
+ return {
193
+ name,
194
+ kind,
195
+ device: declaration.device,
196
+ local: false,
197
+ key: profileConnectionKey(name, declaration.device),
198
+ target,
199
+ profile: {
200
+ ...profile,
201
+ binary: resolved.binary,
202
+ targetFilter: resolved.targetFilter,
203
+ },
204
+ picked,
205
+ };
206
+ }
207
+ throw unreachableDeclaringDevicesError(name, devices, failures);
208
+ }
209
+ /**
210
+ * Rename a leftover `<name>@endpoint-N` runtime dir to the new
211
+ * `<name>@<device>` key so logins in chrome-data are not orphaned.
212
+ *
213
+ * Only when exactly one legacy dir exists and the destination does not —
214
+ * several endpoint dirs stay put so a multi-endpoint profile is not merged.
215
+ */
216
+ /**
217
+ * Adopt a leftover `@endpoint-N` runtime dir onto the new key ONLY for a
218
+ * local connect. Doing this for a tunnel key would rename this machine's
219
+ * pre-T2 logged-out chrome-data onto `comet-local@zion`, and connectProfile
220
+ * would then attach localhost CDP instead of tunnelling.
221
+ */
222
+ export function adoptLegacyRuntimeIfLocal(local, profileName, newKey, runtimeRoot) {
223
+ if (!local)
224
+ return;
225
+ migrateLegacyRuntimeDir(profileName, newKey, runtimeRoot);
226
+ }
227
+ export function migrateLegacyRuntimeDir(profileName, newKey, runtimeRoot) {
228
+ const dest = path.join(runtimeRoot, newKey);
229
+ if (fs.existsSync(dest))
230
+ return;
231
+ if (!fs.existsSync(runtimeRoot))
232
+ return;
233
+ const legacy = fs.readdirSync(runtimeRoot).filter((entry) => {
234
+ const parsed = parseConnectionKey(entry);
235
+ return (parsed.profile === profileName &&
236
+ parsed.endpoint !== undefined &&
237
+ /^endpoint-\d+$/.test(parsed.endpoint) &&
238
+ parsed.fork === undefined);
239
+ });
240
+ if (legacy.length !== 1)
241
+ return;
242
+ fs.renameSync(path.join(runtimeRoot, legacy[0]), dest);
243
+ }
244
+ /** True when a live key is a leftover `@endpoint-N` key of `profileName`. */
245
+ export function isLegacyEndpointKey(key, profileName) {
246
+ const parsed = parseConnectionKey(key);
247
+ return (parsed.profile === profileName &&
248
+ parsed.endpoint !== undefined &&
249
+ /^endpoint-\d+$/.test(parsed.endpoint));
250
+ }
@@ -1,5 +1,33 @@
1
- import { type ProfileScope } from './profiles.js';
1
+ import { type BrowserProfileWithDeclarations } from './profiles.js';
2
2
  import { type ProfileName } from './types.js';
3
+ /**
4
+ * How many devices declare this profile — the shipped model's two kinds.
5
+ *
6
+ * NOT the deleted `scope: local | fleet`, which described where the config was
7
+ * stored. `fungible` means several devices declare the name and each uses its
8
+ * own browser; `identity` means exactly one does, so the name is a singular
9
+ * browser. Prune only ever sees profiles THIS device declares, so `fungible`
10
+ * here means "declared here and elsewhere too".
11
+ */
12
+ type PruneProfileClass = 'identity' | 'fungible';
13
+ /**
14
+ * Detect an identity-bearing profile whose stored endpoint is loopback, viewed
15
+ * from a machine that is NOT the declaring device.
16
+ *
17
+ * That is the original `comet-local` bug: five real logins on the declaring
18
+ * box, a bare logged-out chromium answering to the same name on a worker
19
+ * because `cdp://localhost:N` is evaluated on the machine running the command.
20
+ * The daemon now tunnels that shape (`resolve-target.ts`); doctor still flags
21
+ * it so a worker does not report a green local binary/port for someone else's
22
+ * browser. Kind comes from the registry (exactly one declaring device), never
23
+ * from a stored field.
24
+ */
25
+ export declare function identityLoopbackMismatch(profile: BrowserProfileWithDeclarations): {
26
+ misfiled: false;
27
+ } | {
28
+ misfiled: true;
29
+ why: string;
30
+ };
3
31
  /**
4
32
  * Per-profile runtime files we persist under
5
33
  * `~/.agents/.cache/browser/<composite>/`:
@@ -51,7 +79,7 @@ export declare function readProfileRuntime(profileName: string): ProfileRuntime
51
79
  export declare function clearProfileRuntime(profileName: string): void;
52
80
  /**
53
81
  * Recursively remove the whole profile cache (chrome-data, tasks.json,
54
- * everything). Used by `profiles delete` so an old profile name doesn't
82
+ * everything). Used by `profiles remove` so an old profile name doesn't
55
83
  * leak its history into a freshly-recreated one.
56
84
  */
57
85
  export declare function removeProfileCache(profileName: string): void;
@@ -135,7 +163,7 @@ export declare function isProfileInUse(profileName: string): boolean;
135
163
  export type PruneReason = 'binary-missing' | 'never-used';
136
164
  export interface PruneCandidate {
137
165
  name: string;
138
- scope: ProfileScope;
166
+ scope: PruneProfileClass;
139
167
  reason: PruneReason;
140
168
  /** Cache dirs that would be removed along with the config entry. */
141
169
  cacheDirs: string[];
@@ -146,8 +174,15 @@ export interface PrunePlan {
146
174
  /** Profiles deliberately left alone, with the guard that kept them. */
147
175
  kept: Array<{
148
176
  name: string;
149
- scope: ProfileScope;
177
+ scope: PruneProfileClass;
150
178
  why: string;
179
+ /**
180
+ * True for an identity-bearing profile whose loopback endpoint only makes
181
+ * sense on the declaring device. A structured flag, not a substring of
182
+ * `why`: both the CLI's human output and `--json` consumers branch on it,
183
+ * and sniffing the reason text would couple them to wording.
184
+ */
185
+ misfiled?: boolean;
151
186
  }>;
152
187
  }
153
188
  export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
@@ -170,8 +205,6 @@ export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
170
205
  * - **the auto-detected profile** (`auto-chrome`, or the `default` an older
171
206
  * build wrote) — auto-regenerated by `ensureDefaultBrowserProfile`, so
172
207
  * pruning it is pure churn.
173
- * - **fleet-synced** — deleting one removes it from EVERY machine, so it takes
174
- * the explicit `includeFleet` opt-in.
175
208
  *
176
209
  * Known limitation: `BrowserProfileConfig` records no creation time, so a
177
210
  * profile created seconds ago and not yet started is indistinguishable from an
@@ -180,22 +213,22 @@ export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
180
213
  */
181
214
  export declare function planProfilePrune(profiles: Array<{
182
215
  name: string;
183
- scope: ProfileScope;
216
+ scope: PruneProfileClass;
184
217
  launchableHere: boolean;
218
+ /** Set when this identity-bearing profile's endpoint only makes sense on one machine. */
219
+ misfiledWhy?: string;
185
220
  }>, opts?: {
186
- includeFleet?: boolean;
187
221
  configuredDefault?: string;
188
222
  }): PrunePlan;
189
223
  /**
190
224
  * Build the prune plan from live config. Split from {@link planProfilePrune} so
191
225
  * the decision rules stay unit-testable without a configured machine.
192
226
  */
193
- export declare function buildProfilePrunePlan(opts?: {
194
- includeFleet?: boolean;
195
- }): Promise<PrunePlan>;
227
+ export declare function buildProfilePrunePlan(): Promise<PrunePlan>;
196
228
  /**
197
229
  * Execute a prune plan: drop each candidate's config entry and wipe its cache
198
230
  * dirs. Returns the plan that was applied so the caller can report exactly what
199
231
  * went, matching what `--dry-run` printed.
200
232
  */
201
233
  export declare function pruneProfiles(plan: PrunePlan): Promise<PrunePlan>;
234
+ export {};