@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
@@ -14,9 +14,28 @@ import { resolveRunDefaults } from './run-defaults.js';
14
14
  export const MODE_DESCRIPTIONS = {
15
15
  plan: 'read-only investigation; no writes, no shell side-effects',
16
16
  edit: 'may edit files; prompts for shell / risky operations',
17
- auto: 'smart classifier auto-approves safe ops, prompts for risky',
17
+ auto: 'more autonomy than edit; the exact policy is per-harness (see notes)',
18
18
  skip: 'bypass every permission prompt (dangerously-skip-permissions)',
19
19
  };
20
+ /**
21
+ * What `auto` actually does, per harness. Two genuinely different mechanisms
22
+ * wear the same mode name, and conflating them is a safety error in both
23
+ * directions: telling a claude operator that auto never prompts invites an
24
+ * unattended run that stalls on a risky-operation gate, and telling a codex
25
+ * operator that auto prompts for risky work asserts a gate codex does not have.
26
+ * Absent entry = no extra note; the row's own text suffices.
27
+ */
28
+ const AUTO_SEMANTICS = {
29
+ // Classifier-style: still escalates. --permission-mode auto / --autopilot.
30
+ claude: 'a smart classifier auto-approves safe operations and still prompts for risky ones.',
31
+ copilot: 'a smart classifier auto-approves safe operations and still prompts for risky ones.',
32
+ // Approvals off, sandbox kept: never escalates. A denied command just fails.
33
+ codex: 'approval_policy=never over the same sandbox as edit — it never prompts, and a sandbox-denied command fails instead of raising an approval request.',
34
+ muse: 'runs --disable-approval, which turns approvals off while keeping the sandbox — like codex, it never prompts and a denied command fails.',
35
+ // Native autonomy dial at its top setting.
36
+ droid: 'runs --auto high, droid\'s full-autonomy setting.',
37
+ kimi: "runs kimi's native --auto.",
38
+ };
20
39
  /**
21
40
  * Build the permission-mode catalog for one harness.
22
41
  * `version` only affects the configured run.defaults lookup (modes themselves
@@ -42,6 +61,15 @@ export function getAgentModesCatalog(agent, version, cwd = process.cwd()) {
42
61
  if (unsupported.includes('auto')) {
43
62
  notes.push(`--mode auto degrades to edit on ${agent} (no native auto classifier).`);
44
63
  }
64
+ // MODE_DESCRIPTIONS is one flat Record<Mode, string> rendered for every agent,
65
+ // so it cannot name any single harness's mechanism without lying about the
66
+ // others: claude/copilot auto STILL PROMPTS for risky operations, while codex
67
+ // auto never prompts at all. The row states only what is true everywhere; the
68
+ // mechanism rides here, per harness.
69
+ const autoSemantics = AUTO_SEMANTICS[agent];
70
+ if (autoSemantics && supported.includes('auto')) {
71
+ notes.push(`${agent} --mode auto: ${autoSemantics}`);
72
+ }
45
73
  if (unsupported.includes('plan')) {
46
74
  notes.push(`--mode plan degrades to ${defaultMode} on ${agent} (no native read-only mode).`);
47
75
  }
@@ -552,9 +552,9 @@ export declare function resolveAgentName(input: string): AgentId | null;
552
552
  /** Check whether the input string matches any known agent name or alias. */
553
553
  export declare function isAgentName(input: string): boolean;
