@crvouga/dev-fleet 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,242 @@
1
+ import { verifyT3Linked } from '../t3-linked-check';
2
+ import { T3_RESTARTED_SUMMARY } from '../platform/t3-policy';
3
+ import { createHash } from 'node:crypto';
4
+ import { verifyT3Endpoint } from '../t3-endpoint';
5
+ import { authorizePrivilege } from './fleet-sync-privilege';
6
+ import { authorizeVault } from './fleet-sync-vault';
7
+ import { managerFailure } from '../manager-failure';
8
+ import { managerText, stdoutColor } from './fleet-sync-output';
9
+ import { syncProgress, type SyncProgress } from './fleet-sync-progress';
10
+ import { saveReport } from './fleet-sync-report';
11
+ import { synchronizeMonitoring } from './fleet-sync-monitoring';
12
+ import { convergeManager } from './fleet-sync-manager';
13
+ import { FleetUsageError } from '../errors';
14
+ import { selectNodes } from '../config';
15
+ import { loadRoster } from './roster';
16
+ import { enrolledRoster } from '../definition';
17
+ import type { FleetDefinition } from '../definition';
18
+ import { fleet as definition } from '../fleet-config';
19
+ import { synchronizeManager } from '../manager-sync';
20
+ import { inspectBootstrap } from '../manager-sync';
21
+ import { prepareBootstrap, bootstrapRemote } from '../remote-bootstrap';
22
+ import { machineRoot } from './fleet-sync';
23
+ import type { Check, Runner } from '../types';
24
+ import type { DeployNode, ManagerReport } from '../manager-sync';
25
+ import type { FleetSyncOptions } from './fleet-sync';
26
+ import type { Settings } from './settings';
27
+ import type { CommandOutcome } from './contracts';
28
+
29
+ /** `settle` runs once the fleet's result is known and before it is reported:
30
+ * it records the result and returns the check that says so.
31
+ */
32
+ export async function selected(
33
+ options: FleetSyncOptions,
34
+ settings: Settings,
35
+ run: Runner,
36
+ settle?: (report: ManagerReport) => Promise<Check>
37
+ ): Promise<CommandOutcome> {
38
+ if (!options.revision)
39
+ throw new FleetUsageError('An approved source revision is required.');
40
+ const canonical = definition;
41
+ let roster;
42
+ try {
43
+ roster = enrolledRoster(canonical, await loadRoster(settings.config));
44
+ } catch {
45
+ return enrollmentRequired(canonical, options.revision);
46
+ }
47
+ const nodes = selectNodes(roster, options.node ?? []);
48
+ const scoped = scopeDefinition(canonical, options.node);
49
+ const ui = interfaceOptions(options);
50
+ const planFingerprint = createHash('sha256')
51
+ .update(
52
+ JSON.stringify({ revision: options.revision, definition: scoped, nodes })
53
+ )
54
+ .digest('hex');
55
+ if (
56
+ options.expectedPlanFingerprint &&
57
+ options.expectedPlanFingerprint !== planFingerprint
58
+ )
59
+ return failed(
60
+ managerFailure(
61
+ scoped,
62
+ options.revision,
63
+ 'FLEET_PLAN_STALE',
64
+ 'Manager connection bindings or target policy changed after approval. Inspect a fresh plan and approve it; no source preparation or node mutation occurred.'
65
+ )
66
+ );
67
+ const payload = options.apply
68
+ ? await prepareBootstrap(machineRoot(), options.revision, run, {
69
+ definition: canonical,
70
+ checkout: settings.workspace,
71
+ })
72
+ : null;
73
+ const started = Date.now();
74
+ const report = await synchronizeManager(
75
+ {
76
+ definition: scoped,
77
+ nodes,
78
+ revision: options.revision,
79
+ apply: Boolean(options.apply),
80
+ ...canary(canonical, options),
81
+ progress: (node, phase) => ui.progress.step(node, phase),
82
+ finished: (node) => ui.progress.finished(node),
83
+ manager: () =>
84
+ convergeManager(
85
+ canonical,
86
+ {
87
+ revision: options.revision!,
88
+ apply: Boolean(options.apply),
89
+ workspace: settings.workspace,
90
+ },
91
+ run
92
+ ),
93
+ },
94
+ run,
95
+ deployWithMonitoring(payload, canonical, run, ui)
96
+ ).finally(() => ui.progress.stop());
97
+ if (settle) report.checks.push(await settle(report));
98
+ return outcome(report, planFingerprint, started);
99
+ }
100
+
101
+ /** A revision new to the fleet is proven on the first declared worker before
102
+ * the rest; repeating one that already converged everywhere needs no canary,
103
+ * so every worker converges at once.
104
+ */
105
+ function canary(
106
+ canonical: FleetDefinition,
107
+ options: FleetSyncOptions
108
+ ): { canary?: string } {
109
+ const first = canonical.workers[0];
110
+ return first && !options.rolledOut ? { canary: first.id } : {};
111
+ }
112
+
113
+ /** Saves the full JSON report, then renders the short human summary. */
114
+ async function outcome(
115
+ report: Awaited<ReturnType<typeof synchronizeManager>>,
116
+ planFingerprint: string,
117
+ started: number
118
+ ): Promise<CommandOutcome> {
119
+ const data = { ...report, planFingerprint };
120
+ const reportPath = await saveReport(machineRoot(), data);
121
+ return {
122
+ exitCode: report.ok ? 0 : 1,
123
+ data,
124
+ text: managerText(report, {
125
+ color: stdoutColor(),
126
+ durationMs: Date.now() - started,
127
+ ...(reportPath ? { reportPath } : {}),
128
+ }),
129
+ };
130
+ }
131
+
132
+ function enrollmentRequired(
133
+ canonical: FleetDefinition,
134
+ revision: string
135
+ ): CommandOutcome {
136
+ const report = managerFailure(
137
+ canonical,
138
+ revision,
139
+ 'FLEET_MANAGER_ENROLLMENT_REQUIRED',
140
+ 'Create or migrate private manager connection overrides for the canonical fleet. Resolve conflicting IDs/SSH aliases; discovery cannot authorize membership.'
141
+ );
142
+ return failed(report);
143
+ }
144
+
145
+ function failed(report: ReturnType<typeof managerFailure>): CommandOutcome {
146
+ return {
147
+ exitCode: 1,
148
+ data: report,
149
+ text: managerText(report, { color: stdoutColor() }),
150
+ };
151
+ }
152
+
153
+ function deployWithMonitoring(
154
+ payload: Awaited<ReturnType<typeof prepareBootstrap>>,
155
+ canonical: FleetDefinition,
156
+ run: Runner,
157
+ ui: {
158
+ progress: SyncProgress;
159
+ interactiveAuthorization: boolean;
160
+ }
161
+ ): DeployNode {
162
+ return async (node, revision) => {
163
+ if (!payload || payload.revision !== revision)
164
+ return {
165
+ node: node.id,
166
+ checks: [
167
+ {
168
+ id: 'source:bootstrap',
169
+ status: 'fail' as const,
170
+ summary: 'FLEET_APPROVED_BOOTSTRAP_UNAVAILABLE',
171
+ remedy: {
172
+ action:
173
+ 'Install Git on the manager and verify the approved source contains the tracked remote bootstrap and matches this package’s canonical fleet definition. Use the matching package release after a membership change. No manager credentials are forwarded.',
174
+ },
175
+ },
176
+ ],
177
+ };
178
+ // Password prompts need a quiet terminal: no spinner while authorizing.
179
+ if (ui.interactiveAuthorization) {
180
+ ui.progress.stop();
181
+ const checks = await authorizePrivilege(node, payload.authorizeSudo, run);
182
+ if (checks.length) return { node: node.id, checks };
183
+ }
184
+ const vault = { interactive: ui.interactiveAuthorization };
185
+ if (ui.interactiveAuthorization) ui.progress.stop();
186
+ const before = await authorizeVault(node, vault, run);
187
+ ui.progress.step(node.id, 'Converging (remote lock, install, verify)');
188
+ const result = await bootstrapRemote(node, payload, run);
189
+ // A fresh node gets vault from the bootstrap; log it in now. Its provider
190
+ // checks in this run predate the login, so the next sync verifies them.
191
+ result.checks.push(
192
+ ...(before.length ? before : await authorizeVault(node, vault, run))
193
+ );
194
+ // Workers are Linux by definition.
195
+ ui.progress.step(node.id, 'Verifying SSH, OS and T3');
196
+ result.checks.push(...(await inspectBootstrap(node, run, 'linux')));
197
+ result.checks.push(...(await verifyT3Endpoint(node, run)));
198
+ // A worker T3 restarted by this sync (idle policy heal) needs a minute or
199
+ // two before the manager's link reconnects.
200
+ const restarted = result.checks.some(
201
+ (check) => check.summary === T3_RESTARTED_SUMMARY
202
+ );
203
+ result.checks.push(
204
+ await verifyT3Linked(node, run, {
205
+ reconnectMs: restarted ? 180_000 : 0,
206
+ })
207
+ );
208
+ if (
209
+ result.checks.some(
210
+ (check) => check.id === 'configuration:apply' && check.status === 'pass'
211
+ )
212
+ ) {
213
+ result.checks = result.checks.filter(
214
+ (check) => check.id !== 'beszel:freshness'
215
+ );
216
+ ui.progress.step(node.id, 'Enrolling monitoring');
217
+ result.checks.push(
218
+ ...(await synchronizeMonitoring(node, canonical, revision, run))
219
+ );
220
+ }
221
+ return result;
222
+ };
223
+ }
224
+
225
+ function scopeDefinition(canonical: FleetDefinition, ids?: string[]) {
226
+ return ids?.length
227
+ ? {
228
+ ...canonical,
229
+ workers: canonical.workers.filter((node) => ids.includes(node.id)),
230
+ }
231
+ : canonical;
232
+ }
233
+ function interfaceOptions(options: FleetSyncOptions) {
234
+ return {
235
+ progress: syncProgress(!options.json),
236
+ interactiveAuthorization: Boolean(
237
+ options.interactiveAuthorization &&
238
+ !options.json &&
239
+ !options.nonInteractive
240
+ ),
241
+ };
242
+ }
@@ -0,0 +1,26 @@
1
+ import { chmod, mkdir, rename, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ /** Saves the full JSON report of the latest run (0600, manager-private) so an
5
+ * agent can debug without rerunning. Reports carry codes and remedies only,
6
+ * never secret values. Returns null when it cannot be written.
7
+ */
8
+ export async function saveReport(
9
+ root: string,
10
+ data: unknown
11
+ ): Promise<string | null> {
12
+ try {
13
+ const directory = join(root, 'reports');
14
+ await mkdir(directory, { recursive: true, mode: 0o700 });
15
+ const path = join(directory, 'last-sync.json');
16
+ const partial = `${path}.partial`;
17
+ await writeFile(partial, `${JSON.stringify(data, null, 2)}\n`, {
18
+ mode: 0o600,
19
+ });
20
+ await chmod(partial, 0o600);
21
+ await rename(partial, path);
22
+ return path;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
@@ -0,0 +1,138 @@
1
+ import { join } from 'node:path';
2
+ import { currentPlatform } from '../platform';
3
+ import type { Platform } from '../platform/types';
4
+ import type { Runner } from '../types';
5
+ import type { CommandOutcome } from './contracts';
6
+ import { machineRoot } from './fleet-sync';
7
+
8
+ const LABEL = 'personal-fleet-sync';
9
+ declare const PERSONAL_VERSION: string | undefined;
10
+ declare const PERSONAL_SOURCE_REVISION: string | null;
11
+ export type ScheduleAction =
12
+ 'enable' | 'pause' | 'resume' | 'disable' | 'status';
13
+ interface ScheduleInput {
14
+ action: ScheduleAction;
15
+ revision: string;
16
+ config: string;
17
+ authorized: boolean;
18
+ }
19
+ export async function managerSchedule(
20
+ input: ScheduleInput,
21
+ run: Runner,
22
+ platform: Platform = currentPlatform()
23
+ ): Promise<CommandOutcome> {
24
+ if (input.action === 'status')
25
+ return {
26
+ exitCode: 0,
27
+ data: {
28
+ version: 2,
29
+ command: 'fleet-sync',
30
+ execution: 'manager-push',
31
+ scheduling: scheduleStatus(platform),
32
+ },
33
+ };
34
+ if (!input.authorized)
35
+ return required('Manager scheduling changes require --apply --yes.');
36
+ if (input.action === 'pause' || input.action === 'disable')
37
+ return removeSchedule(input, platform);
38
+ return enableSchedule(input, run, platform);
39
+ }
40
+ function removeSchedule(
41
+ input: ScheduleInput,
42
+ platform: Platform
43
+ ): CommandOutcome {
44
+ const result = platform.removeScheduledJob(LABEL);
45
+ return {
46
+ exitCode: result.ok ? 0 : 1,
47
+ data: {
48
+ version: 2,
49
+ command: 'fleet-sync',
50
+ ok: result.ok,
51
+ scheduling: input.action,
52
+ summary: result.ok
53
+ ? 'Manager schedule removed; node application services are untouched.'
54
+ : 'Manager scheduler removal failed; inspect its platform status.',
55
+ },
56
+ };
57
+ }
58
+ async function enableSchedule(
59
+ input: ScheduleInput,
60
+ run: Runner,
61
+ platform: Platform
62
+ ): Promise<CommandOutcome> {
63
+ if (
64
+ typeof PERSONAL_VERSION !== 'string' ||
65
+ !/^\d+\.\d+\.\d+$/.test(PERSONAL_VERSION) ||
66
+ typeof PERSONAL_SOURCE_REVISION !== 'string'
67
+ )
68
+ return required(
69
+ 'Scheduling requires a published CLI with clean source provenance; dirty review artifacts cannot enable unattended deployment.'
70
+ );
71
+ const root = machineRoot();
72
+ const prefix = join(root, 'runtime');
73
+ const installed = await run(
74
+ [
75
+ 'npm',
76
+ 'install',
77
+ '--prefix',
78
+ prefix,
79
+ '--ignore-scripts',
80
+ '--no-audit',
81
+ '--no-fund',
82
+ `dev-fleet@${PERSONAL_VERSION}`,
83
+ ],
84
+ { timeoutMs: 120_000 }
85
+ );
86
+ if (!installed.ok)
87
+ return required(
88
+ 'The exact CLI package could not be installed in the managed manager runtime. No schedule was enabled.'
89
+ );
90
+ const result = platform.installScheduledJob({
91
+ label: LABEL,
92
+ intervalSeconds: 1800,
93
+ command: [
94
+ process.execPath,
95
+ join(prefix, 'node_modules/dev-fleet/cli.mjs'),
96
+ 'fleet-sync',
97
+ '--config',
98
+ input.config,
99
+ '--revision',
100
+ input.revision,
101
+ '--apply',
102
+ '--yes',
103
+ '--non-interactive',
104
+ '--json',
105
+ ],
106
+ logDir: join(root, 'logs'),
107
+ env: { PATH: process.env['PATH'] ?? '/usr/bin:/bin' },
108
+ });
109
+ return {
110
+ exitCode: result.ok ? 0 : 1,
111
+ data: {
112
+ version: 2,
113
+ command: 'fleet-sync',
114
+ ok: result.ok,
115
+ scheduling: result.ok ? 'enabled' : 'action-required',
116
+ revision: input.revision,
117
+ summary:
118
+ 'Manager schedule uses one exact package and approved revision. Linux user scheduling requires lingering; macOS LaunchAgent requires the manager user session. Inspect status and logs; boot acceptance is separate.',
119
+ },
120
+ };
121
+ }
122
+ function required(summary: string): CommandOutcome {
123
+ return {
124
+ exitCode: 1,
125
+ data: {
126
+ version: 2,
127
+ command: 'fleet-sync',
128
+ ok: false,
129
+ scheduling: 'action-required',
130
+ summary,
131
+ },
132
+ };
133
+ }
134
+
135
+ function scheduleStatus(platform: Platform) {
136
+ const { installed, supported } = platform.scheduledJobStatus(LABEL);
137
+ return { installed, supported };
138
+ }
@@ -0,0 +1,86 @@
1
+ import type { Enrollment } from '../machine/state';
2
+ import { replicaCheck } from '../state/check';
3
+ import { storeEnrollment } from '../state/enrollment';
4
+ import { adoptEarlier } from '../state/legacy';
5
+ import { databasePath, stateRoot } from '../state/paths';
6
+ import { writeDocument } from '../state/store';
7
+ import { pending, push, type Standing, type StateContext } from '../state/sync';
8
+ import type { Check } from '../types';
9
+
10
+ /** What one fleet-sync run records in the fleet state: the revision it was
11
+ * approved for and the configuration it runs with, then what it applied, then
12
+ * an upload so the bucket holds all of it.
13
+ */
14
+ export interface RunRecord {
15
+ /** Before any machine changes: this revision is the approved one. */
16
+ approve(): void;
17
+ /** Once the result is known. The check says whether the bucket holds it. */
18
+ settle(converged: boolean): Promise<Check>;
19
+ /** For a run that stopped before its result: its approval is still uploaded. */
20
+ abandon(): Promise<void>;
21
+ }
22
+
23
+ export function runRecord(
24
+ previous: Enrollment | null,
25
+ revision: string,
26
+ state: {
27
+ context: StateContext;
28
+ standing: Standing;
29
+ env?: NodeJS.ProcessEnv;
30
+ /** The configuration this run reads from a project's fleet.json. Stored
31
+ * so that a run started anywhere else (the schedule, a hand-off from a
32
+ * worker, a replacement manager) uses the one last applied.
33
+ */
34
+ definition?: string;
35
+ }
36
+ ): RunRecord {
37
+ const env = state.env ?? process.env;
38
+ const approval: Enrollment = {
39
+ ...previous,
40
+ version: 1,
41
+ id: 'manager',
42
+ approvedRevision: revision,
43
+ };
44
+ let settled = false;
45
+ /** Uploads only what this run changed; otherwise the bucket stands as it
46
+ * did when the run started.
47
+ */
48
+ const publish = (): Promise<Standing> =>
49
+ pending(state.context)
50
+ ? push(state.context)
51
+ : Promise.resolve(state.standing);
52
+ return {
53
+ approve: () => {
54
+ storeEnrollment(approval, env);
55
+ if (state.definition === undefined) return;
56
+ adoptEarlier(env);
57
+ writeDocument(
58
+ databasePath(stateRoot(env)),
59
+ 'definition',
60
+ state.definition
61
+ );
62
+ },
63
+ async settle(converged) {
64
+ settled = true;
65
+ if (converged)
66
+ storeEnrollment(
67
+ {
68
+ ...approval,
69
+ appliedRevision: revision,
70
+ ...(previous?.appliedRevision &&
71
+ previous.appliedRevision !== revision
72
+ ? { previousRevision: previous.appliedRevision }
73
+ : {}),
74
+ },
75
+ env
76
+ );
77
+ // The fleet converged either way: a bucket problem is reported, not fatal.
78
+ return replicaCheck(await publish().catch(() => 'unreachable' as const), {
79
+ advisory: true,
80
+ });
81
+ },
82
+ async abandon() {
83
+ if (!settled) await publish().catch(() => undefined);
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,166 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fleet as definition } from '../fleet-config';
3
+ import { sshArgs, shellQuote } from '../ssh';
4
+ import { resolveVaultConfig, usesVault } from '../vault-config';
5
+ import type { Check, FleetNode, Runner } from '../types';
6
+
7
+ const TOOL_PATH =
8
+ 'export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"';
9
+
10
+ /** Runs an interactive command with the operator's terminal attached. */
11
+ export type TerminalRunner = (args: string[]) => boolean;
12
+
13
+ const terminal: TerminalRunner = (args) =>
14
+ spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' }).status === 0;
15
+
16
+ type VaultState = 'authenticated' | 'unauthenticated' | 'unavailable';
17
+
18
+ /** Vault field holding the issued token for one worker node. */
19
+ export function workerTokenField(node: string): string {
20
+ return `FLEET_WORKER_TOKEN_${node.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
21
+ }
22
+
23
+ /**
24
+ * The manager reads each node's own token from Vault, so it must be logged in
25
+ * first. Interactive runs prompt once on this terminal; the password never
26
+ * passes through this process.
27
+ */
28
+ export async function authorizeManager(
29
+ options: { interactive: boolean },
30
+ run: Runner,
31
+ attach: TerminalRunner = terminal
32
+ ): Promise<boolean> {
33
+ if (!usesVault()) return true;
34
+ if ((await run(lookup(), { timeoutMs: 20_000 })).ok) return true;
35
+ if (!options.interactive) return false;
36
+ process.stderr.write(
37
+ 'This manager is not logged in to Vault. Log in once; each machine then gets its own token.\n'
38
+ );
39
+ attach(login());
40
+ return (await run(lookup(), { timeoutMs: 20_000 })).ok;
41
+ }
42
+
43
+ /**
44
+ * Each node keeps its own periodic Vault token (renewed by the worker job). The
45
+ * proxy key, GitHub token and provider credentials all come from that session,
46
+ * so a node without one has no working proxy providers. The node's
47
+ * issued token is delivered once over SSH stdin, never as an argument;
48
+ * interactive runs fall back to a password login on the node's own terminal.
49
+ */
50
+ export async function authorizeVault(
51
+ node: FleetNode,
52
+ options: { interactive: boolean },
53
+ run: Runner,
54
+ attach: TerminalRunner = terminal
55
+ ): Promise<Check[]> {
56
+ // The other secrets providers need no per-node session.
57
+ if (!usesVault()) return [];
58
+ const before = await vaultState(node, run);
59
+ if (before === 'authenticated') return [pass(node)];
60
+ // Fresh node: the locked toolchain (including vault) is not installed yet.
61
+ if (before === 'unavailable') return [];
62
+ if (await deliverWorkerToken(node, run)) return [pass(node)];
63
+ if (!options.interactive) return [required(node)];
64
+ process.stderr.write(
65
+ `[${node.id}] No issued worker token found. Enter the Vault password to log this machine in.\n`
66
+ );
67
+ attach(remote(node, login(), '-t'));
68
+ return [
69
+ (await vaultState(node, run)) === 'authenticated'
70
+ ? pass(node)
71
+ : required(node),
72
+ ];
73
+ }
74
+
75
+ async function deliverWorkerToken(
76
+ node: FleetNode,
77
+ run: Runner
78
+ ): Promise<boolean> {
79
+ const { addr, mount, project, config } = resolveVaultConfig();
80
+ const secret = await run(
81
+ [
82
+ 'vault',
83
+ 'kv',
84
+ 'get',
85
+ `-address=${addr}`,
86
+ `-mount=${mount}`,
87
+ `-field=${workerTokenField(node.id)}`,
88
+ `${project}/${config}`,
89
+ ],
90
+ { timeoutMs: 20_000 }
91
+ );
92
+ const token = secret.ok ? secret.stdout.trim() : '';
93
+ if (!/^[\x21-\x7e]{16,4096}$/.test(token)) return false;
94
+ const delivered = await run(
95
+ remote(node, ['vault', 'login', `-address=${addr}`, '-no-print', '-']),
96
+ { input: token + '\n', timeoutMs: 30_000 }
97
+ );
98
+ return delivered.ok && (await vaultState(node, run)) === 'authenticated';
99
+ }
100
+
101
+ async function vaultState(node: FleetNode, run: Runner): Promise<VaultState> {
102
+ const result = await run(
103
+ [
104
+ ...sshArgs(node),
105
+ `${TOOL_PATH}; command -v vault >/dev/null 2>&1 || { printf vault-missing; exit 1; }; ${quote(lookup())} >/dev/null 2>&1`,
106
+ ],
107
+ { timeoutMs: 20_000 }
108
+ );
109
+ if (result.ok) return 'authenticated';
110
+ // Unreachable nodes are reported by the bootstrap's own SSH checks.
111
+ if (result.reason !== 'exit') return 'unavailable';
112
+ return result.stdout.includes('vault-missing')
113
+ ? 'unavailable'
114
+ : 'unauthenticated';
115
+ }
116
+
117
+ function lookup(): string[] {
118
+ const { addr } = resolveVaultConfig();
119
+ return ['vault', 'token', 'lookup', `-address=${addr}`, '-format=json'];
120
+ }
121
+
122
+ function login(): string[] {
123
+ const { addr } = resolveVaultConfig();
124
+ return [
125
+ 'vault',
126
+ 'login',
127
+ `-address=${addr}`,
128
+ '-method=userpass',
129
+ '-no-print',
130
+ `username=${definition.worker!.vaultUser}`,
131
+ ];
132
+ }
133
+
134
+ /** Same verified connection options; `-t` attaches a terminal for prompts. */
135
+ export function remote(
136
+ node: FleetNode,
137
+ command: string[],
138
+ tty: '-T' | '-t' = '-T'
139
+ ): string[] {
140
+ const args = sshArgs(node).map((arg) => (arg === '-T' ? tty : arg));
141
+ return [...args, `${TOOL_PATH}; ${quote(command)}`];
142
+ }
143
+
144
+ function quote(command: string[]): string {
145
+ return command.map(shellQuote).join(' ');
146
+ }
147
+
148
+ function pass(node: FleetNode): Check {
149
+ return {
150
+ id: 'auth:vault',
151
+ status: 'pass',
152
+ summary: `${node.id} has its own Vault session; proxy providers can read their key.`,
153
+ };
154
+ }
155
+
156
+ function required(node: FleetNode): Check {
157
+ const { addr } = resolveVaultConfig();
158
+ return {
159
+ id: 'auth:vault',
160
+ status: 'fail',
161
+ summary: 'VAULT_LOGIN_REQUIRED',
162
+ remedy: {
163
+ action: `Run devfleet fleet-sync in a terminal, or store this node's token in the shared Vault document as ${workerTokenField(node.id)}. Manual fallback on ${node.id}: vault login -address=${addr} -method=userpass username=${definition.worker!.vaultUser}. Proxy providers in T3 stay unavailable until then.`,
164
+ },
165
+ };
166
+ }