@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,194 @@
1
+ /**
2
+ * Shared on-demand download + verification for the macOS native helper `.app`
3
+ * bundles that ship as signed + notarized GitHub release assets on each helper's
4
+ * OWN tag (`ComputerHelper.app`, `MenubarHelper.app`, `Agents CLI.app`) — see
5
+ * `helper-versions.ts` for why they are no longer keyed to the CLI's tag.
6
+ *
7
+ * A `.app` is a directory, so each asset is a zip (`ditto -c -k --keepParent`);
8
+ * a fresh `npm i -g` machine with no bundled copy fetches the asset for the
9
+ * pinned helper version, verifies its sha256 against the published `.sha256`,
10
+ * then verifies the code signature (Developer ID Team + notarization — and, for
11
+ * the menu-bar helper, its designated requirement) before it is ever installed.
12
+ *
13
+ * `computer/download.ts` (ComputerHelper) and `menubar/download-menubar.ts`
14
+ * (MenubarHelper) are thin per-helper wrappers over the primitives here — one
15
+ * download+verify machinery, two specs — so a fix to the verify/download logic
16
+ * lands for both helpers at once.
17
+ */
18
+ import { execFileSync, spawnSync } from 'node:child_process';
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { Readable } from 'node:stream';
22
+ import { pipeline } from 'node:stream/promises';
23
+ import { getCacheDir } from './state.js';
24
+ import { parseSha256Asset, sha256File } from './sha256-asset.js';
25
+ import { helperTag } from './helper-versions.js';
26
+ /** GitHub repo whose `v<version>` releases carry the helper assets. */
27
+ export const HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
28
+ /** Apple Developer ID Team every helper must be signed by ("Developer ID
29
+ * Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
30
+ export const EXPECTED_TEAM_ID = '2HTP252L87';
31
+ /** Cache dir for a downloaded helper, one subdir per release tag. */
32
+ export function helperCacheDir(spec, version) {
33
+ return path.join(getCacheDir(), ...spec.cacheSubdir, `v${version}`);
34
+ }
35
+ /**
36
+ * Release-asset URLs for one helper build, at that HELPER's own tag.
37
+ *
38
+ * The tag is `<helper>/v<version>` (e.g. `menubar/v1.0.0`), never the CLI's
39
+ * `v<cliVersion>`. Keying these to the CLI tag is what forced every CLI release
40
+ * to re-stage every helper asset, and simultaneously made a helper fix
41
+ * unreachable without a CLI release — see `helper-versions.ts` for the full why.
42
+ *
43
+ * `version` is a HELPER version. Passing a CLI version here yields a tag that
44
+ * does not exist; callers default it from {@link helperFloor}.
45
+ */
46
+ export function helperAssetUrls(spec, version) {
47
+ const base = `https://github.com/${HELPER_RELEASE_REPO}/releases/download/${helperTag(spec.helper, version)}`;
48
+ // Asset names must not contain spaces: GitHub rewrites a space to a dot on
49
+ // upload, so a spaced name is requested at a URL that 404s forever. Caught on
50
+ // `Agents CLI.app.zip`, which GitHub served as `Agents.CLI.app.zip`. Use an
51
+ // underscore -- GitHub preserves it verbatim.
52
+ if (spec.assetName.includes(' ')) {
53
+ throw new Error(`helper asset name ${JSON.stringify(spec.assetName)} contains a space; GitHub rewrites it to a dot on `
54
+ + 'upload, so this URL would always 404. Use an underscore instead.');
55
+ }
56
+ return { zip: `${base}/${spec.assetName}`, sha256: `${base}/${spec.assetName}.sha256` };
57
+ }
58
+ /** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
59
+ * emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
60
+ export function parseTeamId(codesignInfo) {
61
+ return codesignInfo.match(/TeamIdentifier=([A-Z0-9]+)/)?.[1] ?? null;
62
+ }
63
+ /** Read the bundle's designated requirement string (`codesign -d --requirements -`).
64
+ * codesign writes it to stdout, but capture both streams so a diagnostic on
65
+ * stderr can never make the pin-check read empty and falsely pass. */
66
+ export function readDesignatedRequirement(appPath) {
67
+ const r = spawnSync('/usr/bin/codesign', ['-d', '--requirements', '-', appPath], { encoding: 'utf8' });
68
+ return `${r.stdout ?? ''}${r.stderr ?? ''}`;
69
+ }
70
+ /**
71
+ * Pure predicate (no I/O) behind the DR pin: does the requirement string pin
72
+ * BOTH the expected CFBundleIdentifier and the Developer ID Team? Returns null
73
+ * when it does, else an actionable error message naming the missing pin. Kept
74
+ * pure so the truth table is unit-testable without a signed bundle or codesign.
75
+ * Mirrors the release-time gate in `scripts/verify-menubar-helper.sh` (identifier
76
+ * + team substring check against the same requirement string).
77
+ */
78
+ export function checkDesignatedRequirement(req, bundleId, teamId) {
79
+ const trimmed = req.trim();
80
+ const shown = trimmed ? JSON.stringify(trimmed.slice(0, 200)) : '<none>';
81
+ if (!trimmed.includes(`identifier "${bundleId}"`)) {
82
+ return (`helper designated requirement does not pin bundle id "${bundleId}" (read: ${shown}). ` +
83
+ `Installing it would revoke the Accessibility grant. Refusing to install.`);
84
+ }
85
+ if (!trimmed.includes(teamId)) {
86
+ return (`helper designated requirement does not pin Developer ID Team ${teamId} (read: ${shown}). ` +
87
+ `Installing it would revoke the Accessibility grant. Refusing to install.`);
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * Verify the bundle's designated requirement pins the expected CFBundleIdentifier
93
+ * AND Developer ID Team. macOS re-validates each new version against this stored
94
+ * requirement to keep an Accessibility grant alive across upgrades; a downloaded
95
+ * bundle whose DR drops either pin would silently revoke that grant on the next
96
+ * paste, so refuse it.
97
+ */
98
+ export function verifyDesignatedRequirement(appPath, bundleId, teamId) {
99
+ const err = checkDesignatedRequirement(readDesignatedRequirement(appPath), bundleId, teamId);
100
+ if (err)
101
+ throw new Error(err);
102
+ }
103
+ /**
104
+ * Verify a helper `.app` bundle is intact, signed by the expected Developer ID
105
+ * Team, (for the menu-bar helper) pinned by its designated requirement, and
106
+ * notarized (Gatekeeper-accepted). Throws with an actionable message on any
107
+ * failure — a downloaded bundle is never trusted without this.
108
+ */
109
+ export function verifyHelperApp(appPath, spec) {
110
+ // 1. Structural + signature integrity.
111
+ try {
112
+ execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'pipe' });
113
+ }
114
+ catch (e) {
115
+ throw new Error(`code signature invalid for ${appPath}: ${e.message}`);
116
+ }
117
+ // 2. Team identity — must be our Developer ID, not some other valid signer.
118
+ // `codesign -dv` writes its details to STDERR even on success (exit 0), so we
119
+ // must read stderr, not stdout. spawnSync captures both streams regardless of
120
+ // exit code; execFileSync would return only (empty) stdout and the Team check
121
+ // would falsely reject every validly-signed helper.
122
+ const dv = spawnSync('/usr/bin/codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf8' });
123
+ const info = `${dv.stdout ?? ''}${dv.stderr ?? ''}`;
124
+ const team = parseTeamId(info);
125
+ if (team !== spec.expectedTeamId) {
126
+ throw new Error(`helper signed by unexpected Team (${team ?? 'none'}), expected ${spec.expectedTeamId}. Refusing to install.`);
127
+ }
128
+ // 3. Designated-requirement pin (menu-bar helper only) — before the Gatekeeper
129
+ // assessment so a DR mismatch fails with its specific, actionable message
130
+ // rather than a generic Gatekeeper rejection.
131
+ if (spec.expectedBundleId) {
132
+ verifyDesignatedRequirement(appPath, spec.expectedBundleId, spec.expectedTeamId);
133
+ }
134
+ // 4. Notarization / Gatekeeper — confirms Apple stapled a notarization ticket.
135
+ try {
136
+ execFileSync('/usr/sbin/spctl', ['--assess', '--type', 'execute', '--verbose', appPath], { stdio: 'pipe' });
137
+ }
138
+ catch (e) {
139
+ throw new Error(`helper is not notarized / rejected by Gatekeeper: ${e.message}. Refusing to install.`);
140
+ }
141
+ }
142
+ /**
143
+ * Download a helper release asset for `version`, verify sha256, extract the
144
+ * `.app`, and verify its signature (+ DR pin when the spec requires one).
145
+ * Returns the path to the extracted bundle. A missing asset is a hard error
146
+ * naming the exact tag — never a silent fallback to another release.
147
+ */
148
+ export async function downloadHelperApp(spec, version) {
149
+ const dir = helperCacheDir(spec, version);
150
+ const cachedApp = path.join(dir, spec.appName);
151
+ if (fs.existsSync(cachedApp)) {
152
+ // Re-verify a cached bundle cheaply; a tampered cache must not be trusted.
153
+ verifyHelperApp(cachedApp, spec);
154
+ return cachedApp;
155
+ }
156
+ // Name the HELPER's tag, not `v${version}`: after the per-helper split that
157
+ // string was a tag that does not exist, so a 404 pointed the reader at the
158
+ // wrong place to look.
159
+ const tag = helperTag(spec.helper, version);
160
+ const { zip: zipUrl, sha256: shaUrl } = helperAssetUrls(spec, version);
161
+ const missing = (status, url) => new Error(`no ${spec.assetName} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
162
+ `The macOS helper ships as a GitHub release asset on its own helper tag; ` +
163
+ `from a repo checkout you can build it locally instead: ${spec.localBuildHint}`);
164
+ // Checksum first: it is tiny and 404s fast when the tag has no assets.
165
+ const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
166
+ if (!shaRes.ok)
167
+ throw missing(shaRes.status, shaUrl);
168
+ const expected = parseSha256Asset(await shaRes.text());
169
+ console.error(`Downloading ${spec.assetName} ${tag} from GitHub releases...`);
170
+ const zipRes = await fetch(zipUrl, { signal: AbortSignal.timeout(15 * 60_000) });
171
+ if (!zipRes.ok || !zipRes.body)
172
+ throw missing(zipRes.status, zipUrl);
173
+ fs.mkdirSync(dir, { recursive: true });
174
+ const partial = path.join(dir, `${spec.assetName}.download`);
175
+ try {
176
+ await pipeline(Readable.fromWeb(zipRes.body), fs.createWriteStream(partial));
177
+ const actual = await sha256File(partial);
178
+ if (actual !== expected) {
179
+ throw new Error(`sha256 mismatch for ${zipUrl}: expected ${expected}, got ${actual}`);
180
+ }
181
+ // Extract the zip (created with `ditto -c -k --keepParent`, so it contains
182
+ // <appName>/ at top level) into the version cache dir.
183
+ fs.rmSync(cachedApp, { recursive: true, force: true });
184
+ execFileSync('/usr/bin/ditto', ['-x', '-k', partial, dir], { stdio: 'pipe' });
185
+ if (!fs.existsSync(cachedApp)) {
186
+ throw new Error(`extracted asset did not contain ${spec.appName}`);
187
+ }
188
+ verifyHelperApp(cachedApp, spec);
189
+ }
190
+ finally {
191
+ fs.rmSync(partial, { force: true });
192
+ }
193
+ return cachedApp;
194
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Which build of each native helper this CLI expects, and where its release
3
+ * lives — the module that decouples helper distribution from CLI releases.
4
+ *
5
+ * WHY THIS EXISTS. `helperAssetUrls` used to build
6
+ * `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
7
+ * own tag. That is coupled in BOTH directions and each one hurts:
8
+ *
9
+ * - A CLI release had to re-stage every helper asset onto its new tag, or the
10
+ * download 404'd. That is what chained an ordinary release to a Mac.
11
+ * - A helper fix could not reach anyone without cutting a CLI release, because
12
+ * no URL existed that a shipped CLI would look at.
13
+ *
14
+ * Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
15
+ * records the FLOOR — the build this CLI was tested against. A floor, not an
16
+ * exact pin: a pin would fix the first problem and leave the second, since an
17
+ * improved helper would still need a CLI release to be reachable. The floor is
18
+ * what a channel manifest can later resolve *upward* from (newest >= floor),
19
+ * without another rewrite of the download path.
20
+ *
21
+ * The floor is also the offline answer: it names an IMMUTABLE tag that always
22
+ * exists, so a machine that cannot reach a manifest still has exactly one
23
+ * correct URL rather than a guess.
24
+ *
25
+ * Bumping an entry here is a deliberate act — it says "this CLI has been tested
26
+ * against that helper build". It is not a mirror of the CLI version and must
27
+ * never be derived from `getCliVersion()`.
28
+ */
29
+ /** The helpers that have their own release train. */
30
+ export type HelperName = 'menubar' | 'keychain' | 'computer-mac' | 'computer-win';
31
+ /** One helper's release identity. */
32
+ export interface HelperRelease {
33
+ /** Tag prefix — the release is `<tagPrefix>/v<version>`. */
34
+ tagPrefix: string;
35
+ /**
36
+ * Lowest helper build this CLI is known to work with. Resolution may pick a
37
+ * NEWER build; it must never pick an older one.
38
+ */
39
+ floor: string;
40
+ }
41
+ /**
42
+ * Floors, by helper.
43
+ *
44
+ * `menubar` and `keychain` start at 1.0.0 — the first builds published under
45
+ * their own tags rather than the CLI's. They are not "version 1 of the helper";
46
+ * they are version 1 of its independent release train.
47
+ */
48
+ export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
49
+ /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
50
+ export declare function helperTag(helper: HelperName, version: string): string;
51
+ /** The floor build for one helper — the version used when nothing resolves higher. */
52
+ export declare function helperFloor(helper: HelperName): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which build of each native helper this CLI expects, and where its release
3
+ * lives — the module that decouples helper distribution from CLI releases.
4
+ *
5
+ * WHY THIS EXISTS. `helperAssetUrls` used to build
6
+ * `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
7
+ * own tag. That is coupled in BOTH directions and each one hurts:
8
+ *
9
+ * - A CLI release had to re-stage every helper asset onto its new tag, or the
10
+ * download 404'd. That is what chained an ordinary release to a Mac.
11
+ * - A helper fix could not reach anyone without cutting a CLI release, because
12
+ * no URL existed that a shipped CLI would look at.
13
+ *
14
+ * Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
15
+ * records the FLOOR — the build this CLI was tested against. A floor, not an
16
+ * exact pin: a pin would fix the first problem and leave the second, since an
17
+ * improved helper would still need a CLI release to be reachable. The floor is
18
+ * what a channel manifest can later resolve *upward* from (newest >= floor),
19
+ * without another rewrite of the download path.
20
+ *
21
+ * The floor is also the offline answer: it names an IMMUTABLE tag that always
22
+ * exists, so a machine that cannot reach a manifest still has exactly one
23
+ * correct URL rather than a guess.
24
+ *
25
+ * Bumping an entry here is a deliberate act — it says "this CLI has been tested
26
+ * against that helper build". It is not a mirror of the CLI version and must
27
+ * never be derived from `getCliVersion()`.
28
+ */
29
+ /**
30
+ * Floors, by helper.
31
+ *
32
+ * `menubar` and `keychain` start at 1.0.0 — the first builds published under
33
+ * their own tags rather than the CLI's. They are not "version 1 of the helper";
34
+ * they are version 1 of its independent release train.
35
+ */
36
+ export const HELPER_RELEASES = {
37
+ menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
38
+ keychain: { tagPrefix: 'keychain', floor: '1.0.0' },
39
+ 'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
40
+ // The Windows helper is a bare .exe, not an .app bundle, so it does not share
41
+ // helper-download.ts's zip/codesign/notarize machinery -- but it has the same
42
+ // URL problem, so it shares the tag namespace and the floor.
43
+ 'computer-win': { tagPrefix: 'computer-win', floor: '1.0.0' },
44
+ };
45
+ /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
46
+ export function helperTag(helper, version) {
47
+ const release = HELPER_RELEASES[helper];
48
+ if (!release)
49
+ throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
50
+ return `${release.tagPrefix}/v${version}`;
51
+ }
52
+ /** The floor build for one helper — the version used when nothing resolves higher. */
53
+ export function helperFloor(helper) {
54
+ const release = HELPER_RELEASES[helper];
55
+ if (!release)
56
+ throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
57
+ return release.floor;
58
+ }
@@ -275,6 +275,25 @@ def should_fire():
275
275
  if not tn or tn not in allowed:
276
276
  return False
277
277
 
278
+ v = m.get("permission_mode")
279
+ if v is not None:
280
+ allowed = arr(v)
281
+ if allowed:
282
+ pm = inp.get("permission_mode") or inp.get("permissionMode")
283
+ if pm and pm not in allowed:
284
+ return False
285
+
286
+ v = m.get("permission_mode_not")
287
+ if v is not None:
288
+ denied = arr(v)
289
+ if denied:
290
+ # Negative form: naming the mode to SKIP keeps every unknown mode
291
+ # firing, so the failure direction is "ran unnecessarily", never
292
+ # "did not run". Absence of a mode fires, same as permission_mode.
293
+ pm = inp.get("permission_mode") or inp.get("permissionMode")
294
+ if pm and pm in denied:
295
+ return False
296
+
278
297
  v = m.get("tool_args_match")
279
298
  if v is not None:
280
299
  ta = inp.get("tool_args")
@@ -3255,8 +3255,8 @@ function resolveSessionTrackerRoot() {
3255
3255
  if (fs.existsSync(path.join(built, 'dist', 'hook.sh'))) {
3256
3256
  return built;
3257
3257
  }
3258
- // Source layout: apps/cli/src/lib/hooks/ -> repo root -> packages/session-tracker
3259
- const source = path.resolve(here, '..', '..', '..', '..', '..', 'packages', 'session-tracker');
3258
+ // Source layout: cli/src/lib/hooks/ -> repo root (4 up) -> packages/session-tracker
3259
+ const source = path.resolve(here, '..', '..', '..', '..', 'packages', 'session-tracker');
3260
3260
  if (fs.existsSync(path.join(source, 'src', 'hook.sh'))) {
3261
3261
  return source;
3262
3262
  }
@@ -20,6 +20,9 @@ export interface HookInput {
20
20
  tool_name?: string;
21
21
  tool_args?: unknown;
22
22
  cwd?: string;
23
+ permission_mode?: string;
24
+ /** Grok-style camelCase spelling of permission_mode. */
25
+ permissionMode?: string;
23
26
  }
24
27
  export declare function isSafeHookRegex(source: string): boolean;
25
28
  /**
@@ -131,6 +131,31 @@ export function shouldFire(matches, input) {
131
131
  return false;
132
132
  }
133
133
  }
134
+ if (matches.permission_mode !== undefined) {
135
+ const allowed = arrayOf(matches.permission_mode);
136
+ if (allowed.length > 0) {
137
+ // Fail-open on absence: only some harnesses (Claude Code) report the
138
+ // live mode. An input with no mode field passes; an explicit value not
139
+ // in the allowed list skips.
140
+ const mode = input.permission_mode || input.permissionMode;
141
+ if (mode && !allowed.includes(mode))
142
+ return false;
143
+ }
144
+ }
145
+ if (matches.permission_mode_not !== undefined) {
146
+ const denied = arrayOf(matches.permission_mode_not);
147
+ if (denied.length > 0) {
148
+ // The negative form exists because the positive one cannot express
149
+ // "everywhere except plan" without enumerating every other mode — and an
150
+ // enumeration silently stops firing the moment a harness adds or renames
151
+ // one, which for a guard means it quietly stops guarding. Naming the mode
152
+ // to skip keeps every unknown mode firing, so the failure direction is
153
+ // "ran unnecessarily", never "did not run".
154
+ const mode = input.permission_mode || input.permissionMode;
155
+ if (mode && denied.includes(mode))
156
+ return false;
157
+ }
158
+ }
134
159
  if (matches.tool_args_match !== undefined) {
135
160
  const serialized = typeof input.tool_args === 'string'
136
161
  ? input.tool_args
@@ -6,7 +6,7 @@
6
6
  * codex/grok/gemini `auth.json`-style files — so the box booted logged-in. Every
7
7
  * one of those is a **rotating, interactive OAuth / session credential**, and
8
8
  * copying it between devices is forbidden by the fleet-auth contract
9
- * (`docs/specifications.md` SING-1b, `docs/credential-management.md` invariant 2):
9
+ * (`docs/specifications.md` SING-1b, `docs/secrets.md`):
10
10
  * a shared refresh token rotates server-side on the next refresh and invalidates
11
11
  * every other copy — the "droid collapsed 10 boxes to 1 overnight" failure.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * codex/grok/gemini `auth.json`-style files — so the box booted logged-in. Every
7
7
  * one of those is a **rotating, interactive OAuth / session credential**, and
8
8
  * copying it between devices is forbidden by the fleet-auth contract
9
- * (`docs/specifications.md` SING-1b, `docs/credential-management.md` invariant 2):
9
+ * (`docs/specifications.md` SING-1b, `docs/secrets.md`):
10
10
  * a shared refresh token rotates server-side on the next refresh and invalidates
11
11
  * every other copy — the "droid collapsed 10 boxes to 1 overnight" failure.
12
12
  *
@@ -37,8 +37,14 @@ export declare function withActorEnv(env?: Record<string, string>): Record<strin
37
37
  * only reach the spawned agent's env and the remote `run auto` would re-pick.
38
38
  * Shared by the interactive (runInteractiveOnHost) and detached
39
39
  * (launchDetached) paths so both behave identically.
40
+ *
41
+ * `extra` carries the markers that are true of ONE path rather than both — the
42
+ * interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
43
+ * to decide it must detach. It goes through this builder rather than being
44
+ * concatenated on at the call site so every remote env marker is exported the
45
+ * same way, in one place.
40
46
  */
41
- export declare function remoteRunShellPrelude(agent: string): string;
47
+ export declare function remoteRunShellPrelude(agent: string, extra?: Record<string, string>): string;
42
48
  /**
43
49
  * Launch a detached login-shell command in its own Unix session/process group.
44
50
  *
@@ -20,7 +20,7 @@ import { followHostTask } from './progress.js';
20
20
  import { wrapHostCommandWithCredentials } from './credentials.js';
21
21
  import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
22
22
  import { deriveMirroredCwd, homeRemainder, remoteCdPrefix } from '../project-root.js';
23
- import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV } from '../types.js';
23
+ import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV, REMOTE_INTERACTIVE_ENV } from '../types.js';
24
24
  // The home-relative portability helpers live in project-root.js (the canonical
25
25
  // home-relative conversion home), reused by the interactive `agents ssh` login
26
26
  // shell builder (devices/connect.ts) as well as this dispatch layer. Re-exported
@@ -75,10 +75,16 @@ export function withActorEnv(env) {
75
75
  * only reach the spawned agent's env and the remote `run auto` would re-pick.
76
76
  * Shared by the interactive (runInteractiveOnHost) and detached
77
77
  * (launchDetached) paths so both behave identically.
78
+ *
79
+ * `extra` carries the markers that are true of ONE path rather than both — the
80
+ * interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
81
+ * to decide it must detach. It goes through this builder rather than being
82
+ * concatenated on at the call site so every remote env marker is exported the
83
+ * same way, in one place.
78
84
  */
79
- export function remoteRunShellPrelude(agent) {
85
+ export function remoteRunShellPrelude(agent, extra = {}) {
80
86
  const guard = agent === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {};
81
- const exports = posixEnvExports(withActorEnv(guard));
87
+ const exports = posixEnvExports(withActorEnv({ ...guard, ...extra }));
82
88
  return exports ? `${exports}; ` : '';
83
89
  }
84
90
  /**
@@ -490,7 +496,13 @@ export async function runInteractiveOnHost(host, opts) {
490
496
  // Forward actor provenance so the interactive remote run inherits it rather
491
497
  // than re-resolving from this box's SSH_CONNECTION (RUSH-2028); a `run auto`
492
498
  // dispatch also gets the chain-hop guard (remoteRunShellPrelude).
493
- const prelude = remoteRunShellPrelude(opts.agent);
499
+ //
500
+ // REMOTE_INTERACTIVE_ENV rides the same prelude and is what makes the remote
501
+ // agent DETACHED: its stdio is this ssh link, so without the tmux wrap a
502
+ // blink SIGHUPs it and the in-flight turn is gone — while reconnect.ts is
503
+ // built to re-attach a pane it assumes survived (RUSH-3125). Set here, on the
504
+ // interactive path only: `launchDetached` already setsids the headless one.
505
+ const prelude = remoteRunShellPrelude(opts.agent, { [REMOTE_INTERACTIVE_ENV]: '1' });
494
506
  let remoteCmd = `${prelude}${cwd}${invocation}`;
495
507
  if (opts.copyCreds) {
496
508
  remoteCmd = wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds);
@@ -501,7 +513,14 @@ export async function runInteractiveOnHost(host, opts) {
501
513
  const credHostKeyOpts = opts.copyCreds ? hostKeyCheckingOpts(true) : undefined;
502
514
  return sshStream(target, remoteCmd, {
503
515
  tty: process.stdin.isTTY,
504
- multiplex: !opts.copyCreds,
516
+ // NOT multiplexed. `ControlPath=cm-%C` hashes only local host / remote host
517
+ // / port / user, so every agent tab pointed at a peer shares ONE master —
518
+ // and OpenSSH closes every channel on it the instant that master dies. One
519
+ // blink therefore ejected all six tabs on a box at once, which is what made
520
+ // a brief outage read as "all my agents exited" (RUSH-3125). The handshake
521
+ // this gives up is a one-time ~200ms on a session that runs for hours;
522
+ // probes and fan-outs keep the shared master, where it actually pays.
523
+ multiplex: false,
505
524
  hostKeyOpts: credHostKeyOpts,
506
525
  extraSshArgs: hostIdentityArgs(host),
507
526
  });
@@ -11,7 +11,7 @@
11
11
  * supervisor outlives a dropped connection.
12
12
  *
13
13
  * Commands with their own richer `--device` handling (`run`/`sessions`/`feed`/
14
- * `computer`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
14
+ * `computer`/`browser`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
15
15
  * fall through to their local actions. Everything else either routes via this
16
16
  * table or, when `--device` is present, exits with a clear
17
17
  * "not supported" message — never commander's raw `unknown option`.
@@ -98,20 +98,13 @@ export declare function maybeRunOnHost(command: string, allArgs: string[], opts?
98
98
  * the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
99
99
  * than being the first argv token.
100
100
  *
101
- * `agents browser … --device <box>` routes through {@link maybeRunOnHost} in
102
- * index.ts, but the standalone `browser` binary never enters index.ts — so without
103
- * this it dropped `--device` entirely (commander errored with `unknown option
104
- * '--device'`). This wires the identical routing: it synthesizes the implicit command
105
- * token (`browser`) at the front of the argv so the remote invocation builds as
106
- * `agents browser …`, delegates to {@link maybeRunOnHost}, and returns `true` when
107
- * it dispatched the run to a remote (the local program must then NOT parse).
108
- *
109
- * On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
110
- * args minus the routing flags — never the synthetic command token — so the
111
- * standalone commander program parses cleanly and never sees an unknown `--device`.
112
- * When no routing flag is present it leaves `process.argv` untouched (the common
113
- * case), so an unrelated flag like `--no-tty` is never stripped from a purely local
114
- * run.
101
+ * `agents computer … --device <box>` still routes through {@link maybeRunOnHost}
102
+ * in index.ts, but the standalone `browser` / `computer` binaries never enter
103
+ * index.ts. Browser now owns `--device` (start binds the task; later verbs
104
+ * reject it), so this function leaves argv intact for OWN_HOST commands and
105
+ * only strips routing flags on `--help`/`--version`. Other standalone commands
106
+ * still synthesize the implicit command token and delegate to
107
+ * {@link maybeRunOnHost}.
115
108
  *
116
109
  * @param command the fixed command name the binary stands for (`'browser'`).
117
110
  */
@@ -11,7 +11,7 @@
11
11
  * supervisor outlives a dropped connection.
12
12
  *
13
13
  * Commands with their own richer `--device` handling (`run`/`sessions`/`feed`/
14
- * `computer`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
14
+ * `computer`/`browser`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
15
15
  * fall through to their local actions. Everything else either routes via this
16
16
  * table or, when `--device` is present, exits with a clear
17
17
  * "not supported" message — never commander's raw `unknown option`.
@@ -28,6 +28,7 @@ import { resolveRemoteOsSync } from './remote-os.js';
28
28
  // 6–7× cheaper graph (RUSH-2374 proposal 2).
29
29
  import { machineId } from '../machine-id.js';
30
30
  import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
31
+ import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../devices/interactive-host.js';
31
32
  import { flagValue, hasHostRoutingFlag } from './routing-flag.js';
32
33
  import { loadDevices } from '../devices/registry.js';
33
34
  import { isSelfHost } from '../devices/self-host.js';
@@ -100,7 +101,6 @@ export const REMOTE_PASSTHROUGH = {
100
101
  tmux: {},
101
102
  watchdog: {},
102
103
  factory: {},
103
- browser: {},
104
104
  };
105
105
  /**
106
106
  * Commands that register and interpret `--device` themselves — must
@@ -115,6 +115,7 @@ export const OWN_HOST_COMMANDS = new Set([
115
115
  'sessions',
116
116
  'feed',
117
117
  'computer',
118
+ 'browser', // `--device` on start binds the task; later verbs resolve it from the task
118
119
  'secrets',
119
120
  'accounts', // `accounts sync --device` names the destination, not remote routing
120
121
  'logs',
@@ -527,6 +528,21 @@ export async function maybeRunOnHost(command, allArgs, opts) {
527
528
  process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
528
529
  hostName = plan.host;
529
530
  }
531
+ // `interactive` is the second affinity sentinel: the box the human sits at
532
+ // (`interactive.host`). Resolved here, ahead of the isSelfHost check below, for
533
+ // the same reason `auto` is — a pin naming THIS machine must run locally rather
534
+ // than self-SSH. It never falls back to the local box when unset: rendering to
535
+ // a screen nobody is watching is the exact failure the sentinel prevents.
536
+ if (isDeviceInteractive(hostName)) {
537
+ const pinned = resolveInteractiveDevice();
538
+ if (!pinned) {
539
+ process.stderr.write(`${interactiveUnsetError()}\n`);
540
+ process.exitCode = 1;
541
+ return true;
542
+ }
543
+ process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
544
+ hostName = pinned;
545
+ }
530
546
  // Running against your own machine is just a local run — skip the SSH round-trip.
531
547
  // Match EVERY identity the box answers to (short id, loopback, tailscale
532
548
  // dnsName), not just machineId() — a `--device <self-dnsName>` used to slip past a
@@ -593,20 +609,13 @@ export async function maybeRunOnHost(command, allArgs, opts) {
593
609
  * the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
594
610
  * than being the first argv token.
595
611
  *
596
- * `agents browser … --device <box>` routes through {@link maybeRunOnHost} in
597
- * index.ts, but the standalone `browser` binary never enters index.ts — so without
598
- * this it dropped `--device` entirely (commander errored with `unknown option
599
- * '--device'`). This wires the identical routing: it synthesizes the implicit command
600
- * token (`browser`) at the front of the argv so the remote invocation builds as
601
- * `agents browser …`, delegates to {@link maybeRunOnHost}, and returns `true` when
602
- * it dispatched the run to a remote (the local program must then NOT parse).
603
- *
604
- * On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
605
- * args minus the routing flags — never the synthetic command token — so the
606
- * standalone commander program parses cleanly and never sees an unknown `--device`.
607
- * When no routing flag is present it leaves `process.argv` untouched (the common
608
- * case), so an unrelated flag like `--no-tty` is never stripped from a purely local
609
- * run.
612
+ * `agents computer … --device <box>` still routes through {@link maybeRunOnHost}
613
+ * in index.ts, but the standalone `browser` / `computer` binaries never enter
614
+ * index.ts. Browser now owns `--device` (start binds the task; later verbs
615
+ * reject it), so this function leaves argv intact for OWN_HOST commands and
616
+ * only strips routing flags on `--help`/`--version`. Other standalone commands
617
+ * still synthesize the implicit command token and delegate to
618
+ * {@link maybeRunOnHost}.
610
619
  *
611
620
  * @param command the fixed command name the binary stands for (`'browser'`).
612
621
  */
@@ -616,6 +625,16 @@ export async function maybeRunStandaloneOnHost(command, opts) {
616
625
  // local run keeps every flag it passed.
617
626
  if (!hasHostRoutingFlag(rawArgs))
618
627
  return false;
628
+ // Commands that interpret `--device` themselves (browser start binds the
629
+ // task→device index; later browser verbs reject the flag) must see it.
630
+ // Help/version still strip so `browser --device x --help` parses.
631
+ if (OWN_HOST_COMMANDS.has(command)) {
632
+ const helpOrVersion = rawArgs.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V');
633
+ if (helpOrVersion) {
634
+ process.argv = [process.argv[0], process.argv[1], ...stripRoutingFlags(rawArgs, STRIP_SPECS)];
635
+ }
636
+ return false;
637
+ }
619
638
  // Keep --help/--version local (docs must work without a reachable host), mirroring
620
639
  // index.ts's `helpOrVersionRequested` guard, but still strip the routing flags
621
640
  // below so commander doesn't choke on them.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
5
5
  * registry.json) into the host pool behind the same `HostProvider` seam as
6
- * `local` — the "tailscale provider" fast-follow named in docs/hosts.md. With
6
+ * `local` — the Tailscale-backed device provider described in docs/fleet.md. With
7
7
  * it, a machine registered once via `agents devices sync` becomes a resolvable
8
8
  * `--device`/`--device` dispatch target, participates in capability routing, and
9
9
  * is enumerable by target pickers — not just resolvable by exact name.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
5
5
  * registry.json) into the host pool behind the same `HostProvider` seam as
6
- * `local` — the "tailscale provider" fast-follow named in docs/hosts.md. With
6
+ * `local` — the Tailscale-backed device provider described in docs/fleet.md. With
7
7
  * it, a machine registered once via `agents devices sync` becomes a resolvable
8
8
  * `--device`/`--device` dispatch target, participates in capability routing, and
9
9
  * is enumerable by target pickers — not just resolvable by exact name.