@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
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ export type Status = 'pass' | 'warn' | 'fail' | 'skip';
2
+ export type Capability =
3
+ | 'github'
4
+ | 'vault'
5
+ | 't3'
6
+ | 'vscode'
7
+ | 'toolchain'
8
+ | 'codex'
9
+ | 'claude'
10
+ | 'services'
11
+ | 'workstation'
12
+ | 'worker';
13
+
14
+ export interface FleetNode {
15
+ id: string;
16
+ ssh: string;
17
+ workspace: string;
18
+ tailscaleId?: string;
19
+ t3EnvironmentId?: string;
20
+ bootstrapViaSsh?: boolean;
21
+ runtime?: string;
22
+ required: Capability[];
23
+ }
24
+
25
+ export interface FleetConfig {
26
+ version: 1;
27
+ nodes: FleetNode[];
28
+ }
29
+
30
+ export interface Check {
31
+ /** Explicitly out-of-scope checks do not affect required readiness. */
32
+ required?: boolean;
33
+ /** Measured enclosing phase duration; omitted when not observed. */
34
+ phaseDurationMs?: number;
35
+ id: string;
36
+ status: Status;
37
+ summary: string;
38
+ remedy?: { action: string; command?: string };
39
+ }
40
+
41
+ /** Only a required failure blocks readiness. Warnings (conditions outside the
42
+ * fleet, such as a provider quota) and skips (not observable here) are shown
43
+ * but never fail a run.
44
+ */
45
+ export function blocking(check: Check): boolean {
46
+ return check.status === 'fail' && check.required !== false;
47
+ }
48
+
49
+ export interface Fingerprint {
50
+ head: string | null;
51
+ files: Record<string, string | null>;
52
+ }
53
+
54
+ export interface Snapshot {
55
+ version: 1;
56
+ platform: string;
57
+ fingerprint: Fingerprint;
58
+ checks: Check[];
59
+ }
60
+
61
+ export interface NodeReport {
62
+ node: string;
63
+ checks: Check[];
64
+ snapshot?: Snapshot;
65
+ }
66
+
67
+ export interface Report {
68
+ version: 1;
69
+ command: string;
70
+ ok: boolean;
71
+ generatedAt: string;
72
+ nodes: NodeReport[];
73
+ summary: Record<Status, number>;
74
+ }
75
+
76
+ export interface CommandOptions {
77
+ /** Internal remote worker: keep the already-held operation lock on fd 9. */
78
+ preserveOperationLock?: boolean;
79
+ cwd?: string;
80
+ input?: string;
81
+ timeoutMs?: number;
82
+ env?: NodeJS.ProcessEnv;
83
+ /** Run with `env` alone instead of on top of this process's environment:
84
+ * for a launch that must see only what a service would.
85
+ */
86
+ isolatedEnv?: boolean;
87
+ }
88
+
89
+ export interface CommandResult {
90
+ ok: boolean;
91
+ stdout: string;
92
+ stderr: string;
93
+ reason: 'exit' | 'missing' | 'timeout' | 'overflow';
94
+ }
95
+
96
+ export type Runner = (
97
+ command: string[],
98
+ options?: CommandOptions
99
+ ) => Promise<CommandResult>;
@@ -0,0 +1,38 @@
1
+ import { fleet } from './fleet-config';
2
+ import { FleetUsageError } from './errors';
3
+
4
+ export type VaultConfig = {
5
+ addr: string;
6
+ mount: string;
7
+ project: string;
8
+ config: string;
9
+ };
10
+
11
+ /** True when this fleet keeps its secrets in Vault. Only then does each node
12
+ * hold its own Vault session, with the logins, renewal and checks that go
13
+ * with it; the other providers need none of that.
14
+ */
15
+ export const usesVault = (): boolean => fleet.secrets.provider === 'vault';
16
+
17
+ /** This fleet's Vault and the shared document (`<mount>/<project>/<config>`)
18
+ * that holds the proxy client key, from its `secrets` configuration.
19
+ */
20
+ export function resolveVaultConfig(): VaultConfig {
21
+ const secrets = fleet.secrets;
22
+ if (secrets.provider !== 'vault')
23
+ throw new FleetUsageError(
24
+ 'This fleet does not use the vault secrets provider.'
25
+ );
26
+ const [project = '', ...rest] =
27
+ secrets.locations['proxy-client-key'].path.split('/');
28
+ return {
29
+ addr: process.env['VAULT_ADDR']?.trim() || secrets.address,
30
+ mount: secrets.mount,
31
+ project,
32
+ config: rest.join('/'),
33
+ };
34
+ }
35
+
36
+ export function kvPath(vault: VaultConfig): string {
37
+ return `${vault.mount}/data/${vault.project}/${vault.config}`;
38
+ }
@@ -0,0 +1,168 @@
1
+ import type { Check, Runner } from './types';
2
+ import { fleet } from './fleet-config';
3
+ import { secretsProvider } from './secrets';
4
+ import { registry } from './agents/registry';
5
+ import { resolveVaultConfig, usesVault } from './vault-config';
6
+ import { record } from './config';
7
+
8
+ /** Inject every subprocess; never put credentials in argv, reports, or logs. */
9
+ export async function workerAccessChecks(run: Runner): Promise<Check[]> {
10
+ const policy = fleet.worker!;
11
+ const github = await run(['gh', 'api', 'user', '--jq', '.login']);
12
+ return [
13
+ {
14
+ id: 'worker:github-principal',
15
+ status:
16
+ github.ok && github.stdout.trim() === policy.githubLogin
17
+ ? 'pass'
18
+ : 'fail',
19
+ summary:
20
+ 'Worker GitHub authentication must match the declared common account.',
21
+ remedy: {
22
+ action:
23
+ 'Authenticate this worker with the declared GitHub account. Credentials stay on their original machines.',
24
+ },
25
+ },
26
+ await proxyAuthentication(run),
27
+ ...(usesVault() ? [await vaultRenewalCheck(run)] : []),
28
+ ];
29
+ }
30
+ /** Vault refuses a read the token's policy does not allow with a 403. */
31
+ const PERMISSION_DENIED = /permission denied|Code: 403/i;
32
+
33
+ async function proxyAuthentication(run: Runner): Promise<Check> {
34
+ // Read through the declared provider, remembering only whether Vault said
35
+ // the policy forbids it: never what it printed.
36
+ let denied = false;
37
+ const secret = secretsProvider(fleet.secrets, {
38
+ run: async (argv) => {
39
+ const result = await run(argv, { timeoutMs: 20_000 });
40
+ if (!result.ok && PERMISSION_DENIED.test(result.stderr)) denied = true;
41
+ return result.ok ? result.stdout : null;
42
+ },
43
+ });
44
+ const forbidden: Check = {
45
+ id: 'worker:proxy-authentication',
46
+ status: 'fail',
47
+ summary: 'VAULT_POLICY_PERMISSION_REQUIRED',
48
+ remedy: {
49
+ action:
50
+ 'This worker’s Vault identity was refused the proxy client key by its policy. Grant that identity read access to the declared location in the Vault policy you manage. fleet-sync seeds no secret, writes no policy and adds no secret store.',
51
+ },
52
+ };
53
+ const unavailable: Check = {
54
+ id: 'worker:proxy-authentication',
55
+ status: 'fail',
56
+ summary: 'UNATTENDED_PROXY_AUTHENTICATION_REQUIRED',
57
+ remedy: {
58
+ action:
59
+ 'Restore this worker’s own Vault authorization and renewal; no other node’s login or gateway identity is copied.',
60
+ },
61
+ };
62
+ try {
63
+ const key = (await secret('proxy-client-key')) ?? '';
64
+ if (denied) return forbidden;
65
+ if (!/^[\x21-\x7e]{16,4096}$/.test(key)) return unavailable;
66
+ const result = await run(
67
+ [
68
+ 'curl',
69
+ '--fail',
70
+ '--silent',
71
+ '--show-error',
72
+ '--max-time',
73
+ '10',
74
+ '--header',
75
+ '@-',
76
+ `${registry.proxy.endpoint}/v1/models`,
77
+ ],
78
+ { input: `Authorization: Bearer ${key}\n`, timeoutMs: 15_000 }
79
+ );
80
+ const data: unknown = JSON.parse(result.stdout);
81
+ if (
82
+ !result.ok ||
83
+ !record(data) ||
84
+ !Array.isArray(data['data']) ||
85
+ !data['data'].length
86
+ )
87
+ return unavailable;
88
+ return {
89
+ ...unavailable,
90
+ status: 'pass',
91
+ summary:
92
+ 'Worker credential authenticates to the declared singleton gateway model catalog.',
93
+ };
94
+ } catch {
95
+ return unavailable;
96
+ }
97
+ }
98
+
99
+ async function vaultRenewalCheck(run: Runner): Promise<Check> {
100
+ const unavailable: Check = {
101
+ id: 'worker:vault-renewal',
102
+ status: 'fail',
103
+ summary: 'UNATTENDED_VAULT_IDENTITY_REQUIRED',
104
+ remedy: {
105
+ action:
106
+ 'Enroll this worker’s own periodic token or auto-auth identity. Ordinary expiring user sessions are not evidence of unattended renewal; no credentials are copied between machines.',
107
+ },
108
+ };
109
+ try {
110
+ const { addr } = resolveVaultConfig();
111
+ const result = await run([
112
+ 'vault',
113
+ 'token',
114
+ 'lookup',
115
+ `-address=${addr}`,
116
+ '-format=json',
117
+ ]);
118
+ const data: unknown = JSON.parse(result.stdout);
119
+ if (!result.ok || !record(data) || !record(data['data']))
120
+ return unavailable;
121
+ const token = data['data'];
122
+ if (
123
+ token['renewable'] !== true ||
124
+ typeof token['period'] !== 'number' ||
125
+ token['period'] <= 0 ||
126
+ typeof token['ttl'] !== 'number' ||
127
+ token['ttl'] <= 1200
128
+ )
129
+ return unavailable;
130
+ if (!(await lastRenewalSucceeded(run)))
131
+ return {
132
+ ...unavailable,
133
+ summary: 'VAULT_RENEWAL_FAILING',
134
+ remedy: {
135
+ action:
136
+ 'The identity is renewable, but the last run of the renewal job failed. Inspect it on this worker with: journalctl --user -u personal-worker-auth.service. Its token expires unless a renewal succeeds; no credentials are copied between machines.',
137
+ },
138
+ };
139
+ return {
140
+ ...unavailable,
141
+ status: 'pass',
142
+ summary:
143
+ 'This worker has a renewable periodic Vault identity with sufficient lifetime for the renewal timer, and its last renewal succeeded.',
144
+ };
145
+ } catch {
146
+ return unavailable;
147
+ }
148
+ }
149
+
150
+ /** An active timer is not evidence that renewal works: the result of the
151
+ * job's last run is. A job that has not run yet has nothing to say.
152
+ */
153
+ async function lastRenewalSucceeded(run: Runner): Promise<boolean> {
154
+ const shown = await run([
155
+ 'systemctl',
156
+ '--user',
157
+ 'show',
158
+ 'personal-worker-auth.service',
159
+ '--property=Result,ExecMainStatus',
160
+ ]);
161
+ if (!shown.ok) return true;
162
+ const result = /^Result=(.*)$/m.exec(shown.stdout)?.[1];
163
+ const status = /^ExecMainStatus=(.*)$/m.exec(shown.stdout)?.[1];
164
+ return (
165
+ (result === undefined || result === 'success') &&
166
+ (status === undefined || status === '0')
167
+ );
168
+ }
@@ -0,0 +1,34 @@
1
+ import { join } from 'node:path';
2
+ import { healthChecks, parseProbe } from './machine-health';
3
+ import type { Check, Runner } from './types';
4
+
5
+ export async function workerCapacity(
6
+ root: string,
7
+ node: string,
8
+ run: Runner
9
+ ): Promise<Check[]> {
10
+ const result = await run(['sh', join(root, 'ansible/net/remote-probe.sh')], {
11
+ timeoutMs: 10_000,
12
+ });
13
+ const sample = parseProbe(result.stdout);
14
+ if (
15
+ !result.ok ||
16
+ sample.ncpu === undefined ||
17
+ sample.load === undefined ||
18
+ sample.memFreePct === undefined ||
19
+ sample.diskMb === undefined
20
+ )
21
+ return [
22
+ {
23
+ id: 'worker:capacity',
24
+ status: 'fail',
25
+ summary: 'Required CPU, memory and disk evidence is unavailable.',
26
+ },
27
+ ];
28
+ return healthChecks(sample, node).map((check) => ({
29
+ id: `worker:capacity:${check.id}`,
30
+ status: check.severity,
31
+ summary: check.detail,
32
+ ...(check.fix ? { remedy: { action: check.fix } } : {}),
33
+ }));
34
+ }
@@ -0,0 +1,181 @@
1
+ import { powerRecoveryCheck } from './power-recovery';
2
+ import { auditUbuntu } from './platform/ubuntu-audit';
3
+ import { verifyUbuntuBoot } from './platform/ubuntu-verify';
4
+ import { verifyResourceSafety } from './platform/resource-verify';
5
+ import policy from '../ansible/ubuntu/policy.json';
6
+ import { userInfo } from 'node:os';
7
+ import type { Check, Runner } from './types';
8
+
9
+ function check(id: string, ok: boolean, summary: string): Check {
10
+ return {
11
+ id: `worker:${id}`,
12
+ status: ok ? 'pass' : 'fail',
13
+ summary,
14
+ ...(!ok
15
+ ? {
16
+ remedy: {
17
+ action:
18
+ 'Apply the approved fleet-sync revision over verified SSH, then rerun fleet-status --workers. No password prompt or state reset is attempted by health.',
19
+ },
20
+ }
21
+ : {}),
22
+ };
23
+ }
24
+ /** Observed unit state for a failed service check, e.g.
25
+ * `SERVICE_NOT_INSTALLED beszel-agent.service (inactive)`. */
26
+ export function serviceState(name: string, show: string): string {
27
+ const field = (key: string) =>
28
+ new RegExp(`^${key}=(.*)$`, 'm').exec(show)?.[1] ?? '';
29
+ const file = field('UnitFileState');
30
+ const state = field('ActiveState') || 'unknown';
31
+ const code = !file
32
+ ? 'SERVICE_NOT_INSTALLED'
33
+ : state !== 'active'
34
+ ? 'SERVICE_NOT_RUNNING'
35
+ : file !== 'enabled'
36
+ ? 'SERVICE_NOT_ENABLED'
37
+ : 'SERVICE_RESTART_POLICY';
38
+ return `${code} ${name}.service (${state}, unit file ${file || 'missing'}, restart ${field('Restart') || 'unknown'})`;
39
+ }
40
+
41
+ /** Measured runtime evidence, independent of installer receipts. */
42
+ export async function workerHostChecks(
43
+ user: string,
44
+ run: Runner
45
+ ): Promise<Check[]> {
46
+ const audit = await auditUbuntu(run);
47
+ const checks = [
48
+ check(
49
+ 'ubuntu',
50
+ audit.supported,
51
+ 'Required Ubuntu release and architecture.'
52
+ ),
53
+ check(
54
+ 'sudo',
55
+ audit.passwordlessSudo,
56
+ 'Designated worker user has noninteractive sudo.'
57
+ ),
58
+ ...Object.entries(policy.packages).map(([name, version]) =>
59
+ check(
60
+ `package:${name}`,
61
+ audit.packages[name] === version,
62
+ 'Exact declarative Ubuntu package version.'
63
+ )
64
+ ),
65
+ check(
66
+ 'package:tailscale',
67
+ audit.packages['tailscale'] === policy.tailscale.version,
68
+ 'Exact declarative Tailscale version.'
69
+ ),
70
+ ];
71
+ for (const name of policy.alwaysOnServices) {
72
+ const unit = await run([
73
+ 'systemctl',
74
+ 'show',
75
+ `${name}.service`,
76
+ '--property=UnitFileState,ActiveState,Restart',
77
+ ]);
78
+ const active =
79
+ name === 'beszel-agent'
80
+ ? /^ActiveState=active$/m.test(unit.stdout)
81
+ : audit.services[name] === true;
82
+ const ok =
83
+ unit.ok &&
84
+ active &&
85
+ /^UnitFileState=enabled$/m.test(unit.stdout) &&
86
+ /^Restart=always$/m.test(unit.stdout);
87
+ checks.push(
88
+ check(
89
+ `service:${name}`,
90
+ ok,
91
+ ok
92
+ ? 'Required system service is active, enabled for boot and restarts unconditionally.'
93
+ : serviceState(name, unit.stdout)
94
+ )
95
+ );
96
+ }
97
+ checks.push(...(await recoveryChecks(run)));
98
+ checks.push(...(await verifyUbuntuBoot(user, run)));
99
+ checks.push(...(await verifyResourceSafety(userInfo().homedir, run)));
100
+ return checks;
101
+ }
102
+
103
+ async function recoveryChecks(run: Runner): Promise<Check[]> {
104
+ const checks: Check[] = [];
105
+ const t3 = await run([
106
+ 'systemctl',
107
+ '--user',
108
+ 'show',
109
+ 't3code-desktop.service',
110
+ '--property=ActiveState,UnitFileState,Restart',
111
+ ]);
112
+ checks.push(
113
+ check(
114
+ 't3-recovery',
115
+ t3.ok &&
116
+ /^ActiveState=active$/m.test(t3.stdout) &&
117
+ /^UnitFileState=enabled$/m.test(t3.stdout) &&
118
+ /^Restart=(always|on-failure)$/m.test(t3.stdout),
119
+ 'The T3 desktop app runs as an enabled session service with automatic crash recovery.'
120
+ )
121
+ );
122
+ checks.push(await t3HealthCheck(run));
123
+ const renewal = await run([
124
+ 'systemctl',
125
+ '--user',
126
+ 'is-active',
127
+ 'personal-worker-auth.timer',
128
+ ]);
129
+ const recovery = await run([
130
+ 'systemctl',
131
+ '--user',
132
+ 'is-active',
133
+ 'personal-worker-recovery.timer',
134
+ ]);
135
+ checks.push(
136
+ check(
137
+ 'credential-renewal',
138
+ renewal.ok,
139
+ 'Worker credential renewal timer is active.'
140
+ )
141
+ );
142
+ checks.push(
143
+ check(
144
+ 'service-recovery',
145
+ recovery.ok,
146
+ 'Worker service recovery timer is active.'
147
+ )
148
+ );
149
+ checks.push(await powerRecoveryCheck(run));
150
+ return checks;
151
+ }
152
+
153
+ /** The server answers its local API now, not merely that a process exists. */
154
+ async function t3HealthCheck(run: Runner): Promise<Check> {
155
+ const probe = await run(
156
+ [
157
+ 'curl',
158
+ '--fail',
159
+ '--silent',
160
+ '--max-time',
161
+ '10',
162
+ 'http://127.0.0.1:3773/.well-known/t3/environment',
163
+ ],
164
+ { timeoutMs: 15_000 }
165
+ );
166
+ let identified = false;
167
+ try {
168
+ const value: unknown = JSON.parse(probe.stdout);
169
+ identified =
170
+ typeof value === 'object' &&
171
+ value !== null &&
172
+ typeof (value as Record<string, unknown>)['environmentId'] === 'string';
173
+ } catch {
174
+ /* Raw responses are never reported. */
175
+ }
176
+ return check(
177
+ 't3-health',
178
+ probe.ok && identified,
179
+ 'T3 local API answers with its environment identity within 10 seconds.'
180
+ );
181
+ }
@@ -0,0 +1,64 @@
1
+ import { CAPABILITIES, record } from './config';
2
+ import { FleetUsageError } from './errors';
3
+ import type { Capability } from './types';
4
+
5
+ export interface WorkerPolicy {
6
+ requiredCapabilities: Capability[];
7
+ githubLogin: string;
8
+ /** Vault userpass login each node uses for its own periodic token. */
9
+ vaultUser: string;
10
+ toolchain: 'mise.lock';
11
+ agentProfile: 'proxy';
12
+ unattendedSudo: true;
13
+ persistentT3: true;
14
+ neverSleep: true;
15
+ }
16
+ const REQUIRED = [
17
+ 'github',
18
+ 'vault',
19
+ 't3',
20
+ 'toolchain',
21
+ 'workstation',
22
+ 'worker',
23
+ ];
24
+ function validCapabilities(value: unknown): boolean {
25
+ return (
26
+ Array.isArray(value) &&
27
+ new Set(value).size === value.length &&
28
+ value.every((item) => CAPABILITIES.includes(item)) &&
29
+ REQUIRED.every((item) => value.includes(item))
30
+ );
31
+ }
32
+ function validPolicy(value: Record<string, unknown>): boolean {
33
+ return (
34
+ Object.keys(value).every((key) =>
35
+ [
36
+ 'requiredCapabilities',
37
+ 'githubLogin',
38
+ 'vaultUser',
39
+ 'toolchain',
40
+ 'agentProfile',
41
+ 'unattendedSudo',
42
+ 'persistentT3',
43
+ 'neverSleep',
44
+ ].includes(key)
45
+ ) &&
46
+ validCapabilities(value['requiredCapabilities']) &&
47
+ typeof value['githubLogin'] === 'string' &&
48
+ /^[a-zA-Z0-9-]{1,39}$/.test(value['githubLogin']) &&
49
+ typeof value['vaultUser'] === 'string' &&
50
+ /^[a-zA-Z0-9_.-]{1,64}$/.test(value['vaultUser']) &&
51
+ value['toolchain'] === 'mise.lock' &&
52
+ value['agentProfile'] === 'proxy' &&
53
+ ['unattendedSudo', 'persistentT3', 'neverSleep'].every(
54
+ (key) => value[key] === true
55
+ )
56
+ );
57
+ }
58
+ export function parseWorkerPolicy(value: unknown): WorkerPolicy {
59
+ if (!record(value) || !validPolicy(value))
60
+ throw new FleetUsageError(
61
+ 'Invalid mandatory worker policy. Workers require the locked toolchain, common services, unattended sudo, persistent T3 and no sleep.'
62
+ );
63
+ return value as unknown as WorkerPolicy;
64
+ }
@@ -0,0 +1,64 @@
1
+ import { workerCapacity } from './worker-capacity';
2
+ import { userInfo, hostname } from 'node:os';
3
+ import { hostId } from './topology';
4
+ import { configPath } from './fleet-config';
5
+ import type { Check, Runner } from './types';
6
+ import { workerHostChecks } from './worker-host-checks';
7
+ import { workerAccessChecks } from './worker-access-checks';
8
+ import { record } from './config';
9
+
10
+ export async function workerReadiness(
11
+ run: Runner,
12
+ nodeId = hostId(hostname()),
13
+ root = process.cwd()
14
+ ): Promise<Check[]> {
15
+ const host = await workerHostChecks(userInfo().username, run);
16
+ const access = await workerAccessChecks(run);
17
+ const revision = await run(['git', 'rev-parse', 'HEAD']);
18
+ const singleton = await run(
19
+ [
20
+ 'python3',
21
+ '-B',
22
+ 'cliproxy/scripts/fleet_sync.py',
23
+ '--definition',
24
+ configPath(),
25
+ '--node',
26
+ nodeId,
27
+ '--revision',
28
+ revision.stdout.trim(),
29
+ ],
30
+ { timeoutMs: 180_000 }
31
+ );
32
+ let proxy: Check = {
33
+ id: 'worker:proxy-singleton',
34
+ status: 'fail',
35
+ summary:
36
+ 'Singleton gateway ownership or client-only role could not be verified.',
37
+ };
38
+ try {
39
+ const value: unknown = JSON.parse(singleton.stdout);
40
+ if (
41
+ singleton.ok &&
42
+ record(value) &&
43
+ Array.isArray(value['phases']) &&
44
+ value['phases'].length &&
45
+ value['phases'].every(
46
+ (item) => record(item) && item['status'] === 'unchanged'
47
+ )
48
+ )
49
+ proxy = {
50
+ ...proxy,
51
+ status: 'pass',
52
+ summary:
53
+ 'Declared gateway host/client-only role verified without changing runtime identity.',
54
+ };
55
+ } catch {
56
+ /* Raw proxy output is never exposed. */
57
+ }
58
+ return [
59
+ ...host,
60
+ ...access,
61
+ proxy,
62
+ ...(await workerCapacity(root, nodeId, run)),
63
+ ];
64
+ }
@@ -0,0 +1,29 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import type { Check, Runner } from './types';
4
+
5
+ /** This machine has the `devfleet` launcher, linked into a checkout. */
6
+ export async function workstationChecks(
7
+ _root: string,
8
+ run: Runner
9
+ ): Promise<Check[]> {
10
+ const launcher = join(homedir(), '.local/bin/devfleet');
11
+ const result = await run(['test', '-x', launcher], { timeoutMs: 5_000 });
12
+ return [
13
+ result.ok
14
+ ? {
15
+ id: 'workstation:configuration',
16
+ status: 'pass',
17
+ summary: 'The devfleet launcher is installed.',
18
+ }
19
+ : {
20
+ id: 'workstation:configuration',
21
+ status: 'fail',
22
+ summary: 'The devfleet launcher is missing.',
23
+ remedy: {
24
+ action:
25
+ 'Run fleet-sync from the manager: it links the launcher to the checkout of the revision it applies.',
26
+ },
27
+ },
28
+ ];
29
+ }