554
554
  /**
555
- * Split a CLI-provided `<agent>[@<version>]` spec into its bare agent id and
556
- * optional exact version token, the same way `agents run` does
557
- * (commands/exec.ts parses its positional with a plain `split('@')`) — NOT the
555
+ * Split a CLI-provided `<agent>[@<version>][#<label>]` spec into its bare agent
556
+ * id, optional exact version token, and optional account label, the same way
557
+ * `agents run` does — NOT the
558
558
  * agent-spec qualifier engine (`@latest`/`@all`), which enumerates installed
559
559
  * versions for read/diagnostic commands, not a single launch target. Returns an
560
560
  * error message rather than throwing so callers decide exit-vs-continue.
@@ -562,6 +562,7 @@ export declare function isAgentName(input: string): boolean;
562
562
  export declare function parseAgentVersionSpec(raw: string): {
563
563
  agent: AgentId;
564
564
  version?: string;
565
+ label?: string;
565
566
  } | {
566
567
  error: string;
567
568
  };
@@ -298,7 +298,7 @@ export const AGENTS = {
298
298
  cloudProvider: 'codex',
299
299
  // Subagents: multi-agent plumbing since 0.117.0; custom agents as
300
300
  // ~/.codex/agents/*.toml (name, description, developer_instructions).
301
- capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
301
+ capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], interactiveRepl: true },
302
302
  },
303
303
  gemini: {
304
304
  id: 'gemini',
@@ -1504,12 +1504,9 @@ const CREDENTIAL_FILE_SEGMENTS = {
1504
1504
  // Muse Code stores OAuth / API credentials at ~/.config/muse/auth.json
1505
1505
  // (or META_API_KEY in the environment, which is not a file).
1506
1506
  muse: [['.config', 'muse', 'auth.json']],
1507
- // Cursor's OAuth token the login gate — is at $XDG_CONFIG_HOME/cursor/auth.json
1508
- // (~/.config/cursor/auth.json by default). cli-config.json holds only account
1509
- // metadata; the token file is what a real launch authenticates from, so a
1510
- // version home with no token of its own is genuinely logged out once runs pin
1511
- // XDG_CONFIG_HOME per home (see buildExecEnv).
1512
- cursor: [['.config', 'cursor', 'auth.json']],
1507
+ // With AGENT_CLI_CREDENTIAL_STORE=file, Cursor stores both OAuth tokens at
1508
+ // ~/.cursor/auth.json. cli-config.json holds account metadata only.
1509
+ cursor: [['.cursor', 'auth.json']],
1513
1510
  };
1514
1511
  /** Whether an agent's credential file exists under a given home. */
