@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,372 @@
1
+ import { workerReadiness } from './worker-readiness';
2
+ import { runAnsible } from './platform/ansible';
3
+ import { runAnsibleWithReceipt } from './platform/ansible-receipt';
4
+ import { loadedT3Policy, stageT3Policy } from './platform/t3-policy';
5
+ import { verifyUbuntuBoot } from './platform/ubuntu-verify';
6
+ import { verifyResourceSafety } from './platform/resource-verify';
7
+ import { convergeT3Route, T3_ROUTE } from './platform/t3-route';
8
+ import { convergeProxy } from './proxy-converge';
9
+ import { providerNames } from './t3/names-check';
10
+ import { machineProviders } from './t3/providers';
11
+ import { quotaLimited, verifyFleetProviders } from './agents/fleet-verify';
12
+ import { verifyNativeAgents } from './agents/native-verify';
13
+ import { managedT3, managedT3Desktop } from '../bootstrap/native-t3';
14
+ import { convergeT3Service, t3StartedAt } from './platform/t3-service';
15
+ import { userInfo } from 'node:os';
16
+ import { resolve } from 'node:path';
17
+ import { auditUbuntu } from './platform/ubuntu-audit';
18
+ import {
19
+ applyAgentProfiles,
20
+ attempt,
21
+ configureWorkstation,
22
+ } from './node-converge-shared';
23
+ import type { Check, Runner } from './types';
24
+
25
+ /** Ubuntu worker convergence: Ansible owns OS policy, services and recovery; the
26
+ * native T3, proxy and provider owners run only after the baseline passes.
27
+ */
28
+ export async function convergeLinux(
29
+ root: string,
30
+ target: { node: string; revision: string },
31
+ run: Runner
32
+ ): Promise<Check[]> {
33
+ const { node, revision } = target;
34
+ const audit = await auditUbuntu(run);
35
+ if (!audit.supported || !audit.passwordlessSudo)
36
+ return [
37
+ {
38
+ id: 'ubuntu:prerequisites',
39
+ status: 'fail',
40
+ summary: audit.supported
41
+ ? 'SUDO_AUTHORIZATION_REQUIRED'
42
+ : 'UBUNTU_BASELINE_MISMATCH',
43
+ remedy: {
44
+ action:
45
+ 'Authorize designated-user sudo through the manager enrollment flow; unsupported releases require an explicit migration.',
46
+ },
47
+ },
48
+ ];
49
+ const checks: Check[] = [];
50
+ checks.push(
51
+ ...(await attempt('tailscale:configuration', async () => [
52
+ await applyTailscale(root, run),
53
+ ]))
54
+ );
55
+ const { baseline, verified } = await baselineStep(root, run);
56
+ checks.push(...baseline, ...verified);
57
+ checks.push(...(await t3AfterBaseline(root, { node, baseline, run })));
58
+ checks.push(
59
+ ...(await attempt('workstation:configuration', () =>
60
+ configureWorkstation(root, run)
61
+ ))
62
+ );
63
+ checks.push(
64
+ ...(await attempt('configuration:apply', () =>
65
+ applyAgentProfiles(root, node, revision, run)
66
+ ))
67
+ );
68
+ checks.push(
69
+ ...(await attempt('proxy:configuration', () =>
70
+ convergeProxy(node, revision, run)
71
+ ))
72
+ );
73
+ const providers = await attempt('provider:verification', () =>
74
+ verifyFleetProviders(node)
75
+ );
76
+ checks.push(...providers);
77
+ checks.push(
78
+ ...(await attempt('agent:verification', () =>
79
+ verifyNativeAgents(node, root, run, {
80
+ outOfQuota: quotaLimited(providers),
81
+ })
82
+ ))
83
+ );
84
+ checks.push(
85
+ ...(await attempt('worker:readiness', () => workerReadiness(run, node)))
86
+ );
87
+ return checks;
88
+ }
89
+
90
+ /** Applies the baseline (skipped on an unchanged, verified receipt), then
91
+ * measures boot and resource policy. Self-healing: if a skipped baseline's
92
+ * effective policy no longer verifies, the baseline is applied in full and
93
+ * measured again in the same run.
94
+ */
95
+ async function baselineStep(
96
+ root: string,
97
+ run: Runner
98
+ ): Promise<{ baseline: Check[]; verified: Check[] }> {
99
+ const measure = async () => [
100
+ ...(await attempt('ubuntu:boot-policy', () =>
101
+ verifyUbuntuBoot(userInfo().username, run)
102
+ )),
103
+ ...(await attempt('ubuntu:resource-safety', () =>
104
+ verifyResourceSafety(userInfo().homedir, run)
105
+ )),
106
+ ];
107
+ let cached = false;
108
+ let baseline = await attempt('ubuntu:baseline', async () => {
109
+ const applied = await applyBaseline(root, run, false);
110
+ cached = applied.cached;
111
+ return applied.checks;
112
+ });
113
+ let verified = await measure();
114
+ if (cached && verified.some((check) => check.status === 'fail')) {
115
+ baseline = await attempt('ubuntu:baseline', async () => {
116
+ return (await applyBaseline(root, run, true)).checks;
117
+ });
118
+ verified = await measure();
119
+ }
120
+ return { baseline, verified };
121
+ }
122
+
123
+ async function applyBaseline(
124
+ root: string,
125
+ run: Runner,
126
+ force: boolean
127
+ ): Promise<{ cached: boolean; checks: Check[] }> {
128
+ const user = userInfo();
129
+ const ubuntu = resolve(root, 'ansible/ubuntu');
130
+ const result = await runAnsibleWithReceipt(
131
+ run,
132
+ resolve(ubuntu, 'baseline.yml'),
133
+ {
134
+ cwd: ubuntu,
135
+ timeoutMs: 900_000,
136
+ input: JSON.stringify({
137
+ development_source: root,
138
+ development_user: user.username,
139
+ development_uid: user.uid,
140
+ development_home: user.homedir,
141
+ }),
142
+ },
143
+ {
144
+ name: 'ubuntu-baseline',
145
+ sources: [ubuntu],
146
+ force,
147
+ verify: () => baselineServicesActive(run),
148
+ }
149
+ );
150
+ return {
151
+ cached: result.cached,
152
+ checks: [
153
+ {
154
+ id: 'ubuntu:baseline',
155
+ status: result.ok ? 'pass' : 'fail',
156
+ summary: result.ok
157
+ ? result.cached
158
+ ? 'Ubuntu baseline unchanged since its last successful apply and its services are active (re-applied in full at least daily).'
159
+ : 'Ubuntu baseline tasks completed, including always-on service recovery; effective boot/session policies require separate verification.'
160
+ : 'UBUNTU_BASELINE_INCOMPLETE',
161
+ },
162
+ ],
163
+ };
164
+ }
165
+
166
+ /** Cheap probe for what the baseline owns: system services and user timers. */
167
+ async function baselineServicesActive(run: Runner): Promise<boolean> {
168
+ const system = await run([
169
+ 'systemctl',
170
+ 'is-active',
171
+ '--quiet',
172
+ 'ssh.service',
173
+ 'tailscaled.service',
174
+ 'docker.service',
175
+ ]);
176
+ if (!system.ok) return false;
177
+ const user = await run([
178
+ 'systemctl',
179
+ '--user',
180
+ 'is-active',
181
+ '--quiet',
182
+ 'personal-worker-auth.timer',
183
+ ]);
184
+ return user.ok;
185
+ }
186
+
187
+ async function applyTailscale(root: string, run: Runner): Promise<Check> {
188
+ const ubuntu = resolve(root, 'ansible/ubuntu');
189
+ const tailscale = await runAnsibleWithReceipt(
190
+ run,
191
+ 'tailscale.yml',
192
+ { cwd: ubuntu, timeoutMs: 300_000, input: '{}' },
193
+ {
194
+ name: 'ubuntu-tailscale',
195
+ sources: [ubuntu],
196
+ verify: async () => {
197
+ const status = await run(['tailscale', 'status', '--json']);
198
+ return (
199
+ status.ok &&
200
+ (JSON.parse(status.stdout) as { BackendState?: unknown })
201
+ .BackendState === 'Running'
202
+ );
203
+ },
204
+ }
205
+ );
206
+ return {
207
+ id: 'tailscale:configuration',
208
+ status: tailscale.ok ? 'pass' : 'fail',
209
+ summary: tailscale.ok
210
+ ? 'Pinned native Tailscale service is enabled, running and authenticated; manager reachability remains separate.'
211
+ : 'TAILSCALE_SETUP_OR_LOGIN_REQUIRED',
212
+ };
213
+ }
214
+
215
+ async function convergeT3(
216
+ root: string,
217
+ node: string,
218
+ run: Runner
219
+ ): Promise<Check[]> {
220
+ const artifact = managedT3();
221
+ const startedAt = await t3StartedAt(run);
222
+ const checks = await convergeT3Service(
223
+ artifact,
224
+ run,
225
+ async () => {
226
+ const staged = await stageT3Policy(root, artifact.path, run, node);
227
+ return staged.status === 'pass' ? [] : [staged];
228
+ },
229
+ () => installT3Desktop(root, run)
230
+ );
231
+ if (checks.every((check) => check.status === 'pass'))
232
+ checks.push(
233
+ await loadedT3Policy(artifact.path, run, {
234
+ restarted: startedAt !== (await t3StartedAt(run)),
235
+ })
236
+ );
237
+ // Settings are watched by T3: providers arrive and stale ones leave at once.
238
+ checks.push(await machineProviders(node, root, true));
239
+ // Names live in the same settings, so they conform without a restart.
240
+ checks.push(await providerNames(node, root, true));
241
+ const serviceReady = checks.some(
242
+ (check) => check.id === 't3:service' && check.status === 'pass'
243
+ );
244
+ if (serviceReady)
245
+ checks.push(
246
+ ...(await convergeT3Route(run, () => publishT3Route(root, run)))
247
+ );
248
+ if (serviceReady) checks.push(...(await enableWorkerRecovery(root, run)));
249
+ return checks;
250
+ }
251
+
252
+ async function publishT3Route(root: string, run: Runner): Promise<boolean> {
253
+ const result = await runAnsible(run, 't3-route.yml', {
254
+ cwd: resolve(root, 'ansible/ubuntu'),
255
+ input: JSON.stringify({ t3_route_target: T3_ROUTE.proxy }),
256
+ timeoutMs: 60_000,
257
+ });
258
+ return result.ok;
259
+ }
260
+
261
+ async function installT3Desktop(root: string, run: Runner): Promise<boolean> {
262
+ const user = userInfo();
263
+ const desktop = managedT3Desktop();
264
+ const ubuntu = resolve(root, 'ansible/ubuntu');
265
+ const result = await runAnsibleWithReceipt(
266
+ run,
267
+ resolve(ubuntu, 't3-desktop.yml'),
268
+ {
269
+ cwd: root,
270
+ timeoutMs: 600_000,
271
+ input: JSON.stringify({
272
+ development_source: root,
273
+ development_user: user.username,
274
+ development_uid: user.uid,
275
+ development_home: user.homedir,
276
+ t3_desktop_url: desktop.url,
277
+ t3_desktop_name: desktop.name,
278
+ t3_desktop_sha256: desktop.sha256,
279
+ }),
280
+ },
281
+ {
282
+ name: 'ubuntu-t3-desktop',
283
+ sources: [ubuntu],
284
+ // A stopped or disabled app reruns the play, which starts it.
285
+ verify: async () =>
286
+ (
287
+ await run([
288
+ 'systemctl',
289
+ '--user',
290
+ 'is-enabled',
291
+ '--quiet',
292
+ 't3code-desktop.service',
293
+ ])
294
+ ).ok &&
295
+ (
296
+ await run([
297
+ 'systemctl',
298
+ '--user',
299
+ 'is-active',
300
+ '--quiet',
301
+ 't3code-desktop.service',
302
+ ])
303
+ ).ok,
304
+ }
305
+ );
306
+ return result.ok;
307
+ }
308
+
309
+ async function t3AfterBaseline(
310
+ root: string,
311
+ context: { node: string; baseline: Check[]; run: Runner }
312
+ ): Promise<Check[]> {
313
+ const { node, baseline, run } = context;
314
+ if (baseline.some((check) => check.status !== 'pass'))
315
+ return [
316
+ {
317
+ id: 't3:prerequisites',
318
+ status: 'skip',
319
+ summary: 'T3_BASELINE_PREREQUISITES_REQUIRED',
320
+ remedy: {
321
+ action:
322
+ 'Complete the designated-user baseline and lingering policy before changing native service lifecycle. Existing service state is preserved.',
323
+ },
324
+ },
325
+ ];
326
+ return attempt('t3:service', () => convergeT3(root, node, run));
327
+ }
328
+
329
+ async function enableWorkerRecovery(
330
+ root: string,
331
+ run: Runner
332
+ ): Promise<Check[]> {
333
+ const user = userInfo();
334
+ const ubuntu = resolve(root, 'ansible/ubuntu');
335
+ const result = await runAnsibleWithReceipt(
336
+ run,
337
+ resolve(ubuntu, 'recovery.yml'),
338
+ {
339
+ cwd: root,
340
+ timeoutMs: 60_000,
341
+ input: JSON.stringify({
342
+ development_source: root,
343
+ development_user: user.username,
344
+ development_uid: user.uid,
345
+ development_home: user.homedir,
346
+ }),
347
+ },
348
+ {
349
+ name: 'ubuntu-recovery',
350
+ sources: [ubuntu],
351
+ verify: async () =>
352
+ (
353
+ await run([
354
+ 'systemctl',
355
+ '--user',
356
+ 'is-active',
357
+ '--quiet',
358
+ 'personal-worker-recovery.timer',
359
+ ])
360
+ ).ok,
361
+ }
362
+ );
363
+ return [
364
+ {
365
+ id: 'worker:service-recovery',
366
+ status: result.ok ? 'pass' : 'fail',
367
+ summary: result.ok
368
+ ? 'T3 recovery enabled: a stopped desktop app is started and an unresponsive one restarted after three failed one-minute health checks.'
369
+ : 'WORKER_RECOVERY_SETUP_FAILED',
370
+ },
371
+ ];
372
+ }
@@ -0,0 +1,83 @@
1
+ import { fingerprint, writeReceipt } from './fingerprint';
2
+ import { configureWorkspace } from './configure';
3
+ import { stableWorkspace } from './platform/t3-policy';
4
+ import type { Check, Runner } from './types';
5
+
6
+ /** Phase wrapper: one failure code per phase, durations measured, no exception text. */
7
+ export async function attempt(
8
+ id: string,
9
+ operation: () => Promise<Check[]>
10
+ ): Promise<Check[]> {
11
+ const started = Date.now();
12
+ try {
13
+ const checks = await operation();
14
+ const phaseDurationMs = Date.now() - started;
15
+ return checks.map((check) => ({ ...check, phaseDurationMs }));
16
+ } catch {
17
+ return [
18
+ {
19
+ id,
20
+ status: 'fail',
21
+ summary: 'REMOTE_PHASE_FAILED',
22
+ phaseDurationMs: Date.now() - started,
23
+ remedy: {
24
+ action:
25
+ 'Inspect the named owner and approved source revision. Prior phase evidence is retained; no exception, environment or auth response is logged.',
26
+ },
27
+ },
28
+ ];
29
+ }
30
+ }
31
+
32
+ /** The `devfleet` launcher (linked to the applied revision, never copied) and
33
+ * this worker's own credential refresh. Identical on Linux and macOS.
34
+ */
35
+ export async function configureWorkstation(
36
+ root: string,
37
+ run: Runner
38
+ ): Promise<Check[]> {
39
+ const installed = await run(
40
+ // The stable path of the applied revision: it survives pruning.
41
+ ['sh', 'bootstrap/install-launcher.sh', await stableWorkspace(root)],
42
+ { cwd: root, timeoutMs: 30_000 }
43
+ );
44
+ if (!installed.ok)
45
+ return [
46
+ {
47
+ id: 'workstation:installation',
48
+ status: 'fail',
49
+ summary: 'LAUNCHER_INSTALLATION_FAILED',
50
+ },
51
+ ];
52
+ // This receipt certifies only frozen dependency inputs, never machine readiness.
53
+ await writeReceipt(root, await fingerprint(root, run));
54
+ await run(['bun', 'bootstrap/worker-auth.ts'], {
55
+ cwd: root,
56
+ timeoutMs: 90_000,
57
+ });
58
+ return [
59
+ {
60
+ id: 'workstation:configuration',
61
+ status: 'pass',
62
+ summary:
63
+ 'The devfleet launcher is linked to the applied revision; authentication remains a separate gate.',
64
+ },
65
+ ];
66
+ }
67
+
68
+ export function applyAgentProfiles(
69
+ root: string,
70
+ node: string,
71
+ revision: string,
72
+ run: Runner
73
+ ): Promise<Check[]> {
74
+ return configureWorkspace(
75
+ root,
76
+ { node, revision, tool: 'codex', profile: 'proxy' },
77
+ run,
78
+ {
79
+ additionalTools: ['claude', 'gemini', 'opencode', 'grok', 'antigravity'],
80
+ bootstrappedDeployment: true,
81
+ }
82
+ );
83
+ }
@@ -0,0 +1,40 @@
1
+ import { runLockedCommand as runCommand } from './runner';
2
+ import { isRevision } from './machine/state';
3
+ import { convergeLinux } from './node-converge-linux';
4
+ import type { Check } from './types';
5
+
6
+ /** Internal remote entry, run on a worker only. The manager owns the operation
7
+ * lock and source selection; this installs no pull daemon or timer. Never
8
+ * prints child output. Workers are Linux.
9
+ */
10
+ async function main(): Promise<Check[]> {
11
+ const [node, revision] = process.argv.slice(2);
12
+ if (!node || !/^[a-z][a-z0-9-]{0,62}$/.test(node) || !isRevision(revision))
13
+ throw new Error();
14
+ const root = process.cwd();
15
+ const head = await runCommand(['git', 'rev-parse', 'HEAD']);
16
+ if (!head.ok || head.stdout.trim() !== revision) throw new Error();
17
+ if (process.platform === 'linux')
18
+ return convergeLinux(root, { node, revision }, runCommand);
19
+ return [
20
+ {
21
+ id: 'platform:unsupported',
22
+ status: 'fail',
23
+ summary: 'PLATFORM_UNSUPPORTED',
24
+ remedy: {
25
+ action:
26
+ 'Workers are Ubuntu 26.04. The macOS manager converges itself locally in fleet-sync.',
27
+ },
28
+ },
29
+ ];
30
+ }
31
+ if (import.meta.main) {
32
+ const checks = await main().catch((): Check[] => [
33
+ {
34
+ id: 'configuration:operation',
35
+ status: 'fail',
36
+ summary: 'REMOTE_CONFIGURATION_INCOMPLETE',
37
+ },
38
+ ]);
39
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
40
+ }
package/src/onboard.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { shellQuote } from './ssh';
2
+ import { syncPlan } from './sync';
3
+ import type { FleetNode } from './types';
4
+
5
+ export function onboarding(node: FleetNode, launcher = 'devfleet'): object {
6
+ return {
7
+ version: 1,
8
+ node: node.id,
9
+ applied: false,
10
+ steps: [
11
+ 'On the new node, review and run bootstrap/bootstrap.sh (or bootstrap.ps1 for native Windows). Use docs/machine-setup.md. This installs the shared locked toolchain; it does not enable SSH.',
12
+ 'Enable the OS SSH service as the operator on a trusted private network. Do not expose SSH publicly or create cloud resources.',
13
+ 'Create a distinct per-machine SSH key and authorize its PUBLIC key on the destination. Verify host fingerprints out of band. Keep private keys and auth tokens on their original machines.',
14
+ 'In the manager SSH config, configure this alias with the real HostName, User, IdentityFile, optional Port/ProxyJump and IdentitiesOnly yes. Fleet will not write or overwrite SSH configuration.',
15
+ 'Test ssh ALIAS manually once after fingerprint verification. Ensure noninteractive shell startup writes no stdout. Set runtime to an absolute Node/Bun executable/shim if SSH PATH omits it.',
16
+ 'Authenticate separately on the node: gh auth login; gh auth setup-git; vault login using the approved method. Configure coding-provider sessions independently; never copy auth directories.',
17
+ 'For T3 Connect, review t3 status and t3 service status, then complete t3 up --connect and operator pairing/linking. T3 pairing/auth state is not an SSH credential.',
18
+ 'For VS Code, install Remote-SSH in the LOCAL editor profile, use the same SSH alias/client/config, enable the login terminal temporarily, and read Output → Remote - SSH. Restart the editor after PATH/agent changes.',
19
+ 'VS Code needs TCP forwarding and compatible remote server prerequisites. Test with ssh -N -L 127.0.0.1:18080:127.0.0.1:REMOTE_PORT ALIAS against a known loopback service, then curl localhost:18080 from a second terminal. Review server policy; never loosen it automatically.',
20
+ 'Run devfleet fleet-status --node NODE --json from every intended manager. This is directed connectivity, not an all-pairs proof. Review skipped T3/VS Code end-to-end checks manually.',
21
+ ],
22
+ sshTemplate: `Host ${node.ssh}\n HostName REPLACE_WITH_PRIVATE_HOST\n User REPLACE_WITH_USER\n IdentityFile ~/.ssh/REPLACE_WITH_NODE_KEY\n IdentitiesOnly yes\n ForwardAgent no\n`,
23
+ vscodeSettings: {
24
+ 'remote.SSH.path': 'REPLACE_WITH_SAME_OPENSSH_BINARY',
25
+ 'remote.SSH.configFile': 'REPLACE_WITH_ABSOLUTE_SSH_CONFIG',
26
+ 'remote.SSH.showLoginTerminal': true,
27
+ },
28
+ workspaceCommand: `cd -- ${shellQuote(node.workspace)}`,
29
+ verifyCommand: `${launcher} fleet-status --node ${node.id} --json`,
30
+ sync: {
31
+ command: `${launcher} fleet-repair --node ${node.id} --repair workspace`,
32
+ steps: syncPlan(false),
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,128 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join, relative } from 'node:path';
5
+ import { runAnsible } from './ansible';
6
+ import type { CommandOptions, CommandResult, Runner } from '../types';
7
+
8
+ /** Full re-apply at least this often, so drift a cheap check misses still heals. */
9
+ export const RECEIPT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
10
+
11
+ export interface Receipt {
12
+ /** Stable receipt name, e.g. `ubuntu-baseline`. */
13
+ name: string;
14
+ /** Files or directories whose content the playbook result depends on. */
15
+ sources: string[];
16
+ /** Cheap postcondition probe; a skip happens only while it passes. */
17
+ verify: () => Promise<boolean>;
18
+ /** Ignore any receipt and apply in full. */
19
+ force?: boolean;
20
+ /** Receipt directory; defaults to the user's fleet state. */
21
+ directory?: string;
22
+ now?: number;
23
+ }
24
+
25
+ export interface ReceiptResult extends CommandResult {
26
+ /** True when the playbook was skipped on an unchanged, verified receipt. */
27
+ cached: boolean;
28
+ }
29
+
30
+ export function receiptDirectory(home = homedir()): string {
31
+ return join(home, '.local/state/personal/fleet/receipts');
32
+ }
33
+
34
+ /** Ansible is idempotent but slow (fact gathering, package and service
35
+ * inspection). A successful apply records a digest of its exact inputs
36
+ * (playbook sources and variables, never the variables themselves). A later
37
+ * run with the same digest skips the playbook while the cheap postcondition
38
+ * still holds; any input change, failed probe, failed apply or a receipt
39
+ * older than a day applies in full again.
40
+ */
41
+ export async function runAnsibleWithReceipt(
42
+ run: Runner,
43
+ playbook: string,
44
+ options: CommandOptions & { input: string },
45
+ receipt: Receipt
46
+ ): Promise<ReceiptResult> {
47
+ const directory = receipt.directory ?? receiptDirectory();
48
+ const path = join(directory, `${receipt.name}.json`);
49
+ const now = receipt.now ?? Date.now();
50
+ const key = await inputKey(playbook, options.input, receipt.sources);
51
+ if (
52
+ !receipt.force &&
53
+ key !== null &&
54
+ (await fresh(path, key, now)) &&
55
+ (await receipt.verify().catch(() => false))
56
+ )
57
+ return { ok: true, reason: 'exit', stdout: '', stderr: '', cached: true };
58
+ await rm(path, { force: true }).catch(() => undefined);
59
+ const result = await runAnsible(run, playbook, options);
60
+ if (result.ok && key !== null)
61
+ await record(directory, path, { version: 1, key, appliedAt: now });
62
+ return { ...result, cached: false };
63
+ }
64
+
65
+ /** Best effort: a missing receipt only means the next run applies in full. */
66
+ async function record(directory: string, path: string, value: object) {
67
+ try {
68
+ await mkdir(directory, { recursive: true, mode: 0o700 });
69
+ await writeFile(path, JSON.stringify(value) + '\n', { mode: 0o600 });
70
+ } catch {
71
+ /* Never fail a successful apply over its receipt. */
72
+ }
73
+ }
74
+
75
+ async function fresh(path: string, key: string, now: number) {
76
+ try {
77
+ const value = JSON.parse(await readFile(path, 'utf8')) as {
78
+ version?: unknown;
79
+ key?: unknown;
80
+ appliedAt?: unknown;
81
+ };
82
+ return (
83
+ value.version === 1 &&
84
+ value.key === key &&
85
+ typeof value.appliedAt === 'number' &&
86
+ now - value.appliedAt >= 0 &&
87
+ now - value.appliedAt < RECEIPT_MAX_AGE_MS
88
+ );
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /** Null when any source is unreadable: then the playbook always runs. */
95
+ export async function inputKey(
96
+ playbook: string,
97
+ input: string,
98
+ sources: string[]
99
+ ): Promise<string | null> {
100
+ const hash = createHash('sha256');
101
+ hash.update(`playbook\0${playbook}\0input\0${input}\0`);
102
+ try {
103
+ for (const source of [...sources].sort())
104
+ for (const file of await files(source)) {
105
+ hash.update(`${relative(source, file)}\0`);
106
+ hash.update(await readFile(file));
107
+ hash.update('\0');
108
+ }
109
+ } catch {
110
+ return null;
111
+ }
112
+ return hash.digest('hex');
113
+ }
114
+
115
+ async function files(source: string): Promise<string[]> {
116
+ const entries = await readdir(source, {
117
+ recursive: true,
118
+ withFileTypes: true,
119
+ }).catch((error: unknown) => {
120
+ if ((error as NodeJS.ErrnoException).code === 'ENOTDIR') return null;
121
+ throw error;
122
+ });
123
+ if (entries === null) return [source];
124
+ return entries
125
+ .filter((entry) => entry.isFile())
126
+ .map((entry) => join(entry.parentPath, entry.name))
127
+ .sort();
128
+ }