@phnx-labs/agents-cli 1.22.47 → 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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -3,48 +3,51 @@
3
3
  * ("ComputerHelper.app").
4
4
  *
5
5
  * The helper is a signed + notarized universal `.app` bundle published as a
6
- * GitHub release asset per tagged CLI version the same distribution model as
7
- * the Windows helper (see `lib/computer/ssh-tunnel.ts`). A fresh `npm i -g` machine has
8
- * no local build, so `agents computer setup` / `agents setup computer` fetch the
9
- * asset for the running CLI version, verify its sha256 against the published
6
+ * GitHub release asset on its own `computer-mac/v<x.y.z>` tag (see
7
+ * `helper-versions.ts`). The Windows helper (`lib/computer/ssh-tunnel.ts`) does
8
+ * NOT yet follow this model it still keys its exe to the CLI's own tag.
9
+ *
10
+ * A fresh `npm i -g` machine has no local build, so `agents computer setup` / `agents setup computer` fetch the
11
+ * asset for the pinned helper version, verify its sha256 against the published
10
12
  * `.sha256`, then verify the code signature (Developer ID Team + notarization)
11
13
  * before it is ever copied to /Applications.
12
14
  *
13
- * A `.app` is a directory, so the asset is a zip (`ditto -c -k --keepParent`);
14
- * we extract it with `ditto -x -k` after the checksum passes.
15
+ * The download + verify machinery is shared with the menu-bar helper in
16
+ * `../helper-download.ts`; this file is the ComputerHelper spec + its wrappers,
17
+ * keeping the historical export surface (`verifyMacHelper`, `downloadMacHelperApp`,
18
+ * `ensureMacHelperApp`, …) stable for existing callers. ComputerHelper does NOT
19
+ * pin a designated requirement (no `expectedBundleId`), matching its verification
20
+ * strength before the shared primitive existed.
15
21
  */
16
- import { execFileSync, spawnSync } from 'node:child_process';
17
- import * as fs from 'node:fs';
18
22
  import * as os from 'node:os';
19
- import * as path from 'node:path';
20
- import { Readable } from 'node:stream';
21
- import { pipeline } from 'node:stream/promises';
22
- import { getCacheDir } from '../state.js';
23
- import { getCliVersion } from '../version.js';
24
- import { parseSha256Asset, sha256File } from './ssh-tunnel.js';
25
23
  import { resolveHelperApp } from './computer-rpc.js';
26
- /** GitHub repo whose `v<version>` releases carry the helper asset. */
27
- export const HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
24
+ import { EXPECTED_TEAM_ID, HELPER_RELEASE_REPO, downloadHelperApp, helperAssetUrls, helperCacheDir, parseTeamId, verifyHelperApp, } from '../helper-download.js';
25
+ import { helperFloor } from '../helper-versions.js';
26
+ // Re-exported so existing importers (and download.test.ts) keep resolving these
27
+ // names from './download.js'.
28
+ export { EXPECTED_TEAM_ID, HELPER_RELEASE_REPO, parseTeamId };
28
29
  /** The zipped `.app` release asset name. */
29
30
  export const MAC_HELPER_ASSET = 'ComputerHelper.app.zip';
30
31
  /** The bundle directory name once extracted. */
31
32
  export const MAC_HELPER_APP_NAME = 'ComputerHelper.app';
32
- /** Apple Developer ID Team the helper must be signed by (defense in depth on top
33
- * of `spctl` notarization assessment). "Developer ID Application: Muqit Nawaz". */
34
- export const EXPECTED_TEAM_ID = '2HTP252L87';
33
+ /** ComputerHelper identity + verification policy. No `expectedBundleId`: this
34
+ * helper has no upgrade-stable Accessibility grant to protect, so its
35
+ * verification is codesign + Team + notarization exactly as before. */
36
+ const COMPUTER_HELPER_SPEC = {
37
+ helper: 'computer-mac',
38
+ assetName: MAC_HELPER_ASSET,
39
+ appName: MAC_HELPER_APP_NAME,
40
+ cacheSubdir: ['computer', 'mac-helper'],
41
+ expectedTeamId: EXPECTED_TEAM_ID,
42
+ localBuildHint: 'bash native/computer-mac/scripts/build.sh release',
43
+ };
35
44
  /** Cache dir for the downloaded helper, one subdir per release tag. */
36
45
  export function macHelperCacheDir(version) {
37
- return path.join(getCacheDir(), 'computer', 'mac-helper', `v${version}`);
46
+ return helperCacheDir(COMPUTER_HELPER_SPEC, version);
38
47
  }
39
48
  /** Release-asset URLs for the helper zip + its checksum at one `v<version>` tag. */
