@crvouga/dev-fleet 0.0.0-stage → 0.1.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 (333) 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 +157 -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 +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -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/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,185 @@
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
+ export async function readEnrollment(root: string): Promise<Enrollment | null> {
29
+ try {
30
+ const path = join(root, 'enrollment.json');
31
+ const info = await lstat(path);
32
+ if (!info.isFile() || info.size > 4096) throw new Error();
33
+ const data: unknown = JSON.parse(await readFile(path, 'utf8'));
34
+ if (!validEnrollment(data)) throw new Error();
35
+ return data as unknown as Enrollment;
36
+ } catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
38
+ throw new FleetUsageError(
39
+ 'Invalid enrollment state. Restore the private enrollment.json backup; do not enroll a replacement using another machine’s identity.'
40
+ );
41
+ }
42
+ }
43
+
44
+ export async function saveEnrollment(
45
+ root: string,
46
+ state: Enrollment
47
+ ): Promise<void> {
48
+ await mkdir(root, { recursive: true, mode: 0o700 });
49
+ const path = join(root, 'enrollment.json');
50
+ const temporary = `${path}.${randomUUID()}.tmp`;
51
+ try {
52
+ await writeFile(temporary, JSON.stringify(state) + '\n', {
53
+ mode: 0o600,
54
+ flag: 'wx',
55
+ });
56
+ await rename(temporary, path);
57
+ } finally {
58
+ await rm(temporary, { force: true });
59
+ }
60
+ }
61
+
62
+ /** A lock directory with no readable owner is only abandoned after this long,
63
+ * so a run that has just created it but not yet written `owner.json` is safe. */
64
+ const OWNERLESS_LOCK_GRACE_MS = 60_000;
65
+
66
+ function processAlive(pid: number): boolean {
67
+ try {
68
+ process.kill(pid, 0);
69
+ return true;
70
+ } catch (error) {
71
+ // EPERM: the process exists but belongs to someone else.
72
+ return (error as NodeJS.ErrnoException).code !== 'ESRCH';
73
+ }
74
+ }
75
+
76
+ /** True only when the lock is provably abandoned: its recorded owner process
77
+ * no longer exists (killed, crashed, closed terminal, reboot), or it never
78
+ * recorded one and is older than the grace period. Anything uncertain is held. */
79
+ async function lockAbandoned(lock: string): Promise<boolean> {
80
+ try {
81
+ const owner: unknown = JSON.parse(
82
+ await readFile(join(lock, 'owner.json'), 'utf8')
83
+ );
84
+ if (record(owner) && Number.isInteger(owner['pid']))
85
+ return !processAlive(owner['pid'] as number);
86
+ } catch {
87
+ // Missing or unreadable owner: fall through to the age check.
88
+ }
89
+ try {
90
+ return Date.now() - (await stat(lock)).mtimeMs > OWNERLESS_LOCK_GRACE_MS;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ /** Removes an abandoned lock. The rename is atomic, so when several managers
97
+ * race to recover it exactly one wins; the others simply retry the lock. */
98
+ async function reclaimAbandonedLock(lock: string): Promise<boolean> {
99
+ if (!(await lockAbandoned(lock))) return false;
100
+ const aside = `${lock}.abandoned-${randomUUID()}`;
101
+ try {
102
+ await rename(lock, aside);
103
+ } catch {
104
+ return true; // Someone else already recovered it; retry.
105
+ }
106
+ await rm(aside, { recursive: true, force: true });
107
+ return true;
108
+ }
109
+
110
+ /** Bundles are written next to the lock and removed in a `finally`, which a
111
+ * killed run never reaches. Holding the lock means none is in use. */
112
+ async function sweepTransferBundles(root: string): Promise<void> {
113
+ for (const name of await readdir(root).catch(() => [] as string[]))
114
+ if (/^transfer-[0-9a-f-]+\.bundle$/.test(name))
115
+ await rm(join(root, name), { force: true });
116
+ }
117
+
118
+ /** Fail closed while a sync is genuinely running; recover automatically from
119
+ * a lock whose owner process is gone so an interrupted run never needs manual
120
+ * cleanup. Never steals a lock from a still-running installer. */
121
+ export async function withMachineLock<T>(
122
+ root: string,
123
+ operation: () => Promise<T>
124
+ ): Promise<T> {
125
+ await mkdir(root, { recursive: true, mode: 0o700 });
126
+ const lock = join(root, 'sync.lock');
127
+ for (let attempt = 0; ; attempt++) {
128
+ try {
129
+ await mkdir(lock, { mode: 0o700 });
130
+ break;
131
+ } catch (error) {
132
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST')
133
+ throw new FleetUsageError(
134
+ 'Cannot create manager lock; inspect private state-directory permissions and free disk space.'
135
+ );
136
+ if (attempt < 3 && (await reclaimAbandonedLock(lock))) continue;
137
+ throw new FleetLockError(
138
+ 'Fleet sync is locked. Inspect running installers before removing an abandoned sync.lock directory, then rerun.'
139
+ );
140
+ }
141
+ }
142
+ try {
143
+ await writeFile(
144
+ join(lock, 'owner.json'),
145
+ JSON.stringify({
146
+ version: 1,
147
+ pid: process.pid,
148
+ startedAt: new Date().toISOString(),
149
+ }) + '\n',
150
+ { flag: 'wx', mode: 0o600 }
151
+ );
152
+ await sweepTransferBundles(root);
153
+ return await operation();
154
+ } finally {
155
+ await rm(lock, { recursive: true });
156
+ }
157
+ }
158
+
159
+ export async function ensurePrivateRoot(root: string): Promise<void> {
160
+ for (let path = root; dirname(path) !== path; path = dirname(path)) {
161
+ try {
162
+ if ((await lstat(path)).isSymbolicLink())
163
+ throw new FleetUsageError(
164
+ 'Fleet state paths cannot contain symbolic links.'
165
+ );
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
168
+ }
169
+ }
170
+ }
171
+
172
+ function validEnrollment(data: unknown): boolean {
173
+ if (!record(data) || data['version'] !== 1) return false;
174
+ if (
175
+ typeof data['id'] !== 'string' ||
176
+ !/^[a-z][a-z0-9-]{0,62}$/.test(data['id'])
177
+ )
178
+ return false;
179
+ return (
180
+ isRevision(data['approvedRevision']) &&
181
+ ['appliedRevision', 'previousRevision'].every(
182
+ (key) => data[key] === undefined || isRevision(data[key])
183
+ )
184
+ );
185
+ }
@@ -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
+ }