@crvouga/dev-fleet 0.0.0-stage → 0.2.0

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 (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ export const INPUT_FILES = [
6
+ 'package.json',
7
+ 'bun.lock',
8
+ 'mise.toml',
9
+ 'mise.lock',
10
+ ];
11
+ export const FINGERPRINT_FILES = [
12
+ ...INPUT_FILES,
13
+ 'workspace-manifests',
14
+ 'mise-sidecars',
15
+ ];
16
+
17
+ export async function fileHash(path: string): Promise<string | null> {
18
+ try {
19
+ return digest(await readFile(path));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export async function manifestHash(root: string): Promise<string | null> {
26
+ try {
27
+ const entries = await readdir(join(root, 'packages'), {
28
+ withFileTypes: true,
29
+ });
30
+ const hashes: string[] = [];
31
+ for (const entry of entries
32
+ .filter((item) => item.isDirectory())
33
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
34
+ hashes.push(
35
+ `${entry.name}:${await fileHash(join(root, 'packages', entry.name, 'package.json'))}`
36
+ );
37
+ }
38
+ return digest(hashes.join('\n'));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export async function sidecarHash(root: string): Promise<string | null> {
45
+ try {
46
+ const files = await readdir(join(root, '.mise/locks'), {
47
+ recursive: true,
48
+ withFileTypes: true,
49
+ });
50
+ if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
51
+ return null;
52
+ const hashes: string[] = [];
53
+ const paths = files
54
+ .filter((entry) => entry.isFile())
55
+ .map((entry) => join(entry.parentPath, entry.name))
56
+ .sort();
57
+ for (const path of paths) {
58
+ const hash = await fileHash(path);
59
+ if (hash === null) return null;
60
+ const relative = path
61
+ .slice(join(root, '.mise/locks').length)
62
+ .replaceAll('\\', '/');
63
+ hashes.push(`${relative}:${hash}`);
64
+ }
65
+ return digest(hashes.join('\n'));
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function digest(value: string | Buffer): string {
72
+ return createHash('sha256').update(value).digest('hex');
73
+ }
@@ -0,0 +1,52 @@
1
+ import { isRevision } from './state';
2
+ import { resolve } from 'node:path';
3
+ import type { Runner } from '../types';
4
+ import { fleet } from '../fleet-config';
5
+
6
+ /** `https://host/owner/repo.git` and `git@host:owner/repo` name one repository. */
7
+ export function sameRepository(remote: string, declared: string): boolean {
8
+ const name = (url: string) =>
9
+ url
10
+ .replace(/^(https:\/\/|ssh:\/\/git@|git@)/, '')
11
+ .replace(':', '/')
12
+ .replace(/\.git$/, '')
13
+ .toLowerCase();
14
+ return name(remote) === name(declared);
15
+ }
16
+
17
+ /** Use committed objects, never working-tree files, for an approved local branch. */
18
+ export async function approvedLocalSource(
19
+ checkout: string,
20
+ revision: string,
21
+ run: Runner
22
+ ): Promise<string | null> {
23
+ const options = { cwd: checkout, timeoutMs: 10_000 };
24
+ const top = await run(['git', 'rev-parse', '--show-toplevel'], options);
25
+ if (!top.ok || resolve(top.stdout.trim()) !== resolve(checkout)) return null;
26
+ const origin = await run(['git', 'remote', 'get-url', 'origin'], options);
27
+ if (
28
+ !origin.ok ||
29
+ !sameRepository(origin.stdout.trim(), fleet.source.repository)
30
+ )
31
+ return null;
32
+ const commit = await run(
33
+ ['git', 'rev-parse', '--verify', `${revision}^{commit}`],
34
+ options
35
+ );
36
+ return commit.ok && commit.stdout.trim() === revision ? checkout : null;
37
+ }
38
+
39
+ export async function localCheckoutRevision(
40
+ checkout: string,
41
+ run: Runner
42
+ ): Promise<string | undefined> {
43
+ const result = await run(['git', 'rev-parse', 'HEAD'], {
44
+ cwd: checkout,
45
+ timeoutMs: 10_000,
46
+ });
47
+ const revision = result.stdout.trim();
48
+ if (!result.ok || !isRevision(revision)) return undefined;
49
+ return (await approvedLocalSource(checkout, revision, run))
50
+ ? revision
51
+ : undefined;
52
+ }
@@ -0,0 +1,2 @@
1
+ import { FleetUsageError } from '../errors';
2
+ export class FleetLockError extends FleetUsageError {}
@@ -0,0 +1,44 @@
1
+ import type { MachineDefinition } from '../definition';
2
+
3
+ export type MachinePlatform = MachineDefinition['os'];
4
+
5
+ /** One POSIX probe for every supported machine: family, release and CPU. */
6
+ export const PLATFORM_PROBE =
7
+ 'sh -c \'if [ "$(uname -s)" = Darwin ]; then printf "macos\\n%s\\n%s\\n" "$(sw_vers -productVersion)" "$(uname -m)"; else . /etc/os-release && printf "%s\\n%s\\n%s\\n" "$ID" "$VERSION_ID" "$(uname -m)"; fi\'';
8
+
9
+ export interface PlatformBaseline {
10
+ id: 'ubuntu:release' | 'macos:release';
11
+ supported: boolean;
12
+ summary: string;
13
+ }
14
+
15
+ /** Ubuntu is the fully accepted worker platform; macOS is best effort. */
16
+ export function evaluatePlatform(
17
+ os: MachinePlatform,
18
+ probe: string
19
+ ): PlatformBaseline {
20
+ if (os === 'macOS') {
21
+ const supported =
22
+ /^macos\n(1[4-9]|[2-9][0-9])(\.[0-9]+)*\n(arm64|x86_64)\s*$/.test(probe);
23
+ return {
24
+ id: 'macos:release',
25
+ supported,
26
+ summary: supported
27
+ ? 'macOS 14 or newer and a supported CPU architecture observed; macOS policy is best effort.'
28
+ : 'MACOS_BASELINE_MISMATCH',
29
+ };
30
+ }
31
+ const supported = /^ubuntu\n26\.04\n(x86_64|aarch64)\s*$/.test(probe);
32
+ return {
33
+ id: 'ubuntu:release',
34
+ supported,
35
+ summary: supported
36
+ ? 'Ubuntu 26.04 and supported CPU architecture observed.'
37
+ : 'UBUNTU_BASELINE_MISMATCH',
38
+ };
39
+ }
40
+
41
+ /** Windows machines stay declared but never receive manager convergence. */
42
+ export function convergeable(node: MachineDefinition): boolean {
43
+ return node.os !== 'windows';
44
+ }
@@ -0,0 +1,68 @@
1
+ import { approvedLocalSource } from './local-source';
2
+ import { join } from 'node:path';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import type { Runner } from '../types';
5
+ import { ensurePrivateRoot } from './state';
6
+ import { fleet } from '../fleet-config';
7
+
8
+ const repository = (): string => fleet.source.repository;
9
+
10
+ /** Called after approval; local sources supply Git objects only, never dirty files. */
11
+ export async function prepareSource(
12
+ root: string,
13
+ revision: string,
14
+ run: Runner,
15
+ checkout?: string
16
+ ): Promise<string | null> {
17
+ if (checkout) {
18
+ const local = await approvedLocalSource(checkout, revision, run);
19
+ if (local) return local;
20
+ }
21
+ return prepareRemoteSource(root, revision, run);
22
+ }
23
+
24
+ async function prepareRemoteSource(
25
+ root: string,
26
+ revision: string,
27
+ run: Runner
28
+ ): Promise<string | null> {
29
+ const source = join(root, 'source');
30
+ await ensurePrivateRoot(source);
31
+ await mkdir(root, { recursive: true, mode: 0o700 });
32
+ let origin = await run(['git', 'remote', 'get-url', 'origin'], {
33
+ cwd: source,
34
+ });
35
+ if (!origin.ok) {
36
+ const cloned = await run(
37
+ [
38
+ 'git',
39
+ 'clone',
40
+ '--no-checkout',
41
+ '--origin',
42
+ 'origin',
43
+ repository(),
44
+ source,
45
+ ],
46
+ { timeoutMs: 120_000 }
47
+ );
48
+ if (!cloned.ok) return null;
49
+ origin = await run(['git', 'remote', 'get-url', 'origin'], { cwd: source });
50
+ }
51
+ if (!origin.ok || origin.stdout.trim() !== repository()) return null;
52
+ let found = await run(
53
+ ['git', 'rev-parse', '--verify', `${revision}^{commit}`],
54
+ { cwd: source }
55
+ );
56
+ if (!found.ok || found.stdout.trim() !== revision) {
57
+ const fetch = await run(['git', 'fetch', '--no-tags', 'origin', revision], {
58
+ cwd: source,
59
+ timeoutMs: 120_000,
60
+ });
61
+ if (!fetch.ok) return null;
62
+ found = await run(
63
+ ['git', 'rev-parse', '--verify', `${revision}^{commit}`],
64
+ { cwd: source }
65
+ );
66
+ }
67
+ return found.ok && found.stdout.trim() === revision ? source : null;
68
+ }
@@ -0,0 +1,173 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ readdir,
5
+ rename,
6
+ rm,
7
+ lstat,
8
+ stat,
9
+ writeFile,
10
+ } from 'node:fs/promises';
11
+ import { dirname, join } from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { FleetUsageError } from '../errors';
14
+ import { FleetLockError } from './lock-error';
15
+ import { record } from '../config';
16
+
17
+ export interface Enrollment {
18
+ version: 1;
19
+ id: string;
20
+ approvedRevision: string;
21
+ appliedRevision?: string;
22
+ previousRevision?: string;
23
+ }
24
+
25
+ export const isRevision = (value: unknown): value is string =>
26
+ typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
27
+
28
+ /** The manager's enrollment from the file that held it before it moved into
29
+ * the fleet state (src/state/enrollment.ts). Nothing writes this file now.
30
+ */
31
+ export async function readEnrollment(root: string): Promise<Enrollment | null> {
32
+ try {
33
+ const path = join(root, 'enrollment.json');
34
+ const info = await lstat(path);
35
+ if (!info.isFile() || info.size > 4096) throw new Error();
36
+ const enrollment = parseEnrollment(
37
+ JSON.parse(await readFile(path, 'utf8'))
38
+ );
39
+ if (!enrollment) throw new Error();
40
+ return enrollment;
41
+ } catch (error) {
42
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
43
+ throw new FleetUsageError(
44
+ 'Invalid enrollment state. Restore the private enrollment.json backup; do not enroll a replacement using another machine’s identity.'
45
+ );
46
+ }
47
+ }
48
+
49
+ /** A lock directory with no readable owner is only abandoned after this long,
50
+ * so a run that has just created it but not yet written `owner.json` is safe. */
51
+ const OWNERLESS_LOCK_GRACE_MS = 60_000;
52
+
53
+ function processAlive(pid: number): boolean {
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ // EPERM: the process exists but belongs to someone else.
59
+ return (error as NodeJS.ErrnoException).code !== 'ESRCH';
60
+ }
61
+ }
62
+
63
+ /** True only when the lock is provably abandoned: its recorded owner process
64
+ * no longer exists (killed, crashed, closed terminal, reboot), or it never
65
+ * recorded one and is older than the grace period. Anything uncertain is held. */
66
+ async function lockAbandoned(lock: string): Promise<boolean> {
67
+ try {
68
+ const owner: unknown = JSON.parse(
69
+ await readFile(join(lock, 'owner.json'), 'utf8')
70
+ );
71
+ if (record(owner) && Number.isInteger(owner['pid']))
72
+ return !processAlive(owner['pid'] as number);
73
+ } catch {
74
+ // Missing or unreadable owner: fall through to the age check.
75
+ }
76
+ try {
77
+ return Date.now() - (await stat(lock)).mtimeMs > OWNERLESS_LOCK_GRACE_MS;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /** Removes an abandoned lock. The rename is atomic, so when several managers
84
+ * race to recover it exactly one wins; the others simply retry the lock. */
85
+ async function reclaimAbandonedLock(lock: string): Promise<boolean> {
86
+ if (!(await lockAbandoned(lock))) return false;
87
+ const aside = `${lock}.abandoned-${randomUUID()}`;
88
+ try {
89
+ await rename(lock, aside);
90
+ } catch {
91
+ return true; // Someone else already recovered it; retry.
92
+ }
93
+ await rm(aside, { recursive: true, force: true });
94
+ return true;
95
+ }
96
+
97
+ /** Bundles are written next to the lock and removed in a `finally`, which a
98
+ * killed run never reaches. Holding the lock means none is in use. */
99
+ async function sweepTransferBundles(root: string): Promise<void> {
100
+ for (const name of await readdir(root).catch(() => [] as string[]))
101
+ if (/^transfer-[0-9a-f-]+\.bundle$/.test(name))
102
+ await rm(join(root, name), { force: true });
103
+ }
104
+
105
+ /** Fail closed while a sync is genuinely running; recover automatically from
106
+ * a lock whose owner process is gone so an interrupted run never needs manual
107
+ * cleanup. Never steals a lock from a still-running installer. */
108
+ export async function withMachineLock<T>(
109
+ root: string,
110
+ operation: () => Promise<T>
111
+ ): Promise<T> {
112
+ await mkdir(root, { recursive: true, mode: 0o700 });
113
+ const lock = join(root, 'sync.lock');
114
+ for (let attempt = 0; ; attempt++) {
115
+ try {
116
+ await mkdir(lock, { mode: 0o700 });
117
+ break;
118
+ } catch (error) {
119
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST')
120
+ throw new FleetUsageError(
121
+ 'Cannot create manager lock; inspect private state-directory permissions and free disk space.'
122
+ );
123
+ if (attempt < 3 && (await reclaimAbandonedLock(lock))) continue;
124
+ throw new FleetLockError(
125
+ 'Fleet sync is locked. Inspect running installers before removing an abandoned sync.lock directory, then rerun.'
126
+ );
127
+ }
128
+ }
129
+ try {
130
+ await writeFile(
131
+ join(lock, 'owner.json'),
132
+ JSON.stringify({
133
+ version: 1,
134
+ pid: process.pid,
135
+ startedAt: new Date().toISOString(),
136
+ }) + '\n',
137
+ { flag: 'wx', mode: 0o600 }
138
+ );
139
+ await sweepTransferBundles(root);
140
+ return await operation();
141
+ } finally {
142
+ await rm(lock, { recursive: true });
143
+ }
144
+ }
145
+
146
+ export async function ensurePrivateRoot(root: string): Promise<void> {
147
+ for (let path = root; dirname(path) !== path; path = dirname(path)) {
148
+ try {
149
+ if ((await lstat(path)).isSymbolicLink())
150
+ throw new FleetUsageError(
151
+ 'Fleet state paths cannot contain symbolic links.'
152
+ );
153
+ } catch (error) {
154
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
155
+ }
156
+ }
157
+ }
158
+
159
+ /** A valid enrollment record, or null. */
160
+ export function parseEnrollment(data: unknown): Enrollment | null {
161
+ if (!record(data) || data['version'] !== 1) return null;
162
+ if (
163
+ typeof data['id'] !== 'string' ||
164
+ !/^[a-z][a-z0-9-]{0,62}$/.test(data['id'])
165
+ )
166
+ return null;
167
+ return isRevision(data['approvedRevision']) &&
168
+ ['appliedRevision', 'previousRevision'].every(
169
+ (key) => data[key] === undefined || isRevision(data[key])
170
+ )
171
+ ? (data as unknown as Enrollment)
172
+ : null;
173
+ }
@@ -0,0 +1,95 @@
1
+ import type { DoctorCheck } from './doctor-check';
2
+
3
+ /** Machine health sample parsed from `net/remote-probe.sh` output. */
4
+ export type HealthSample = {
5
+ readonly ncpu?: number;
6
+ readonly load?: number;
7
+ readonly memFreePct?: number;
8
+ readonly swapPct?: number;
9
+ readonly diskMb?: number;
10
+ };
11
+
12
+ const PROBE_KEYS = {
13
+ ncpu: 'ncpu',
14
+ load: 'load',
15
+ memfree: 'memFreePct',
16
+ swap: 'swapPct',
17
+ diskmb: 'diskMb',
18
+ } as const;
19
+
20
+ function isProbeKey(key: string): key is keyof typeof PROBE_KEYS {
21
+ return Object.hasOwn(PROBE_KEYS, key);
22
+ }
23
+
24
+ /** Parse `key=value` probe lines; unknown keys and non-numbers are ignored. */
25
+ export function parseProbe(output: string): HealthSample {
26
+ const sample: Record<string, number> = {};
27
+ for (const line of output.split(/\r?\n/)) {
28
+ const [key, value] = line.trim().split('=', 2);
29
+ if (key === undefined || value === undefined || !isProbeKey(key)) continue;
30
+ const n = Number(value);
31
+ if (value.length > 0 && Number.isFinite(n)) sample[PROBE_KEYS[key]] = n;
32
+ }
33
+ return sample;
34
+ }
35
+
36
+ const LABELS = { load: 'CPU load', memory: 'Memory', disk: 'Disk space' };
37
+
38
+ function check(
39
+ id: keyof typeof LABELS,
40
+ severity: DoctorCheck['severity'],
41
+ detail: string,
42
+ fix?: string
43
+ ): DoctorCheck {
44
+ return { id, label: LABELS[id], severity, detail, ...(fix ? { fix } : {}) };
45
+ }
46
+
47
+ /** CPU load relative to core count: ≥2× per core fails, ≥1× warns. */
48
+ export function loadCheck(s: HealthSample, alias: string): DoctorCheck | null {
49
+ if (s.ncpu === undefined || s.load === undefined || s.ncpu <= 0) return null;
50
+ const ratio = s.load / s.ncpu;
51
+ const what = `load ${s.load} on ${s.ncpu} cores (${ratio.toFixed(1)}x)`;
52
+ if (ratio >= 2) {
53
+ return check(
54
+ 'load',
55
+ 'fail',
56
+ `overloaded, ${what}. This makes SSH and T3 time out.`,
57
+ `Find the culprit: ssh ${alias} 'ps aux | sort -rk3 | head'`
58
+ );
59
+ }
60
+ if (ratio >= 1) return check('load', 'warn', `busy, ${what}`);
61
+ return check('load', 'pass', what);
62
+ }
63
+
64
+ /** RAM + swap pressure. */
65
+ export function memoryCheck(s: HealthSample): DoctorCheck | null {
66
+ if (s.memFreePct === undefined || s.swapPct === undefined) return null;
67
+ const what = `${s.memFreePct}% RAM free, swap ${s.swapPct}% used`;
68
+ if (s.swapPct >= 90 && s.memFreePct < 15) {
69
+ return check('memory', 'fail', `exhausted, ${what}`);
70
+ }
71
+ if (s.memFreePct < 10 || s.swapPct >= 80) {
72
+ return check('memory', 'warn', `tight, ${what}`);
73
+ }
74
+ return check('memory', 'pass', what);
75
+ }
76
+
77
+ /** Free disk: <2 GB fails (VS Code server, git, worktrees break), <10 GB warns. */
78
+ export function diskCheck(diskMb: number | undefined): DoctorCheck | null {
79
+ if (diskMb === undefined) return null;
80
+ if (diskMb < 2048) {
81
+ return check(
82
+ 'disk',
83
+ 'fail',
84
+ `almost full, ${diskMb} MB free. VS Code server, git and T3 worktrees will break.`
85
+ );
86
+ }
87
+ if (diskMb < 10_240) return check('disk', 'warn', `low, ${diskMb} MB free`);
88
+ return check('disk', 'pass', `${Math.floor(diskMb / 1024)} GB free`);
89
+ }
90
+
91
+ export function healthChecks(s: HealthSample, alias: string): DoctorCheck[] {
92
+ return [loadCheck(s, alias), memoryCheck(s), diskCheck(s.diskMb)].filter(
93
+ (c): c is DoctorCheck => c !== null
94
+ );
95
+ }
@@ -0,0 +1,47 @@
1
+ import { sshArgs } from './ssh';
2
+ import { isRevision } from './machine/state';
3
+ import type { Check, FleetNode, Runner } from './types';
4
+
5
+ /** Audit the immutable deployed revision, never confuse it with a developer checkout. */
6
+ export async function managedProbeTarget(
7
+ node: FleetNode,
8
+ revision: string | null,
9
+ run: Runner
10
+ ): Promise<{ node: FleetNode; checks: Check[] }> {
11
+ if (!node.required.includes('worker') || !isRevision(revision))
12
+ return { node, checks: [] };
13
+ const result = await run(
14
+ [
15
+ ...sshArgs(node),
16
+ `d="$HOME/.local/share/personal/fleet/revisions/${revision}"; test ! -L "$d" && test -d "$d/.git" && test "$(git -C "$d" rev-parse HEAD 2>/dev/null)" = '${revision}' && test "$(git -C "$d" rev-parse --show-toplevel 2>/dev/null)" = "$d" && clean=$(git -C "$d" status --porcelain 2>/dev/null) && test -z "$clean" && printf '%s' "$d"`,
17
+ ],
18
+ { timeoutMs: 15_000 }
19
+ );
20
+ if (!result.ok || !/^\/[^\x00-\x1f\x7f]{1,4000}$/.test(result.stdout))
21
+ return {
22
+ node,
23
+ checks: [
24
+ {
25
+ id: 'worker:desired-source',
26
+ status: 'fail',
27
+ summary:
28
+ 'The declared immutable worker revision has not been deployed. Developer checkout diagnostics follow separately.',
29
+ remedy: {
30
+ action:
31
+ 'Run fleet-sync for this exact release/revision; developer branches and dirty work are never reset.',
32
+ },
33
+ },
34
+ ],
35
+ };
36
+ return {
37
+ node: { ...node, workspace: result.stdout },
38
+ checks: [
39
+ {
40
+ id: 'worker:desired-source',
41
+ status: 'pass',
42
+ summary:
43
+ 'Auditing the deployed immutable revision, independently of the developer checkout.',
44
+ },
45
+ ],
46
+ };
47
+ }
@@ -0,0 +1,48 @@
1
+ import { diagnostic } from './diagnostics';
2
+ import type { FleetDefinition } from './definition';
3
+ import type { ManagerReport } from './manager-sync';
4
+ import type { Check } from './types';
5
+
6
+ /** Even preflight failures account for every declared target, without discovery,
7
+ * authentication, filesystem mutations or accidental target substitution.
8
+ */
9
+ export function managerFailure(
10
+ definition: FleetDefinition,
11
+ revision: string | null,
12
+ code: string,
13
+ action: string
14
+ ): ManagerReport {
15
+ const check: Check = {
16
+ id: 'manager:preflight',
17
+ status: 'fail',
18
+ summary: code,
19
+ remedy: { action },
20
+ };
21
+ return {
22
+ version: 3,
23
+ command: 'fleet-sync',
24
+ execution: 'manager-push',
25
+ ok: false,
26
+ applied: false,
27
+ desiredRevision: revision,
28
+ checks: [check],
29
+ candidates: 0,
30
+ dashboardUrl: null,
31
+ nodes: definition.workers.map((node) => ({
32
+ node: node.id,
33
+ diagnostics: [diagnostic(node.id, check)],
34
+ reachability: 'unknown',
35
+ desiredRevision: revision,
36
+ appliedRevision: null,
37
+ durationMs: 0,
38
+ checks: [
39
+ {
40
+ id: 'rollout:prerequisites',
41
+ status: 'skip',
42
+ summary: code,
43
+ remedy: { action },
44
+ },
45
+ ],
46
+ })),
47
+ };
48
+ }