40
49
  export function macHelperAssetUrls(version) {
41
- const base = `https://github.com/${HELPER_RELEASE_REPO}/releases/download/v${version}`;
42
- return { zip: `${base}/${MAC_HELPER_ASSET}`, sha256: `${base}/${MAC_HELPER_ASSET}.sha256` };
43
- }
44
- /** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
45
- * emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
46
- export function parseTeamId(codesignInfo) {
47
- return codesignInfo.match(/TeamIdentifier=([A-Z0-9]+)/)?.[1] ?? null;
50
+ return helperAssetUrls(COMPUTER_HELPER_SPEC, version);
48
51
  }
49
52
  /**
50
53
  * Verify a helper `.app` bundle is intact, signed by the expected Developer ID
@@ -52,31 +55,7 @@ export function parseTeamId(codesignInfo) {
52
55
  * on any failure — a downloaded bundle is never trusted without this.
53
56
  */
54
57
  export function verifyMacHelper(appPath) {
55
- // 1. Structural + signature integrity.
56
- try {
57
- execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'pipe' });
58
- }
59
- catch (e) {
60
- throw new Error(`code signature invalid for ${appPath}: ${e.message}`);
61
- }
62
- // 2. Team identity — must be our Developer ID, not some other valid signer.
63
- // `codesign -dv` writes its details to STDERR even on success (exit 0), so we
64
- // must read stderr, not stdout. spawnSync captures both streams regardless of
65
- // exit code; execFileSync would return only (empty) stdout and the Team check
66
- // would falsely reject every validly-signed helper.
67
- const dv = spawnSync('/usr/bin/codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf8' });
68
- const info = `${dv.stdout ?? ''}${dv.stderr ?? ''}`;
69
- const team = parseTeamId(info);
70
- if (team !== EXPECTED_TEAM_ID) {
71
- throw new Error(`helper signed by unexpected Team (${team ?? 'none'}), expected ${EXPECTED_TEAM_ID}. Refusing to install.`);
72
- }
73
- // 3. Notarization / Gatekeeper — confirms Apple stapled a notarization ticket.
74
- try {
75
- execFileSync('/usr/sbin/spctl', ['--assess', '--type', 'execute', '--verbose', appPath], { stdio: 'pipe' });
76
- }
77
- catch (e) {
78
- throw new Error(`helper is not notarized / rejected by Gatekeeper: ${e.message}. Refusing to install.`);
79
- }
58
+ verifyHelperApp(appPath, COMPUTER_HELPER_SPEC);
80
59
  }
81
60
  /**
82
61
  * Download the helper release asset for `version`, verify sha256, extract the
@@ -84,58 +63,16 @@ export function verifyMacHelper(appPath) {
84
63
  * `ComputerHelper.app`. A missing asset is a hard error naming the exact tag —
85
64
  * never a silent fallback to another release.
86
65
  */