1515
1512
  function credentialFileExistsUnder(agentId, home) {
@@ -2112,7 +2109,7 @@ export async function getAccountInfo(agentId, home) {
2112
2109
  case 'cursor': {
2113
2110
  // Cursor CLI keeps account metadata in ~/.cursor/cli-config.json
2114
2111
  // (authInfo: { email, userId, authId }) and its OAuth tokens SEPARATELY
2115
- // in ~/.config/cursor/auth.json ({ accessToken, refreshToken }). Presence
2112
+ // in ~/.cursor/auth.json ({ accessToken, refreshToken }). Presence
2116
2113
  // of an access token is the signed-in signal; email/ids come from
2117
2114
  // cli-config. authId is the OAuth subject (e.g. "google-oauth2|<n>") — the
2118
2115
  // same value the usage endpoint keys on (see getCursorUsageInfo).
@@ -2126,7 +2123,7 @@ export async function getAccountInfo(agentId, home) {
2126
2123
  const accountId = normalizeIdentityPart(authInfo?.authId ?? authInfo?.userId);
2127
2124
  if (!email && !accountId)
2128
2125
  return { ...empty, lastActive };
2129
- const authPath = resolveAccountCredentialPath(base, '.config', 'cursor', 'auth.json');
2126
+ const authPath = resolveAccountCredentialPath(base, '.cursor', 'auth.json');
2130
2127
  let hasToken = false;
2131
2128
  if (authPath) {
2132
2129
  try {
@@ -2135,8 +2132,13 @@ export async function getAccountInfo(agentId, home) {
2135
2132
  }
2136
2133
  catch { /* unreadable token file */ }
2137
2134
  }
2135
+ // cli-config survives logout and may describe a different keychain
2136
+ // account. Without this version's file token, its identity is stale
2137
+ // metadata rather than a usable managed login.
2138
+ if (!hasToken)
2139
+ return { ...empty, lastActive };
2138
2140
  const accountKey = buildIdentityKey(agentId, [['user', accountId]]);
2139
- return { ...empty, email, accountId, accountKey, signedIn: hasToken || !!email, lastActive };
2141
+ return { ...empty, email, accountId, accountKey, signedIn: true, lastActive };
2140
2142
  }
2141
2143
  catch { }
2142
2144
  return { ...empty, lastActive };
@@ -3050,15 +3052,23 @@ export function isAgentName(input) {
3050
3052
  return resolveAgentName(input) !== null;
3051
3053
  }
3052
3054
  /**
3053
- * Split a CLI-provided `<agent>[@<version>]` spec into its bare agent id and
3054
- * optional exact version token, the same way `agents run` does
3055
- * (commands/exec.ts parses its positional with a plain `split('@')`) — NOT the
3055
+ * Split a CLI-provided `<agent>[@<version>][#<label>]` spec into its bare agent
3056
+ * id, optional exact version token, and optional account label, the same way
3057
+ * `agents run` does — NOT the
3056
3058
  * agent-spec qualifier engine (`@latest`/`@all`), which enumerates installed
3057
3059
  * versions for read/diagnostic commands, not a single launch target. Returns an
3058
3060
  * error message rather than throwing so callers decide exit-vs-continue.
3059
3061
  */
3060
3062
  export function parseAgentVersionSpec(raw) {
3061
- const parts = raw.split('@');
3063
+ const labelParts = raw.split('#');
3064
+ if (labelParts.length > 2) {
3065
+ return { error: `Invalid agent spec '${raw}': at most one '#label' is allowed` };
3066
+ }
3067
+ const [versionSpec, rawLabel] = labelParts;
3068
+ if (rawLabel !== undefined && (rawLabel === '' || !/^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/.test(rawLabel))) {
3069
+ return { error: `Invalid account label '${rawLabel}' in '${raw}'` };
3070
+ }
3071
+ const parts = versionSpec.split('@');
3062
3072
  if (parts.length > 2) {
3063
3073
  return { error: `Invalid agent spec '${raw}': at most one '@version' is allowed` };
3064
3074
  }
@@ -3070,7 +3080,7 @@ export function parseAgentVersionSpec(raw) {
3070
3080
  if (rawVersion !== undefined && (rawVersion === '' || !VERSION_RE.test(rawVersion))) {
3071
3081
  return { error: `Invalid version '${rawVersion}' in '${raw}'` };
3072
3082
  }
3073
- return { agent, ...(rawVersion ? { version: rawVersion } : {}) };
3083
+ return { agent, ...(rawVersion ? { version: rawVersion } : {}), ...(rawLabel ? { label: rawLabel } : {}) };
3074
3084
  }
3075
3085
  /**
3076
3086
  * Build the deprecation notice lines for an agent, or null if it isn't
@@ -5,7 +5,7 @@
5
5
  * peer of `agents insights` with overlapping "analytics" meaning, so agents and
6
6
  * humans kept picking the wrong verb. The cheap counter path (sessions index +
7
7
  * usage.db) still exists — it is now `agents insights mix` and the recipe
8
- * subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
8
+ * subcommands below. Latency stays on `agents perf`; quota on `agents view`.
9
9
  *
10
10
  * Former top-level `agents trends` is gone. The nested spelling
11
11
  * `agents insights trends` is an alias of `agents insights mix`.
@@ -5,7 +5,7 @@
5
5
  * peer of `agents insights` with overlapping "analytics" meaning, so agents and
6
6
  * humans kept picking the wrong verb. The cheap counter path (sessions index +
7
7
  * usage.db) still exists — it is now `agents insights mix` and the recipe
8
- * subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
8
+ * subcommands below. Latency stays on `agents perf`; quota on `agents view`.
9
9
  *
10
10
  * Former top-level `agents trends` is gone. The nested spelling
11
11
  * `agents insights trends` is an alias of `agents insights mix`.
@@ -97,7 +97,7 @@ export function registerMixCommands(parent) {
97
97
  Session recipes read sessions.db; resource recipes read ~/.agents/.history/analytics/usage.db.
98
98
  Empty recipes are skipped on the default mix board.
99
99
  This is the cheap counter path. Behavioural report (transcript content, account split)
100
- is bare \`agents insights\`. Latency is \`agents perf\`; quota is \`agents usage\`.
100
+ is bare \`agents insights\`. Latency is \`agents perf\`; quota is \`agents view\`.
101
101
  Skill/slash-command popularity is \`agents sessions stats\`.
102
102
  `,
103
103
  });
@@ -148,13 +148,21 @@ export declare function mergeAuthHealthEntries(current: Record<string, AuthHealt
148
148
  export declare function writeAuthHealthEntries(entries: Record<string, AuthHealth>): void;
149
149
  /**
150
150
  * Complete a live auth probe for one (agent, home). For claude/kimi/droid this
151
- * hits the provider; for everyone else it reports a best-effort local verdict
152
- * (`unverified` when a credential is present, `unconfigured` otherwise) never
153
- * masquerading as `live`.
151
+ * hits the provider unless the account's usage cache already holds a fresh
152
+ * successful fetch, which is the same authenticated request and proves the
153
+ * token live without spending a second one (RUSH-3036). For everyone else it
154
+ * reports a best-effort local verdict (`unverified` when a credential is
155
+ * present, `unconfigured` otherwise) — never masquerading as `live`.
154
156
  */
155
157
  export declare function probeAuthHealth(agent: AgentId, home: string | undefined, opts?: {
156
158
  cliVersion?: string | null;
157
159
  info?: AccountInfo | null;
160
+ /**
161
+ * Skip the derived-from-usage shortcut and fire a real network probe
162
+ * (RUSH-3036). Set by `agents devices ping [--strict]`, whose contract is
163
+ * a genuinely live request that surfaces `revoked` immediately.
164
+ */
165
+ forceLive?: boolean;
158
166
  }): Promise<AuthHealth>;
159
167
  /** One probed install on a host. */
160
168
  export interface AuthProbeRow {
@@ -215,6 +223,8 @@ export declare function groupFleetAuthInstalls<T extends FleetAuthInstall>(insta
215
223
  export declare function probeLocalFleetAuth(opts?: {
216
224
  cliVersion?: string | null;
217
225
  agents?: readonly AgentId[];
226
+ /** Fire real network probes even when fresh usage evidence exists (RUSH-3036) — the `devices ping [--strict]` contract. */
227
+ forceLive?: boolean;
218
228
  }): Promise<AuthProbeRow[]>;
219
229
  /** Persist a host's probed rows into the cache (keyed by host+agent+version). */
220
230
  export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[]): void;
@@ -20,7 +20,7 @@ import * as fs from 'fs';
20
20
  import * as path from 'path';
21
21
  import { ALL_AGENT_IDS, getAccountInfo } from './agents.js';
22
22
  import { getCacheDir } from './state.js';
23
- import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, } from './accounting/usage.js';
23
+ import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, readClaudeUsageCache, } from './accounting/usage.js';
24
24
  import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
25
25
  import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
26
26
  /** Agents with a live network probe wired up today. The rest are best-effort. */
@@ -294,22 +294,64 @@ export function writeAuthHealthEntries(entries) {
294
294
  // ---------------------------------------------------------------------------
295
295
  // The probe (writer side)
296
296
  // ---------------------------------------------------------------------------
297
+ /**
298
+ * A usage snapshot this recent is live proof the account's shared setup-token
299
+ * works: the snapshot only exists because an authenticated `/oauth/usage`
300
+ * request succeeded. Matches the periodic tick's own probe window
301
+ * (AUTH_PROBE_MAX_AGE_MS in daemon-ticks.ts — not imported to avoid a cycle;
302
+ * a drift here only widens/narrows evidence freshness, never correctness).
303
+ */
304
+ const FRESH_USAGE_VERDICT_MAX_AGE_MS = 20 * 60_000;
305
+ /**
306
+ * A `live` verdict derived from the account's usage cache instead of a second
307
+ * network request (RUSH-3036). The auth probe and the usage fetch hit the SAME
308
+ * rate-limited endpoint with the SAME fleet-shared setup-token, so a fresh
309
+ * successful usage snapshot already proves everything the probe would: paying a
310
+ * second request per account per box was half the fleet's endpoint load.
311
+ *
312
+ * Two guards keep the evidence honest (both review findings on the first cut):
313
+ * the caller must assert this box holds a LOCAL credential for the account
314
+ * (`signedIn`) — a fleet-imported snapshot proves the shared token works, not
315
+ * that THIS box can authenticate, so an unsigned home never derives `live`;
316
+ * and a `forceLive` caller (`agents devices ping --strict`) skips derivation
317
+ * entirely, because its contract is a real request that can surface `revoked`
318
+ * within seconds, not minutes. Returns null when there is no admissible fresh
319
+ * evidence — the caller then live-probes as before.
320
+ */
321
+ function verdictFromFreshUsage(usageKey, signedIn, now) {
322
+ if (!usageKey || !signedIn)
323
+ return null;
324
+ const snapshot = readClaudeUsageCache(usageKey);
325
+ const capturedAt = snapshot?.capturedAt?.getTime();
326
+ if (!capturedAt || now - capturedAt >= FRESH_USAGE_VERDICT_MAX_AGE_MS)
327
+ return null;
328
+ const ageMin = Math.max(1, Math.round((now - capturedAt) / 60_000));
329
+ return { verdict: 'live', checkedAt: now, detail: `token proven live by a usage fetch ${ageMin}m ago` };
330
+ }
297
331
  /**
298
332
  * Complete a live auth probe for one (agent, home). For claude/kimi/droid this
299
- * hits the provider; for everyone else it reports a best-effort local verdict
300
- * (`unverified` when a credential is present, `unconfigured` otherwise) never
301
- * masquerading as `live`.
333
+ * hits the provider unless the account's usage cache already holds a fresh
334
+ * successful fetch, which is the same authenticated request and proves the
335
+ * token live without spending a second one (RUSH-3036). For everyone else it
336
+ * reports a best-effort local verdict (`unverified` when a credential is
337
+ * present, `unconfigured` otherwise) — never masquerading as `live`.
302
338
  */
303
339
  export async function probeAuthHealth(agent, home, opts) {
304
340
  const checkedAt = Date.now();
305
341
  if (LIVE_PROBE_AGENTS.has(agent)) {
342
+ const usageScope = opts?.info?.usageKey ?? null;
343
+ if (opts?.forceLive !== true) {
344
+ const derived = verdictFromFreshUsage(usageScope, opts?.info?.signedIn === true, checkedAt);
345
+ if (derived)
346
+ return derived;
347
+ }
306
348
  let probe;
307
349
  if (agent === 'claude')
308
- probe = await probeClaudeStatus(home, opts?.cliVersion);
350
+ probe = await probeClaudeStatus(home, opts?.cliVersion, usageScope);
309
351
  else if (agent === 'kimi')
310
- probe = await probeKimiStatus(home);
352
+ probe = await probeKimiStatus(home, usageScope);
311
353
  else
312
- probe = await probeDroidStatus(home);
354
+ probe = await probeDroidStatus(home, usageScope);
313
355
  return { verdict: verdictFromProbe(probe), checkedAt, detail: probeDetail(probe) };
314
356
  }
315
357
  const info = opts?.info !== undefined ? opts.info : await getAccountInfo(agent, home).catch(() => null);
@@ -384,7 +426,7 @@ export async function probeLocalFleetAuth(opts) {
384
426
  // accounts, so no same-account concurrency is left to trip the throttle.
385
427
  const perGroup = await Promise.all(groupFleetAuthInstalls(installs, (inst) => LIVE_PROBE_AGENTS.has(inst.agent)).map(async (group) => {
386
428
  const rep = group.probe;
387
- const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info });
429
+ const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive });
388
430
  health.account = authAccountLabel(rep.info);
389
431
  if (health.verdict === 'unconfigured')
390
432
  return [];
@@ -13,3 +13,22 @@ export declare const DEFAULT_VIEWPORT: {
13
13
  export declare const DEVICES: Record<string, DeviceDescriptor>;
14
14
  export declare function getDevice(name: string): DeviceDescriptor | undefined;
15
15
  export declare function listDevices(): string[];
16
+ /**
17
+ * Parse a `--window WxH` value. Returns null when the value is malformed, so
18
+ * callers emit their own actionable error.
19
+ *
20
+ * Shared by `profiles create` and `profiles edit` — one regex, one test, no
21
+ * drift between the two surfaces.
22
+ */
23
+ export declare function parseWindowSize(raw: string): {
24
+ width: number;
25
+ height: number;
26
+ } | null;
27
+ /**
28
+ * Parse a `--position X,Y` value. Negative coordinates are valid — a window may
29
+ * sit on a display left of or above the primary one.
30
+ */
31
+ export declare function parseWindowPosition(raw: string): {
32
+ x: number;
33
+ y: number;
34
+ } | null;
@@ -36,3 +36,26 @@ export function getDevice(name) {
36
36
  export function listDevices() {
37
37
  return Object.keys(DEVICES);
38
38
  }
39
+ /**
40
+ * Parse a `--window WxH` value. Returns null when the value is malformed, so
41
+ * callers emit their own actionable error.
42
+ *
43
+ * Shared by `profiles create` and `profiles edit` — one regex, one test, no
44
+ * drift between the two surfaces.
45
+ */
46
+ export function parseWindowSize(raw) {
47
+ const m = String(raw).match(/^(\d+)x(\d+)$/);
48
+ if (!m)
49
+ return null;
50
+ return { width: parseInt(m[1], 10), height: parseInt(m[2], 10) };
51
+ }
52
+ /**
53
+ * Parse a `--position X,Y` value. Negative coordinates are valid — a window may
54
+ * sit on a display left of or above the primary one.
55
+ */
56
+ export function parseWindowPosition(raw) {
57
+ const m = String(raw).match(/^(-?\d+),(-?\d+)$/);
58
+ if (!m)
59
+ return null;
60
+ return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) };
61
+ }
@@ -16,13 +16,27 @@ export interface SSHConnection {
16
16
  * strings differ (no `&` backgrounding, no `lsof`, `.exe` instead of `.app`).
17
17
  */
18
18
  export type RemoteOs = 'windows' | 'posix';
19
+ export 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
+ };
19
33
  export declare function connectSSH(endpoint: string, profile: BrowserProfile,
20
34
  /**
21
- * Runtime key (`<profile>@<endpoint>`) this connection is stored under. It
35
+ * Runtime key (`<profile>@<device>`) this connection is stored under. It
22
36
  * keys the tunnel's runtime record; `profile.name` is the bare, user-facing
23
37
  * name and appears only in messages (RUSH-2709).
24
38
  */
25
- key: ConnectionKey): Promise<SSHConnection>;
39
+ key: ConnectionKey, opts?: SSHConnectOptions): Promise<SSHConnection>;
26
40
  /**
27
41
  * Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
28
42
  * UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
@@ -13,13 +13,19 @@ export { shellQuote };
13
13
  // driver's original foreground, stderr-captured behavior exactly.
14
14
  import { startSSHTunnel } from '../../computer/ssh-tunnel.js';
15
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
+ }
16
22
  export async function connectSSH(endpoint, profile,
17
23
  /**
18
- * Runtime key (`<profile>@<endpoint>`) this connection is stored under. It
24
+ * Runtime key (`<profile>@<device>`) this connection is stored under. It
19
25
  * keys the tunnel's runtime record; `profile.name` is the bare, user-facing
20
26
  * name and appears only in messages (RUSH-2709).
21
27
  */
22
- key) {
28
+ key, opts = {}) {
23
29
  const url = new URL(endpoint);
24
30
  if (url.protocol !== 'ssh:') {
25
31
  throw new Error(`Invalid SSH endpoint: ${endpoint}`);
@@ -59,7 +65,14 @@ key) {
59
65
  // (the launch backgrounds/WMI-spawns regardless), so this does not spuriously
60
66
  // fail the reconnect path — only genuine launch failures reach the caller
61
67
  // instead of being swallowed and mis-reported later as a tunnel timeout.
62
- await ensureRemoteBrowser(user, host, profile.browser, remotePort, remoteOs, profile.binary);
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
+ }
63
76
  let tunnel;
64
77
  if (occupant) {
65
78
  // Reuse the existing tunnel rather than spawning a duplicate.
@@ -78,7 +91,19 @@ key) {
78
91
  tunnel.kill();
79
92
  throw new Error(`SSH tunnel failed to establish to ${host}`);
80
93
  }
81
- const { wsUrl, browser } = await discoverBrowserWsUrl(localPort, 'localhost', profile.name);
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
+ }
82
107
  try {
83
108
  verifyBrowserIdentity(browser, profile.browser, remotePort, host);
84
109
  }
@@ -108,11 +133,13 @@ key) {
108
133
  pid: tunnelPid,
109
134
  cleanup: () => {
110
135
  cdp.close();
111
- // Kill the remote browser BEFORE tearing down the tunnel. It runs on a
112
- // separate ssh connection (independent of the tunnel we're about to
113
- // kill), so tunnel teardown can't cut it off. Fire-and-forget cleanup
114
- // stays synchronous, and killRemoteBrowser never rejects.
115
- killRemoteBrowser(user, host, remoteOs, remotePort).catch(() => { });
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
+ }
116
143
  tunnel.kill();
117
144
  clearProfileRuntime(key);
118
145
  },
@@ -96,7 +96,7 @@ export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOpt
96
96
  * Fill actor / launchId / sessionId from the calling process when the request
97
97
  * left them blank. Explicit values on the request always win.
98
98
  */
99
- export declare function stampCallerIdentity(request: IPCRequest): IPCRequest;
99
+ export declare function stampCallerIdentity(request: IPCRequest, env?: NodeJS.ProcessEnv): IPCRequest;
100
100
  /**
101
101
  * Open one connection that can serve many browser operations. This uses the
102
102
  * same daemon readiness and version-reconciliation path as sendIPCRequest;
@@ -8,6 +8,7 @@ import { startDaemon, stopDaemon } from '../daemon/daemon.js';
8
8
  import { getCliVersion } from '../version.js';
9
9
  import { compareVersions } from '../agent-spec/primitives.js';
10
10
  import { getDaemonLogPath } from '../daemon/daemon.js';
11
+ import { isFleetRemoteInvocation } from './remote-control.js';
11
12
  import { resolveCallerIdentity } from './caller-identity.js';
12
13
  import { actionable } from './service.js';
13
14
  /**
@@ -382,6 +383,7 @@ export class BrowserIPCServer {
382
383
  actor: request.actor,
383
384
  launchId: request.launchId,
384
385
  sessionId: request.sessionId,
386
+ fleetRemote: request.fleetRemote,
385
387
  createIfMissing,
386
388
  title: request.title,
387
389
  url: request.url,
@@ -396,6 +398,9 @@ export class BrowserIPCServer {
396
398
  };
397
399
  }
398
400
  request.task = resolved.task.name;
401
+ if (resolved.created && resolved.picked) {
402
+ request.picked = resolved.picked;
403
+ }
399
404
  return undefined;
400
405
  }
401
406
  catch (err) {
@@ -421,6 +426,22 @@ export class BrowserIPCServer {
421
426
  case 'version': {
422
427
  return { ok: true, version: getCliVersion() };
423
428
  }
429
+ case 'show': {
430
+ // Task-less by design — see BrowserService.showUrl. Absent from both
431
+ // PAGE_CREATE_VERBS and PAGE_RESOLVE_VERBS, so bindTask never ran above
432
+ // and this request carries no task.
433
+ if (!request.url) {
434
+ return { ok: false, error: actionable('URL required.', 'Next: agents browser navigate <url>') };
435
+ }
436
+ if (!request.profile) {
437
+ return { ok: false, error: actionable('Profile required.', 'Next: agents browser profiles list') };
438
+ }
439
+ const shown = await this.service.showUrl(request.profile, request.url, {
440
+ fleetRemote: request.fleetRemote,
441
+ actor: request.actor,
442
+ });
443
+ return { ok: true, tabId: shown.tabId, message: shown.picked };
444
+ }
424
445
  case 'start': {
425
446
  if (!request.profile) {
426
447
  return { ok: false, error: actionable('Profile required.', 'Next: agents browser profiles list') };
@@ -434,6 +455,7 @@ export class BrowserIPCServer {
434
455
  actor: request.actor,
435
456
  launchId: request.launchId,
436
457
  sessionId: request.sessionId,
458
+ fleetRemote: request.fleetRemote,
437
459
  title: request.title,
438
460
  });
439
461
  return {
@@ -442,10 +464,11 @@ export class BrowserIPCServer {
442
464
  tabId: result.tabId,
443
465
  windowTargetId: result.windowId,
444
466
  skill: result.skill,
467
+ message: result.picked,
445
468
  };
446
469
  }
447
470
  // The out-of-process seam onto the abandoned-task reaper: the daemon
448
- // owns the live BrowserService, so a CLI verb (`agents browser gc`) can
471
+ // owns the live BrowserService, so a CLI verb (`agents browser prune`) can
449
472
  // only reach `reapAbandoned` through IPC. The daemon's own periodic tick
450
473
  // calls the service method directly.
451
474
  case 'gc': {
@@ -514,7 +537,8 @@ export class BrowserIPCServer {
514
537
  };
515
538
  }
516
539
  const result = await this.service.navigate(request.task, request.url, request.profile);
517
- return { ok: true, tabId: result.tabId, task: request.task };
540
+ const picked = request.picked;
541
+ return { ok: true, tabId: result.tabId, task: request.task, message: picked };
518
542
  }
519
543
  case 'tab-add': {
520
544
  if (!request.task || !request.url) {
@@ -855,15 +879,22 @@ export async function sendIPCRequest(request, opts = {}) {
855
879
  * Fill actor / launchId / sessionId from the calling process when the request
856
880
  * left them blank. Explicit values on the request always win.
857
881
  */
858
- export function stampCallerIdentity(request) {
859
- if (request.actor && request.launchId && request.sessionId)
860
- return request;
882
+ export function stampCallerIdentity(request, env = process.env) {
883
+ // The consent marker is stamped FIRST, above the identity early-return: a
884
+ // request that already carries a full identity (every streamed request does)
885
+ // would otherwise skip stamping entirely and reach the daemon unmarked.
886
+ // OR-ed, never overwritten — a client may assert the marker but must not be
887
+ // able to clear one this environment sets.
888
+ const fleetRemote = isFleetRemoteInvocation(env) || request.fleetRemote === true;
889
+ const marked = request.fleetRemote === fleetRemote ? request : { ...request, fleetRemote };
890
+ if (marked.actor && marked.launchId && marked.sessionId)
891
+ return marked;
861
892
  const id = resolveCallerIdentity();
862
893
  return {
863
- ...request,
864
- actor: request.actor ?? id.actor,
865
- launchId: request.launchId ?? id.launchId,
866
- sessionId: request.sessionId ?? id.sessionId,
894
+ ...marked,
895
+ actor: marked.actor ?? id.actor,
896
+ launchId: marked.launchId ?? id.launchId,
897
+ sessionId: marked.sessionId ?? id.sessionId,
867
898
  };
868
899
  }
869
900
  /**