@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,92 @@
1
+ import type { Check } from './types';
2
+
3
+ export interface Diagnostic {
4
+ node: string;
5
+ phase: string;
6
+ checkId: string;
7
+ status: Check['status'];
8
+ code: string;
9
+ summary: string;
10
+ expected: string;
11
+ observed: string;
12
+ scope: 'manager' | 'node' | 'unverified';
13
+ durationMs: number | null;
14
+ retryable: boolean;
15
+ actionRequired: boolean;
16
+ remedy: { owner: string; action: string };
17
+ }
18
+ const EXPECTED: Record<string, string> = {
19
+ ssh: 'Fresh noninteractive SSH session with the enrolled host key and designated user.',
20
+ tailscale:
21
+ 'The enrolled device identity is connected and reachable through the intended tailnet.',
22
+ ubuntu:
23
+ 'Observed Ubuntu configuration matches the reviewed baseline without a distribution upgrade.',
24
+ t3: 'Pinned native T3 service and the specifically named runtime acceptance check pass.',
25
+ provider:
26
+ 'The declared gateway model passes authenticated inference, completed streaming and a bounded tool round trip.',
27
+ proxy:
28
+ 'One declared gateway owner preserves runtime identities and serves authorized clients.',
29
+ beszel:
30
+ 'Pinned monitoring artifacts, canonical registration and fresh authenticated metrics are independently verified.',
31
+ rollout: 'Required prerequisites and canary gates pass before promotion.',
32
+ manager:
33
+ 'Manager enrollment, approval and exclusive operation ownership are valid.',
34
+ source:
35
+ 'The approved full commit and packaged canonical definition match verified source.',
36
+ configuration:
37
+ 'The existing Workstation owner applies and verifies the selected source configuration.',
38
+ };
39
+
40
+ /** Derived only from already-redacted checks. No raw subprocess errors, journal
41
+ * contents, auth payloads or environment fields enter the diagnostic contract.
42
+ */
43
+ export function diagnostic(node: string, check: Check): Diagnostic {
44
+ const phase = check.id.split(':')[0]!;
45
+ const failed = check.status !== 'pass';
46
+ const code = /^[A-Z][A-Z0-9_]{2,100}$/.test(check.summary)
47
+ ? check.summary
48
+ : `${check.id.replaceAll(/[^a-z0-9]/g, '_').toUpperCase()}_${failed ? 'UNVERIFIED' : 'VERIFIED'}`;
49
+ return {
50
+ node,
51
+ phase,
52
+ checkId: check.id,
53
+ status: check.status,
54
+ code,
55
+ summary: check.summary,
56
+ expected:
57
+ EXPECTED[phase] ??
58
+ 'The named required check has independent verification evidence.',
59
+ observed: check.summary,
60
+ scope: scope(check.id),
61
+ durationMs: check.phaseDurationMs ?? null,
62
+ retryable: failed && /OFFLINE|UNREACHABLE|METRICS_STALE|TIMEOUT/.test(code),
63
+ actionRequired: failed,
64
+ remedy: {
65
+ owner: owner(phase),
66
+ action:
67
+ check.remedy?.action ??
68
+ 'Inspect this named check in the Fleet runbook; preserve runtime identity and repair persistent configuration in source.',
69
+ },
70
+ };
71
+ }
72
+ function scope(id: string): Diagnostic['scope'] {
73
+ if (
74
+ /^(t3:manager-endpoint|ssh:|manager:|rollout:|source:|tailscale:(reachability|identity|enrolled-identity))/.test(
75
+ id
76
+ )
77
+ )
78
+ return 'manager';
79
+ if (
80
+ /^(provider:|proxy:|configuration:|ubuntu:baseline|t3:(service|policy)|tailscale:configuration)/.test(
81
+ id
82
+ )
83
+ )
84
+ return 'node';
85
+ if (/^beszel:(version|registration|connection|freshness|api)/.test(id))
86
+ return 'manager';
87
+ return 'unverified';
88
+ }
89
+ function owner(phase: string): string {
90
+ if (phase === 'proxy') return 'cliproxy';
91
+ return 'dev-fleet';
92
+ }
@@ -0,0 +1,10 @@
1
+ export type CheckSeverity = 'pass' | 'warn' | 'fail';
2
+
3
+ /** One human-readable health finding, with an optional fix to run. */
4
+ export type DoctorCheck = {
5
+ id: string;
6
+ label: string;
7
+ severity: CheckSeverity;
8
+ detail: string;
9
+ fix?: string | undefined;
10
+ };
package/src/errors.ts ADDED
@@ -0,0 +1 @@
1
+ export class FleetUsageError extends Error {}
@@ -0,0 +1,120 @@
1
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { record } from './config';
4
+ import {
5
+ fileHash,
6
+ manifestHash,
7
+ sidecarHash,
8
+ INPUT_FILES,
9
+ FINGERPRINT_FILES,
10
+ } from './input-hashes';
11
+ import type { Fingerprint, Runner, Check } from './types';
12
+
13
+ const RECEIPT = 'node_modules/.fleet-sync.json';
14
+
15
+ export async function fingerprint(
16
+ root: string,
17
+ run: Runner
18
+ ): Promise<Fingerprint> {
19
+ const files: Fingerprint['files'] = {};
20
+ for (const name of INPUT_FILES)
21
+ files[name] = await fileHash(join(root, name));
22
+ files['workspace-manifests'] = await manifestHash(root);
23
+ files['mise-sidecars'] = await sidecarHash(root);
24
+ const head = await run(['git', 'rev-parse', 'HEAD'], { cwd: root });
25
+ return {
26
+ head:
27
+ head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout)
28
+ ? head.stdout.trim()
29
+ : null,
30
+ files,
31
+ };
32
+ }
33
+
34
+ export async function dependencyCheck(
35
+ root: string,
36
+ current: Fingerprint
37
+ ): Promise<Check> {
38
+ const remedy = {
39
+ action:
40
+ 'Review devfleet fleet-repair --repair workspace, then explicitly apply a frozen toolchain/dependency sync.',
41
+ };
42
+ try {
43
+ const receipt: unknown = JSON.parse(
44
+ await readFile(join(root, RECEIPT), 'utf8')
45
+ );
46
+ const matches =
47
+ record(receipt) &&
48
+ receipt['version'] === 1 &&
49
+ JSON.stringify(receipt['files']) === JSON.stringify(current.files);
50
+ return {
51
+ id: 'dependencies',
52
+ status: matches ? 'pass' : 'fail',
53
+ summary: matches
54
+ ? 'Last successful frozen sync matches input hashes; not a full integrity audit.'
55
+ : 'Dependency sync receipt is stale or invalid.',
56
+ ...(matches ? {} : { remedy }),
57
+ };
58
+ } catch {
59
+ return {
60
+ id: 'dependencies',
61
+ status: 'warn',
62
+ summary: 'No frozen-sync receipt; installed dependencies are unverified.',
63
+ remedy,
64
+ };
65
+ }
66
+ }
67
+
68
+ export async function writeReceipt(
69
+ root: string,
70
+ current: Fingerprint
71
+ ): Promise<void> {
72
+ await mkdir(join(root, 'node_modules'), { recursive: true });
73
+ await writeFile(
74
+ join(root, RECEIPT),
75
+ JSON.stringify({ version: 1, files: current.files }),
76
+ { mode: 0o600 }
77
+ );
78
+ }
79
+
80
+ export function compareFingerprints(
81
+ expected: Fingerprint,
82
+ actual: Fingerprint
83
+ ): Check[] {
84
+ return [
85
+ compare('revision', expected.head, actual.head),
86
+ ...FINGERPRINT_FILES.map((name) =>
87
+ compare(
88
+ `sync:${name}`,
89
+ expected.files[name] ?? null,
90
+ actual.files[name] ?? null
91
+ )
92
+ ),
93
+ ];
94
+ }
95
+
96
+ function compare(
97
+ id: string,
98
+ expected: string | null,
99
+ actual: string | null
100
+ ): Check {
101
+ const known = expected !== null && actual !== null;
102
+ const matches = known && expected === actual;
103
+ return {
104
+ id,
105
+ status: matches ? 'pass' : 'fail',
106
+ summary: matches
107
+ ? 'Matches the manager checkout.'
108
+ : known
109
+ ? 'Differs from the manager checkout.'
110
+ : 'Cannot verify; reference or node input is missing.',
111
+ ...(matches
112
+ ? {}
113
+ : {
114
+ remedy: {
115
+ action:
116
+ 'Reconcile branches/lockfiles; review devfleet fleet-sync for the approved revision. Never reset a working tree.',
117
+ },
118
+ }),
119
+ };
120
+ }
@@ -0,0 +1,88 @@
1
+ import { spawn } from 'node:child_process';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { parseDefinition, type FleetDefinition } from './definition';
12
+ import { FleetUsageError } from './errors';
13
+ import { pinnedPath } from './agents/toolchain';
14
+ import type { Runner } from './types';
15
+ import {
16
+ secretsProvider,
17
+ type SecretCommand,
18
+ type SecretsProvider,
19
+ } from './secrets';
20
+
21
+ /** Where every machine keeps the fleet configuration. fleet-sync installs the
22
+ * approved copy here, so launchers and scheduled jobs find it without any
23
+ * environment. `DEVFLEET_CONFIG` names another file (the manager's checkout of
24
+ * its configuration repository, or a test fixture).
25
+ */
26
+ export function configPath(env = process.env, home = homedir()): string {
27
+ return env['DEVFLEET_CONFIG'] || join(home, '.config/dev-fleet/fleet.json');
28
+ }
29
+
30
+ /** Installs the configuration this process runs with as the machine's copy,
31
+ * so launchers and scheduled jobs (which get no environment) use the same one.
32
+ * Returns whether the file changed.
33
+ */
34
+ export function installConfig(home = homedir()): boolean {
35
+ const target = join(home, '.config/dev-fleet/fleet.json');
36
+ const content = JSON.stringify(fleet, null, 2) + '\n';
37
+ if (existsSync(target) && readFileSync(target, 'utf8') === content)
38
+ return false;
39
+ mkdirSync(join(home, '.config/dev-fleet'), { recursive: true, mode: 0o700 });
40
+ writeFileSync(`${target}.new`, content, { mode: 0o600 });
41
+ renameSync(`${target}.new`, target);
42
+ return true;
43
+ }
44
+
45
+ function load(): FleetDefinition {
46
+ const path = configPath();
47
+ if (!existsSync(path))
48
+ throw new FleetUsageError(
49
+ `No fleet configuration at ${path}. Copy examples/fleet.json, edit it for your machines, and set DEVFLEET_CONFIG or place it there.`
50
+ );
51
+ return parseDefinition(JSON.parse(readFileSync(path, 'utf8')));
52
+ }
53
+
54
+ /** This fleet's own configuration: its machines, endpoints, accounts and source
55
+ * repository. Fleet code never names any of them; it reads them here, and this
56
+ * is the only module that loads the file.
57
+ */
58
+ export const fleet = load();
59
+
60
+ /** Reads one of this fleet's secrets for this node. Tools run from the pinned
61
+ * toolchain, whatever PATH the caller (T3, launchd, systemd) started with.
62
+ */
63
+ export const secret: SecretsProvider = (name) =>
64
+ secretsProvider(fleet.secrets, { run: pinned })(name);
65
+
66
+ /** The same, through an injected runner (fleet-sync phases and their tests). */
67
+ export const secretVia = (run: Runner): SecretsProvider =>
68
+ secretsProvider(fleet.secrets, {
69
+ run: async (argv) => {
70
+ const result = await run(argv, { timeoutMs: 20_000 });
71
+ return result.ok ? result.stdout : null;
72
+ },
73
+ });
74
+
75
+ const pinned: SecretCommand = (argv) =>
76
+ new Promise((resolve) => {
77
+ const child = spawn(argv[0]!, argv.slice(1), {
78
+ stdio: ['ignore', 'pipe', 'ignore'],
79
+ timeout: 20_000,
80
+ env: { ...process.env, PATH: pinnedPath() },
81
+ });
82
+ let output = '';
83
+ child.stdout.on('data', (chunk: Buffer) => {
84
+ if (output.length < 65_536) output += chunk.toString('utf8');
85
+ });
86
+ child.on('error', () => resolve(null));
87
+ child.on('close', (code) => resolve(code === 0 ? output : null));
88
+ });
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { parseConfig, loadConfig, selectNodes } from './config';
2
+ export { doctor } from './manager';
3
+ export { probe } from './probe';
4
+ export { makeReport, reportExitCode, renderReport } from './report';
5
+ export { syncPlan, syncWorkspace } from './sync';
6
+ export type * from './types';
@@ -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 = 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
+ }