87
- export async function downloadMacHelperApp(version) {
88
- const dir = macHelperCacheDir(version);
89
- const cachedApp = path.join(dir, MAC_HELPER_APP_NAME);
90
- if (fs.existsSync(cachedApp)) {
91
- // Re-verify a cached bundle cheaply; a tampered cache must not be trusted.
92
- verifyMacHelper(cachedApp);
93
- return cachedApp;
94
- }
95
- const tag = `v${version}`;
96
- const { zip: zipUrl, sha256: shaUrl } = macHelperAssetUrls(version);
97
- const missing = (status, url) => new Error(`no ${MAC_HELPER_ASSET} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
98
- `The macOS helper ships as a GitHub release asset per tagged CLI version; ` +
99
- `from a repo checkout you can build it locally instead: ` +
100
- `bash native/computer-mac/scripts/build.sh release`);
101
- // Checksum first: it is tiny and 404s fast when the tag has no assets.
102
- const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
103
- if (!shaRes.ok)
104
- throw missing(shaRes.status, shaUrl);
105
- const expected = parseSha256Asset(await shaRes.text());
106
- console.error(`Downloading ${MAC_HELPER_ASSET} ${tag} from GitHub releases...`);
107
- const zipRes = await fetch(zipUrl, { signal: AbortSignal.timeout(15 * 60_000) });
108
- if (!zipRes.ok || !zipRes.body)
109
- throw missing(zipRes.status, zipUrl);
110
- fs.mkdirSync(dir, { recursive: true });
111
- const partial = path.join(dir, `${MAC_HELPER_ASSET}.download`);
112
- try {
113
- await pipeline(Readable.fromWeb(zipRes.body), fs.createWriteStream(partial));
114
- const actual = await sha256File(partial);
115
- if (actual !== expected) {
116
- throw new Error(`sha256 mismatch for ${zipUrl}: expected ${expected}, got ${actual}`);
117
- }
118
- // Extract the zip (created with `ditto -c -k --keepParent`, so it contains
119
- // ComputerHelper.app/ at top level) into the version cache dir.
120
- fs.rmSync(cachedApp, { recursive: true, force: true });
121
- execFileSync('/usr/bin/ditto', ['-x', '-k', partial, dir], { stdio: 'pipe' });
122
- if (!fs.existsSync(cachedApp)) {
123
- throw new Error(`extracted asset did not contain ${MAC_HELPER_APP_NAME}`);
124
- }
125
- verifyMacHelper(cachedApp);
126
- }
127
- finally {
128
- fs.rmSync(partial, { force: true });
129
- }
130
- return cachedApp;
66
+ export function downloadMacHelperApp(version) {
67
+ return downloadHelperApp(COMPUTER_HELPER_SPEC, version);
131
68
  }
132
69
  /**
133
70
  * Resolve the helper `.app` to install from: a local build / bundled copy first
134
71
  * (repo checkout), else the checksum + signature-verified release-asset download
135
- * for the running CLI version. Throws with the tag it checked when neither
72
+ * for the pinned helper version. Throws with the tag it checked when neither
136
73
  * exists. macOS only.
137
74
  */
138
- export async function ensureMacHelperApp(version = getCliVersion()) {
75
+ export async function ensureMacHelperApp(version = helperFloor('computer-mac')) {
139
76
  if (os.platform() !== 'darwin') {
140
77
  throw new Error('The macOS computer helper is only available on macOS.');
141
78
  }
@@ -72,9 +72,6 @@ export declare function winHelperAssetUrls(version: string): {
72
72
  * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
73
73
  * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
74
74
  */
75
- export declare function parseSha256Asset(text: string): string;
76
- /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
77
- export declare function sha256File(file: string): Promise<string>;
78
75
  /**
79
76
  * Download the exe release asset for this CLI version, verify its sha256
80
77
  * against the published `.sha256` asset, and cache it under the agents cache
@@ -21,7 +21,8 @@ import * as net from 'net';
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import { fileURLToPath } from 'url';
24
- import { randomBytes, createHash } from 'crypto';
24
+ import { randomBytes } from 'crypto';
25
+ import { parseSha256Asset, sha256File } from '../sha256-asset.js';
25
26
  import { Readable } from 'stream';
26
27
  import { pipeline } from 'stream/promises';
27
28
  import { sshExec, SSH_OPTS, assertValidSshTarget } from '../ssh-exec.js';
@@ -31,8 +32,8 @@ import { getDevice } from '../devices/registry.js';
31
32
  import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
32
33
  import { hostNameFor } from '../devices/ssh-config.js';
33
34
  import { getCacheDir } from '../state.js';
34
- import { getCliVersion } from '../version.js';
35
35
  import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
36
+ import { helperFloor, helperTag } from '../helper-versions.js';
36
37
  /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
37
38
  *
38
39
  * Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
@@ -115,8 +116,8 @@ export const WIN_HELPER_TOKEN_FILE = 'helper-token';
115
116
  export function resolveWinHelperExe() {
116
117
  const here = path.dirname(fileURLToPath(import.meta.url));
117
118
  const candidates = [
118
- // Running from the agents-cli checkout. apps/cli/dist/lib/computer -> repo root (5 up) -> native/computer-win.
119
- path.resolve(here, '..', '..', '..', '..', '..', 'native', 'computer-win', 'dist', WIN_HELPER_EXE),
119
+ // Running from the agents-cli checkout. cli/dist/lib/computer -> repo root (4 up) -> native/computer-win.
120
+ path.resolve(here, '..', '..', '..', '..', 'native', 'computer-win', 'dist', WIN_HELPER_EXE),
120
121
  // Bundled with the npm package (dist/lib/computer -> dist/).
121
122
  path.resolve(here, '..', '..', 'computer-helper-win', WIN_HELPER_EXE),
122
123
  ];
@@ -134,29 +135,18 @@ export function winHelperCacheDir(version) {
134
135
  }
135
136
  /** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
136
137
  export function winHelperAssetUrls(version) {
137
- const base = `https://github.com/${WIN_HELPER_RELEASE_REPO}/releases/download/v${version}`;
138
+ // The helper's OWN tag, not the CLI's. This exe is not an .app bundle so it
139
+ // cannot share helper-download.ts's zip/codesign machinery, but it had the
140
+ // identical coupling: keyed to `v${cliVersion}`, every CLI release had to
141
+ // re-stage a ~165MB exe or the download 404'd, and a helper fix could not
142
+ // reach anyone without a CLI release. Same fix, same tag namespace.
143
+ const base = `https://github.com/${WIN_HELPER_RELEASE_REPO}/releases/download/${helperTag('computer-win', version)}`;
138
144
  return { exe: `${base}/${WIN_HELPER_EXE}`, sha256: `${base}/${WIN_HELPER_EXE}.sha256` };
139
145
  }
140
146
  /**
141
147
  * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
142
148
  * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
143
149
  */
144
- export function parseSha256Asset(text) {
145
- const m = text.trim().match(/^([A-Fa-f0-9]{64})(\s|$)/);
146
- if (!m)
147
- throw new Error(`malformed .sha256 release asset: ${JSON.stringify(text.slice(0, 80))}`);
148
- return m[1].toLowerCase();
149
- }
150
- /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
151
- export function sha256File(file) {
152
- return new Promise((resolve, reject) => {
153
- const hash = createHash('sha256');
154
- fs.createReadStream(file)
155
- .on('error', reject)
156
- .on('data', (d) => hash.update(d))
157
- .on('end', () => resolve(hash.digest('hex')));
158
- });
159
- }
160
150
  /**
161
151
  * Download the exe release asset for this CLI version, verify its sha256
162
152
  * against the published `.sha256` asset, and cache it under the agents cache
@@ -203,7 +193,7 @@ export async function downloadWinHelperExe(version) {
203
193
  * checkout / bundled), then the checksum-verified release-asset download for
204
194
  * the running CLI version. Throws with the tag it checked when neither exists.
205
195
  */
206
- export async function ensureWinHelperExe(version = getCliVersion()) {
196
+ export async function ensureWinHelperExe(version = helperFloor('computer-win')) {
207
197
  const local = resolveWinHelperExe();
208
198
  if (local)
209
199
  return local;
@@ -9,7 +9,7 @@
9
9
  import type { AgentId } from './types.js';
10
10
  import { type ModelTier } from './model-tiers.js';
11
11
  /** The top-level scope of a unified config key. */
12
- export type ConfigScope = 'run' | 'interactive' | 'usage' | 'auto' | 'browser' | 'project' | 'device';
12
+ export type ConfigScope = 'run' | 'interactive' | 'auto' | 'browser' | 'project' | 'device';
13
13
  /** A run-time default key: model, mode, effort, or tier override. */
14
14
  export interface ParsedRunConfigKey {
15
15
  scope: 'run';
@@ -23,11 +23,6 @@ export interface ParsedInteractiveConfigKey {
23
23
  scope: 'interactive';
24
24
  property: 'host';
25
25
  }
26
- /** The host whose usage snapshots are authoritative for fleet-wide reporting. */
27
- export interface ParsedUsageConfigKey {
28
- scope: 'usage';
29
- property: 'primary-host';
30
- }
31
26
  /** Which devices automatic placement (`--device auto`) may pick. */
32
27
  export interface ParsedAutoConfigKey {
33
28
  scope: 'auto';
@@ -50,7 +45,7 @@ export interface ParsedDeviceConfigKey {
50
45
  device: string;
51
46
  property: DeviceConfigProperty;
52
47
  }
53
- export type ParsedConfigKey = ParsedRunConfigKey | ParsedInteractiveConfigKey | ParsedUsageConfigKey | ParsedAutoConfigKey | ParsedBrowserConfigKey | ParsedProjectConfigKey | ParsedDeviceConfigKey;
48
+ export type ParsedConfigKey = ParsedRunConfigKey | ParsedInteractiveConfigKey | ParsedAutoConfigKey | ParsedBrowserConfigKey | ParsedProjectConfigKey | ParsedDeviceConfigKey;
54
49
  export type DeviceConfigProperty = 'role' | 'max-agents' | 'scheduler' | 'daemon' | 'watchdog' | 'tmux' | 'browser.remote-control' | 'browser.task-idle-minutes' | 'notes' | 'browser.profile' | 'browser.viewer';
55
50
  /** Normalize agent@version to use `@` consistently. */
56
51
  export declare function formatAgentVersion(agent: AgentId, version: string): string;
@@ -63,7 +58,6 @@ export declare function formatAgentVersion(agent: AgentId, version: string): str
63
58
  * run.<agent@version>.effort
64
59
  * run.<agent@version>.tier.<cheap|default|best|ultra>
65
60
  * interactive.host
66
- * usage.primary-host
67
61
  * auto.pool
68
62
  * browser.profile
69
63
  * project.root
@@ -48,7 +48,6 @@ export function formatAgentVersion(agent, version) {
48
48
  * run.<agent@version>.effort
49
49
  * run.<agent@version>.tier.<cheap|default|best|ultra>
50
50
  * interactive.host
51
- * usage.primary-host
52
51
  * auto.pool
53
52
  * browser.profile
54
53
  * project.root
@@ -80,9 +79,6 @@ export function parseConfigKey(key) {
80
79
  if (raw === 'interactive.host') {
81
80
  return { scope: 'interactive', property: 'host' };
82
81
  }
83
- if (raw === 'usage.primary-host') {
84
- return { scope: 'usage', property: 'primary-host' };
85
- }
86
82
  if (raw === 'auto.pool') {
87
83
  return { scope: 'auto', property: 'pool' };
88
84
  }
@@ -110,9 +106,6 @@ export function parseConfigKey(key) {
110
106
  if (raw.startsWith('interactive.')) {
111
107
  throw new Error(`Invalid interactive config key '${key}'. Use interactive.host.`);
112
108
  }
113
- if (raw.startsWith('usage.')) {
114
- throw new Error(`Invalid usage config key '${key}'. Use usage.primary-host.`);
115
- }
116
109
  if (raw.startsWith('auto.')) {
117
110
  throw new Error(`Invalid auto config key '${key}'. Use auto.pool.`);
118
111
  }
@@ -125,7 +118,7 @@ export function parseConfigKey(key) {
125
118
  if (raw.startsWith('devices.')) {
126
119
  throw new Error(`Invalid device config key '${key}'. Expected devices.<name>.<${DEVICE_CONFIG_PROPERTIES.join('|')}>.`);
127
120
  }
128
- throw new Error(`Unknown config scope in '${key}'. Use one of: run, interactive, usage, auto, browser, project, devices.`);
121
+ throw new Error(`Unknown config scope in '${key}'. Use one of: run, interactive, auto, browser, project, devices.`);
129
122
  }
130
123
  /** Render a parsed key back to its canonical dotted string. */
131
124
  export function formatConfigKey(parsed) {
@@ -137,8 +130,6 @@ export function formatConfigKey(parsed) {
137
130
  return `run.${formatAgentVersion(parsed.agent, parsed.version)}.${parsed.property}`;
138
131
  case 'interactive':
139
132
  return 'interactive.host';
140
- case 'usage':
141
- return 'usage.primary-host';
142
133
  case 'auto':
143
134
  return 'auto.pool';
144
135
  case 'browser':
@@ -158,7 +149,7 @@ export function listKnownConfigKeys() {
158
149
  for (const tier of MODEL_TIERS) {
159
150
  keys.push(`run.<agent@version>.tier.${tier}`);
160
151
  }
161
- keys.push('interactive.host', 'usage.primary-host', 'auto.pool', 'browser.profile', 'browser.viewer', 'project.root');
152
+ keys.push('interactive.host', 'auto.pool', 'browser.profile', 'browser.viewer', 'project.root');
162
153
  for (const prop of DEVICE_CONFIG_PROPERTIES) {
163
154
  keys.push(`devices.<name>.${prop}`);
164
155
  }
@@ -208,8 +199,6 @@ export function configKeyStorageHint(parsed) {
208
199
  return `run.defaults.${parsed.agent}:${parsed.version}.${parsed.property}`;
209
200
  case 'interactive':
210
201
  return 'config.interactiveHost';
211
- case 'usage':
212
- return 'config.usagePrimaryHost';
213
202
  case 'auto':
214
203
  return 'config.autoPool';
215
204
  case 'browser': {
@@ -242,7 +242,7 @@ export declare function reapSafeOrphans(boxes: CrabboxBox[], nowSecs: number): C
242
242
  /**
243
243
  * List reap-safe orphans and (unless `dryRun`) stop them. Returns the candidates
244
244
  * considered and the slugs actually stopped. Best-effort per box — a stop failure
245
- * is skipped, never thrown. Backs `agents devices lease gc` and the 403 auto-reap opt-in.
245
+ * is skipped, never thrown. Backs `agents devices lease prune` and the 403 auto-reap opt-in.
246
246
  */
247
247
  export declare function reapOrphans(opts?: CrabboxOptions & {
248
248
  nowSecs?: number;
@@ -394,7 +394,7 @@ export async function crabboxWarmup(opts = {}) {
394
394
  if (orphans.length) {
395
395
  hint =
396
396
  ` ${orphans.length} expired, idle box(es) are holding the quota — free them with ` +
397
- `\`agents devices lease gc\` (or \`crabbox stop ${orphans[0].slug}\`).`;
397
+ `\`agents devices lease prune\` (or \`crabbox stop ${orphans[0].slug}\`).`;
398
398
  }
399
399
  }
400
400
  catch {
@@ -586,7 +586,7 @@ export function reapSafeOrphans(boxes, nowSecs) {
586
586
  /**
587
587
  * List reap-safe orphans and (unless `dryRun`) stop them. Returns the candidates
588
588
  * considered and the slugs actually stopped. Best-effort per box — a stop failure
589
- * is skipped, never thrown. Backs `agents devices lease gc` and the 403 auto-reap opt-in.
589
+ * is skipped, never thrown. Backs `agents devices lease prune` and the 403 auto-reap opt-in.
590
590
  */
591
591
  export function reapOrphans(opts = {}) {
592
592
  const nowSecs = opts.nowSecs ?? Math.floor(Date.now() / 1000);
@@ -115,6 +115,15 @@ export interface LeaseRunResult {
115
115
  exitCode: number | null;
116
116
  toreDown: boolean;
117
117
  }
118
+ /**
119
+ * Exit code the box-side bootstrap raises when `agents setup` did not leave a
120
+ * usable install (`~/.agents/.system` is still not a git repo). The run-side gate
121
+ * (`ensureInitialized`, commands/setup.ts) refuses `agents run` with "agents-cli
122
+ * is not set up" in exactly that state, so the bootstrap aborts with this code
123
+ * instead of running the agent into that refusal. `leaseAndRun` reads it to stop
124
+ * a box THIS run provisioned — a newly created box that can't run is pure cost.
125
+ */
126
+ export declare const LEASE_BOOTSTRAP_FAILED_CODE = 97;
118
127
  /** Build a shell-safe, collision-resistant workspace id for one lease run. */
119
128
  export declare function leaseWorkspaceId(repoRoot: string, startedAtMs?: number, pid?: number): string;
120
129
  /** Isolated box-home path for one run, relative to the shared box user's home. */
@@ -35,6 +35,15 @@ import { assertNoNativeOAuthTransfer, buildCredentialScript, buildHomeFileWriteS
35
35
  import { LEASE_AGENT_MARKER, leasePhaseSentinel } from './progress.js';
36
36
  import { copySetupToBox } from './setup-copy.js';
37
37
  import { DEFAULT_CRABBOX_PROFILE } from './config.js';
38
+ /**
39
+ * Exit code the box-side bootstrap raises when `agents setup` did not leave a
40
+ * usable install (`~/.agents/.system` is still not a git repo). The run-side gate
41
+ * (`ensureInitialized`, commands/setup.ts) refuses `agents run` with "agents-cli
42
+ * is not set up" in exactly that state, so the bootstrap aborts with this code
43
+ * instead of running the agent into that refusal. `leaseAndRun` reads it to stop
44
+ * a box THIS run provisioned — a newly created box that can't run is pure cost.
45
+ */
46
+ export const LEASE_BOOTSTRAP_FAILED_CODE = 97;
38
47
  /** POSIX single-quote for safe embedding in the generated bootstrap script. */
39
48
  function q(s) {
40
49
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -74,12 +83,14 @@ function runtimeInstallSpec(id, dispatchProfile) {
74
83
  return id;
75
84
  }
76
85
  /**
77
- * Bash snippet that guarantees `agents` is runnable on the box. Fresh crabbox
78
- * images ship without node, and the box user may not own the global npm prefix,
79
- * so everything installs user-level under ~/.local (node from the official
86
+ * Bash snippet that guarantees `agents` is runnable AND set up on the box. Fresh
87
+ * crabbox images ship without node, and the box user may not own the global npm
88
+ * prefix, so everything installs user-level under ~/.local (node from the official
80
89
  * latest-v22.x tarball to satisfy engines.node >=22.5.0). Exits 96 with a
81
- * diagnostic when the CLI still isn't runnable a silent `|| true` here used
82
- * to surface only as `agents: command not found` deep in the script.
90
+ * diagnostic when the CLI still isn't runnable, and {@link LEASE_BOOTSTRAP_FAILED_CODE}
91
+ * when `agents setup` did not leave a git-repo `~/.agents/.system` both used to
92
+ * surface only as a downstream failure (`agents: command not found`, or the run-side
93
+ * "agents-cli is not set up" gate) after a swallowed `|| true`.
83
94
  */
84
95
  const ENSURE_AGENTS_CLI = [
85
96
  'export PATH="$HOME/.local/bin:$PATH"',
@@ -97,9 +108,22 @@ const ENSURE_AGENTS_CLI = [
97
108
  ' echo "lease bootstrap: agents-cli install failed (node: $(command -v node || echo missing))" >&2',
98
109
  ' exit 96',
99
110
  'fi',
100
- // Same first-run guard the hosts bootstrap uses (hosts/ready.ts) a fresh
101
- // install refuses `agents run` with "agents-cli is not set up" until setup ran.
102
- 'if [ ! -d "$HOME/.agents/.system" ]; then agents setup >/dev/null 2>&1 || true; fi',
111
+ // The run-side gate (`ensureInitialized`, commands/setup.ts) refuses `agents run`
112
+ // with "agents-cli is not set up" until `~/.agents/.system` is a GIT REPO. Gate
113
+ // setup on that same postcondition not a swallowed exit: capture setup's output,
114
+ // and if the repo still isn't there afterward, print the real cause and abort so
115
+ // the box is never run into that refusal (a swallowed `|| true` here is what left
116
+ // a provisioned box dying at "agents-cli is not set up"). `-e` (not `-d`) matches
117
+ // the gate's `isGitRepo` (git.ts), which is `existsSync('.git')` — true for a
118
+ // gitfile/gitlink too, so a linked-worktree `.system` never false-aborts here.
119
+ 'if [ ! -e "$HOME/.agents/.system/.git" ]; then',
120
+ ' setup_out=$(agents setup 2>&1)',
121
+ ' if [ ! -e "$HOME/.agents/.system/.git" ]; then',
122
+ ' echo "lease bootstrap: agents setup did not complete (~/.agents/.system is not a git repo)" >&2',
123
+ ' printf "%s\\n" "$setup_out" >&2',
124
+ ` exit ${LEASE_BOOTSTRAP_FAILED_CODE}`,
125
+ ' fi',
126
+ 'fi',
103
127
  ].join('\n');
104
128
  /**
105
129
  * Build the single bootstrap script run on the box: ensure agents-cli, install
@@ -331,28 +355,59 @@ export async function leaseAndRun(opts) {
331
355
  remoteDir: opts.workspaceId ? `${leaseHomeDir(opts.workspaceId)}/.agents/` : undefined,
332
356
  });
333
357
  }
334
- catch {
335
- /* best-effort — never block the run on a config-copy failure */
358
+ catch (err) {
359
+ // Best-effort — a config-copy failure never blocks the run (the agent runs
360
+ // without the pushed ~/.agents config), but surface it instead of swallowing
361
+ // it silently so a broken sync is visible rather than a mystery.
362
+ opts.onData?.(`lease: setup copy failed — running without pushed ~/.agents config (${err.message})\n`);
336
363
  }
337
364
  }
338
365
  const script = buildBootstrapScript(opts);
339
366
  let exitCode = null;
340
367
  let toreDown = false;
368
+ // Track whether the box-side script reached the agent — the bootstrap echoes
369
+ // LEASE_AGENT_MARKER on its own line immediately before `agents run`, and aborts
370
+ // (LEASE_BOOTSTRAP_FAILED_CODE) strictly BEFORE that. So "marker never seen" is
371
+ // how we tell a pre-agent bootstrap abort from an agent that merely happened to
372
+ // exit with the same numeric code — only the former is an unusable box. A short
373
+ // rolling tail keeps the check robust if the marker straddles two output chunks.
374
+ let sawAgentMarker = false;
375
+ let markerTail = '';
376
+ const trackedOnData = (chunk) => {
377
+ if (!sawAgentMarker) {
378
+ const combined = markerTail + chunk;
379
+ if (combined.includes(LEASE_AGENT_MARKER))
380
+ sawAgentMarker = true;
381
+ // Keep only enough tail to catch a marker straddling the NEXT chunk boundary.
382
+ else
383
+ markerTail = combined.slice(-(LEASE_AGENT_MARKER.length + 8));
384
+ }
385
+ opts.onData?.(chunk);
386
+ };
341
387
  try {
342
388
  const renewIdleTimeoutSecs = reused && !opts.reuseBox && box.idleTimeoutSecs !== null
343
389
  ? box.idleTimeoutSecs
344
390
  : undefined;
345
391
  exitCode = await crabboxRunScript(box.slug, script, {
346
392
  secretsBundle: opts.secretsBundle,
347
- onData: opts.onData,
393
+ onData: trackedOnData,
348
394
  renewIdleTimeoutSecs,
349
395
  });
350
396
  }
351
397
  finally {
352
- // Normal --lease establishes or reuses the warm pool, so the box outlives
353
- // this run; credentials are still shredded inside the script above. Only
354
- // --fresh requests the old one-shot lifecycle and tears its new box down.
355
- if (!opts.keep && opts.fresh && !reused) {
398
+ // A box this run PROVISIONED (never a reused one) whose BOOTSTRAP failed is
399
+ // unusable capacity keep it and it bills until the idle GC window. Stop it
400
+ // regardless of --keep-box: the user asked to keep a working box, not a broken
401
+ // one. A reused box is left alone (a concurrent run may share it). The
402
+ // failed-code check is gated on the agent never having started (marker unseen),
403
+ // so an agent that legitimately exits with the same code keeps its --keep-box box.
404
+ const bootstrapFailed = exitCode === LEASE_BOOTSTRAP_FAILED_CODE && !sawAgentMarker;
405
+ // Normal --lease establishes or reuses the warm pool, so a healthy box outlives
406
+ // this run; credentials are still shredded inside the script above. Only --fresh
407
+ // requests the old one-shot lifecycle and tears its new box down.
408
+ const teardownHealthy = !opts.keep && opts.fresh && !reused;
409
+ const teardownBroken = bootstrapFailed && !reused;
410
+ if (teardownHealthy || teardownBroken) {
356
411
  opts.onPhase?.({ kind: 'teardown' });
357
412
  toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
358
413
  }
@@ -32,7 +32,7 @@ export const LEASE_RUNTIMES = [
32
32
  * copying any of them between devices — including to an ephemeral leased box —
33
33
  * because a shared refresh token rotates server-side on the next refresh and
34
34
  * invalidates every other copy (`docs/specifications.md` SING-1b,
35
- * `docs/credential-management.md` invariant 2). The set is derived from
35
+ * `docs/secrets.md`). The set is derived from
36
36
  * `LEASE_RUNTIMES` so a newly-added runtime can never be silently exempted. This
37
37
  * is the single canonical predicate; `--copy-creds` (`hosts/credentials.ts`) and
38
38
  * `--lease` (this module's `buildCredentialScript`) both refuse against it.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Account-state service lifecycle as a `DaemonService` (RUSH-3193 P2).
3
+ *
4
+ * Wraps `startAccountStateService()` under the `ServiceSupervisor` contract
5
+ * so the supervisor owns start/stop and reports health through the uniform
6
+ * path. The account-state service owns its own internal intervals for usage
7
+ * and auth refreshes; the supervisor just starts and stops it.
8
+ */
9
+ import { BaseDaemonService, type DaemonContext } from './service.js';
10
+ import type { DaemonServiceId } from '../daemon-services.js';
11
+ export declare class AccountStateDaemonService extends BaseDaemonService {
12
+ readonly id: DaemonServiceId;
13
+ private service;
14
+ protected onStart(ctx: DaemonContext): Promise<void>;
15
+ protected onStop(): Promise<void>;
16
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Account-state service lifecycle as a `DaemonService` (RUSH-3193 P2).
3
+ *
4
+ * Wraps `startAccountStateService()` under the `ServiceSupervisor` contract
5
+ * so the supervisor owns start/stop and reports health through the uniform
6
+ * path. The account-state service owns its own internal intervals for usage
7
+ * and auth refreshes; the supervisor just starts and stops it.
8
+ */
9
+ import { BaseDaemonService } from './service.js';
10
+ import { startAccountStateService } from '../account-state-service.js';
11
+ import { runUsageRefreshTick, runFleetCacheWarmTick } from '../daemon-ticks.js';
12
+ export class AccountStateDaemonService extends BaseDaemonService {
13
+ id = 'account-state';
14
+ service = null;
15
+ async onStart(ctx) {
16
+ this.service = startAccountStateService({
17
+ refreshUsage: runUsageRefreshTick,
18
+ refreshAuth: runFleetCacheWarmTick,
19
+ onError: (area, error) => ctx.log('WARN', `${area} state refresh failed: ${error.message}`),
20
+ });
21
+ }
22
+ async onStop() {
23
+ this.service?.stop();
24
+ this.service = null;
25
+ }
26
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Browser IPC server lifecycle as a `DaemonService` (RUSH-3193 P2).
3
+ *
4
+ * Wraps `BrowserIPCServer` (backed by a `BrowserService`) under the
5
+ * `ServiceSupervisor` contract. `onStart()` runs the orphan-process reap
6
+ * that must happen before the BrowserService comes up (same as the comment
7
+ * at daemon.ts:1303 — reaps browser/tunnel processes from prior daemons so
8
+ * the new session doesn't hijack stale tunnels or fail to bind claimed ports).
9
+ */
10
+ import { BaseDaemonService, type DaemonContext } from './service.js';
11
+ import type { DaemonServiceId } from '../daemon-services.js';
12
+ import { BrowserService } from '../browser/service.js';
13
+ export declare class BrowserIPCService extends BaseDaemonService {
14
+ readonly id: DaemonServiceId;
15
+ private readonly browserSvc;
16
+ private ipc;
17
+ constructor(browserService: BrowserService);
18
+ protected onStart(ctx: DaemonContext): Promise<void>;
19
+ protected onStop(): Promise<void>;
20
+ }