@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,185 @@
1
+ import type { FleetNode, Runner, Check } from '../types';
2
+ import type { FleetDefinition } from '../definition';
3
+ import { hubUrl, roleOf } from '../topology';
4
+ import { discoverTailnet } from '../tailscale';
5
+ import { fleetIdentity, hubAccess } from '../monitoring/hub-access';
6
+ import { enrollBeszel } from '../monitoring/enroll';
7
+ import { verifyBeszel } from '../monitoring/beszel';
8
+ import { installRemoteAgent } from '../monitoring/remote';
9
+ import { installManagerAgent } from '../monitoring/manager-agent';
10
+
11
+ export interface MonitoringDependencies {
12
+ access?: typeof hubAccess;
13
+ sampleDeadlineMs?: number;
14
+ }
15
+
16
+ /** Enrolls one worker's Beszel agent against the manager's hub. Workers never
17
+ * host the hub; it is converged on the manager by convergeManagerHub. Called
18
+ * after source bootstrap, only under approved manager application.
19
+ */
20
+ export function synchronizeMonitoringWith(
21
+ dependencies: MonitoringDependencies = {}
22
+ ) {
23
+ return async function synchronize(
24
+ node: FleetNode,
25
+ definition: FleetDefinition,
26
+ revision: string,
27
+ run: Runner
28
+ ): Promise<Check[]> {
29
+ try {
30
+ return await enrollWorker(node, definition, {
31
+ revision,
32
+ run,
33
+ ...dependencies,
34
+ });
35
+ } catch (error) {
36
+ return unavailable(errorCode(error));
37
+ }
38
+ };
39
+ }
40
+ export const synchronizeMonitoring = synchronizeMonitoringWith();
41
+
42
+ /** The manager is always a monitored system on its own hub: after the hub
43
+ * converges, fleet-sync enrolls the manager like a worker, but installs its
44
+ * agent locally as a per-user launch agent (no SSH, no sudo).
45
+ */
46
+ export async function enrollManagerMonitoring(
47
+ definition: FleetDefinition,
48
+ workspace: string,
49
+ run: Runner,
50
+ dependencies: MonitoringDependencies & {
51
+ install?: typeof installManagerAgent;
52
+ } = {}
53
+ ): Promise<Check[]> {
54
+ const { manager } = definition;
55
+ try {
56
+ const hub = hubUrl(definition);
57
+ if (!(await hubReachable(hub, run)))
58
+ return unavailable('BESZEL_HUB_UNREACHABLE');
59
+ const access = await (dependencies.access ?? hubAccess)(hub);
60
+ const enrollment = await enrollBeszel(
61
+ {
62
+ node: manager.id,
63
+ host: manager.network.tailscaleName,
64
+ hub,
65
+ userId: access.userId,
66
+ },
67
+ access.request
68
+ );
69
+ const checks = await (dependencies.install ?? installManagerAgent)(
70
+ workspace,
71
+ { node: manager.id, enrollment },
72
+ run
73
+ );
74
+ if (checks.some((check) => check.status === 'fail')) return checks;
75
+ const evidence = await waitForSample(
76
+ hub,
77
+ manager.id,
78
+ access.request,
79
+ dependencies.sampleDeadlineMs ?? 90_000
80
+ );
81
+ return [...checks, ...evidence.checks];
82
+ } catch (error) {
83
+ return unavailable(errorCode(error));
84
+ }
85
+ }
86
+
87
+ async function enrollWorker(
88
+ node: FleetNode,
89
+ definition: FleetDefinition,
90
+ context: MonitoringDependencies & { revision: string; run: Runner }
91
+ ): Promise<Check[]> {
92
+ const { revision, run } = context;
93
+ if (roleOf(definition, node.id) !== 'worker')
94
+ return unavailable('BESZEL_NOT_ENROLLED');
95
+ const hub = hubUrl(definition);
96
+ if (!(await hubReachable(hub, run)))
97
+ return unavailable('BESZEL_HUB_UNREACHABLE');
98
+ const tailnet = await discoverTailnet(run);
99
+ const target = tailnet.computers.find((peer) => peer.id === node.tailscaleId);
100
+ if (!target) return unavailable('BESZEL_NODE_TAILNET_REQUIRED');
101
+ const access = await (context.access ?? hubAccess)(hub);
102
+ const enrollment = await enrollBeszel(
103
+ { node: node.id, host: target.dns, hub, userId: access.userId },
104
+ access.request
105
+ );
106
+ const checks = await installRemoteAgent(node, revision, enrollment, run);
107
+ if (checks.some((check) => check.status === 'fail')) return checks;
108
+ const evidence = await waitForSample(
109
+ hub,
110
+ node.id,
111
+ access.request,
112
+ context.sampleDeadlineMs ?? 90_000
113
+ );
114
+ return [...checks, ...evidence.checks];
115
+ }
116
+
117
+ const PASSTHROUGH = [
118
+ 'BESZEL_ACCOUNT_REQUIRED',
119
+ 'BESZEL_ACCOUNT_INVALID',
120
+ 'BESZEL_AUTH_REQUIRED',
121
+ 'BESZEL_API_UNAVAILABLE',
122
+ ];
123
+ function errorCode(error: unknown): string {
124
+ return error instanceof Error && PASSTHROUGH.includes(error.message)
125
+ ? error.message
126
+ : 'BESZEL_ENROLLMENT_INCOMPLETE';
127
+ }
128
+
129
+ async function hubReachable(hub: string, run: Runner): Promise<boolean> {
130
+ const probe = await run(
131
+ [
132
+ 'curl',
133
+ '-fsS',
134
+ '--max-time',
135
+ '10',
136
+ '-o',
137
+ '/dev/null',
138
+ `${hub}/api/health`,
139
+ ],
140
+ { timeoutMs: 15_000 }
141
+ );
142
+ return probe.ok;
143
+ }
144
+
145
+ const remedies = (): Record<string, string> => ({
146
+ BESZEL_ACCOUNT_REQUIRED: `The hub rejected the fleet login ${fleetIdentity()}. A new hub on the manager creates it on first start from BESZEL_FLEET_AUTH; an older hub needs it once, by its superuser, as a verified users record with that Vault password. Never paste the password on a command line.`,
147
+ BESZEL_HUB_UNREACHABLE:
148
+ 'The hub runs on the manager; keep it awake and Docker Desktop running, then rerun fleet-sync from the manager so it converges the hub first.',
149
+ BESZEL_NODE_TAILNET_REQUIRED:
150
+ 'The worker is not visible on the tailnet. Sign it in to Tailscale and record its tailscaleId in the local roster, then rerun fleet-sync.',
151
+ BESZEL_NOT_ENROLLED:
152
+ 'Only workers declared in fleet.json run the agent over SSH; fleet-sync installs the manager’s own agent locally after its hub.',
153
+ });
154
+ function unavailable(code: string): Check[] {
155
+ return [
156
+ {
157
+ id: 'beszel:enrollment',
158
+ status: 'fail',
159
+ summary: code,
160
+ remedy: {
161
+ action:
162
+ remedies()[code] ??
163
+ "Verify the manager's private hub is running and the manager has BESZEL_FLEET_AUTH through existing Vault authorization. The normal hub account must access these systems. Inspect duplicate registration/host conflicts before retry; no token or raw response is printed.",
164
+ },
165
+ },
166
+ ];
167
+ }
168
+
169
+ async function waitForSample(
170
+ hub: string,
171
+ node: string,
172
+ request: Parameters<typeof verifyBeszel>[2],
173
+ deadlineMs: number
174
+ ) {
175
+ const deadline = Date.now() + deadlineMs;
176
+ let evidence = await verifyBeszel(hub, node, request);
177
+ while (
178
+ Date.now() < deadline &&
179
+ evidence.checks.some((check) => check.summary === 'BESZEL_METRICS_STALE')
180
+ ) {
181
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
182
+ evidence = await verifyBeszel(hub, node, request);
183
+ }
184
+ return evidence;
185
+ }
@@ -0,0 +1,317 @@
1
+ import { Chalk, type ChalkInstance } from 'chalk';
2
+ import type { ManagerNode, ManagerReport } from '../manager-sync';
3
+ import { blocking, type Check, type Status } from '../types';
4
+
5
+ /** Report columns: each owns the checks whose id matches. A check matches the
6
+ * first column that claims it; anything unclaimed still counts in RESULT.
7
+ */
8
+ const COLUMNS: { title: string; matches: (id: string) => boolean }[] = [
9
+ { title: 'NET', matches: (id) => id.startsWith('tailscale:') },
10
+ { title: 'SSH', matches: (id) => id.startsWith('ssh:') },
11
+ {
12
+ title: 'OS',
13
+ matches: (id) =>
14
+ ['ubuntu:baseline', 'configuration:apply'].includes(id) ||
15
+ id.startsWith('manager:'),
16
+ },
17
+ {
18
+ title: 'T3',
19
+ matches: (id) =>
20
+ [
21
+ 't3:service',
22
+ 't3:policy',
23
+ 't3:providers',
24
+ 't3:antigravity-key',
25
+ 't3:private-route',
26
+ 'worker:t3-recovery',
27
+ 'worker:t3-health',
28
+ 'worker:service-recovery',
29
+ ].includes(id),
30
+ },
31
+ {
32
+ title: 'LINK',
33
+ matches: (id) =>
34
+ ['t3:manager-endpoint', 't3:manager-origin', 't3:paired-access'].includes(
35
+ id
36
+ ),
37
+ },
38
+ {
39
+ title: 'PROXY',
40
+ matches: (id) =>
41
+ id.startsWith('provider:') ||
42
+ id.startsWith('proxy:') ||
43
+ id === 'worker:proxy-singleton',
44
+ },
45
+ { title: 'AGENTS', matches: (id) => id.startsWith('agent:') },
46
+ {
47
+ title: 'MON',
48
+ matches: (id) =>
49
+ id.startsWith('beszel:') || id === 'worker:service:beszel-agent',
50
+ },
51
+ ];
52
+
53
+ const ORDER: Status[] = ['fail', 'warn', 'skip', 'pass'];
54
+ const WORD: Record<Status, string> = {
55
+ fail: 'FAIL',
56
+ warn: 'WARN',
57
+ skip: 'SKIP',
58
+ pass: 'PASS',
59
+ };
60
+ const SYMBOL: Record<Status, string> = {
61
+ fail: '✖',
62
+ warn: '▲',
63
+ skip: '○',
64
+ pass: '✔',
65
+ };
66
+
67
+ export interface OutputOptions {
68
+ /** Colors and symbols; plain words otherwise (pipes, NO_COLOR, agents). */
69
+ color?: boolean;
70
+ durationMs?: number;
71
+ /** Where the full JSON report of this run was saved. */
72
+ reportPath?: string;
73
+ }
74
+
75
+ /** Colors only for an interactive stdout that allows them. */
76
+ export function stdoutColor(): boolean {
77
+ if (process.env['NO_COLOR'] !== undefined) return false;
78
+ if (process.env['FORCE_COLOR'] !== undefined) return true;
79
+ return Boolean(process.stdout.isTTY) && process.env['TERM'] !== 'dumb';
80
+ }
81
+
82
+ /** Short, stable report: one header line, a node grid, then each problem once
83
+ * (deduplicated across nodes) with its code and fix. Every line is greppable:
84
+ * `FAIL <check> <CODE> [nodes]` in plain mode, the same with symbols in color.
85
+ */
86
+ export function managerText(
87
+ report: ManagerReport,
88
+ options: OutputOptions = {}
89
+ ): string {
90
+ const c = new Chalk({ level: options.color ? 1 : 0 });
91
+ const mark = (status: Status | null): string =>
92
+ status === null
93
+ ? c.dim('·')
94
+ : paint(c, status, options.color ? SYMBOL[status] : WORD[status]);
95
+ const all = [
96
+ ...report.checks.map((check) => ({ node: 'fleet', check })),
97
+ ...report.nodes.flatMap((node) =>
98
+ node.checks.map((check) => ({ node: node.node, check }))
99
+ ),
100
+ ];
101
+ const counts = count(all.map((entry) => entry.check));
102
+ const failing = all.some((entry) => blocking(entry.check));
103
+ const lines: string[] = [header(c, report, failing, options)];
104
+ lines.push('', ...grid(c, report.nodes, mark, options.color === true));
105
+ lines.push('', totals(c, counts, options.color === true));
106
+ for (const status of ['fail', 'warn', 'skip'] as const) {
107
+ const problems = groupProblems(
108
+ all.filter((entry) => entry.check.status === status)
109
+ );
110
+ if (!problems.length) continue;
111
+ lines.push('', c.bold(SECTION[status]));
112
+ for (const problem of problems)
113
+ lines.push(...problemLines(c, problem, options.color === true));
114
+ }
115
+ lines.push('');
116
+ if (report.dashboardUrl)
117
+ lines.push(`${c.dim('dashboard')} ${c.cyan(report.dashboardUrl)}`);
118
+ if (options.reportPath)
119
+ lines.push(
120
+ `${c.dim('report ')} ${options.reportPath} ${c.dim('(full JSON; or rerun with --json)')}`
121
+ );
122
+ if (report.candidates)
123
+ lines.push(
124
+ `${c.dim('tailnet ')} ${report.candidates} unmanaged device(s) seen; discovery never enrolls them`
125
+ );
126
+ if (!report.applied) lines.push(c.dim('No node deployment was performed.'));
127
+ return stableUrls(lines.join('\n'));
128
+ }
129
+
130
+ const TAILNET_URL =
131
+ /\bhttps?:\/\/[a-z0-9.-]+\.ts\.net(?::\d+)?(?:\/[^\s'",;)]*[^\s'",;).])?/gi;
132
+
133
+ /** Printed output names only stable public URLs. A tailnet
134
+ * MagicDNS address in any summary or remedy is replaced, never shown.
135
+ */
136
+ export function stableUrls(text: string): string {
137
+ return text.replace(TAILNET_URL, 'its private tailnet address');
138
+ }
139
+
140
+ const SECTION: Record<'fail' | 'warn' | 'skip', string> = {
141
+ fail: 'Failing — fix these',
142
+ warn: 'Warnings — outside the fleet; sync still succeeds',
143
+ skip: 'Not observable on this hardware',
144
+ };
145
+
146
+ function header(
147
+ c: ChalkInstance,
148
+ report: ManagerReport,
149
+ failing: boolean,
150
+ options: OutputOptions
151
+ ): string {
152
+ const verdict = failing
153
+ ? c.red.bold(`${options.color ? '✖ ' : ''}ACTION REQUIRED`)
154
+ : report.applied
155
+ ? c.green.bold(`${options.color ? '✔ ' : ''}CONVERGED`)
156
+ : c.green.bold(`${options.color ? '✔ ' : ''}PREVIEW OK`);
157
+ const parts = [
158
+ c.bold('fleet-sync'),
159
+ verdict,
160
+ `${c.dim('revision')} ${(report.desiredRevision ?? 'none').slice(0, 12)}`,
161
+ ];
162
+ if (options.durationMs !== undefined)
163
+ parts.push(`${c.dim('in')} ${duration(options.durationMs)}`);
164
+ return parts.join(' ');
165
+ }
166
+
167
+ function grid(
168
+ c: ChalkInstance,
169
+ nodes: ManagerNode[],
170
+ mark: (status: Status | null) => string,
171
+ color: boolean
172
+ ): string[] {
173
+ const width = Math.max(4, ...nodes.map((node) => node.node.length));
174
+ const cell = (text: string, visible: number, size: number) =>
175
+ text + ' '.repeat(Math.max(1, size - visible + 1));
176
+ const head = [
177
+ 'NODE'.padEnd(width + 1),
178
+ 'ROLE'.padEnd(8),
179
+ ...COLUMNS.map((column) => column.title.padEnd(6)),
180
+ 'RESULT',
181
+ ].join(' ');
182
+ const rows = nodes.map((node) => {
183
+ const statuses = COLUMNS.map((column) =>
184
+ worst(node.checks.filter((check) => column.matches(check.id)))
185
+ );
186
+ const failing = node.checks.filter(blocking).length;
187
+ const result =
188
+ node.reachability !== 'online' && node.role !== 'manager'
189
+ ? c.red(node.reachability.toUpperCase())
190
+ : failing
191
+ ? c.red(`${failing} failing`)
192
+ : node.appliedRevision
193
+ ? c.green('ready')
194
+ : c.yellow('not applied');
195
+ return [
196
+ cell(c.bold(node.node), node.node.length, width),
197
+ cell(c.dim(node.role ?? 'worker'), (node.role ?? 'worker').length, 7),
198
+ ...statuses.map((status) =>
199
+ cell(
200
+ mark(status),
201
+ status === null ? 1 : color ? 1 : WORD[status].length,
202
+ 5
203
+ )
204
+ ),
205
+ result,
206
+ ].join(' ');
207
+ });
208
+ return [c.dim(head), ...rows];
209
+ }
210
+
211
+ interface Problem {
212
+ check: Check;
213
+ nodes: string[];
214
+ }
215
+
216
+ function groupProblems(entries: { node: string; check: Check }[]): Problem[] {
217
+ const groups = new Map<string, Problem>();
218
+ for (const { node, check } of entries) {
219
+ const key = [
220
+ check.id,
221
+ check.status,
222
+ check.summary,
223
+ check.remedy?.action ?? '',
224
+ ].join('\u0000');
225
+ const group = groups.get(key);
226
+ if (group) group.nodes.push(node);
227
+ else groups.set(key, { check, nodes: [node] });
228
+ }
229
+ return [...groups.values()];
230
+ }
231
+
232
+ function problemLines(
233
+ c: ChalkInstance,
234
+ { check, nodes }: Problem,
235
+ color: boolean
236
+ ): string[] {
237
+ const { code, detail } = splitSummary(check.summary);
238
+ const lead = paint(
239
+ c,
240
+ check.status,
241
+ color ? SYMBOL[check.status] : WORD[check.status]
242
+ );
243
+ const lines = [
244
+ `${lead} ${c.bold(check.id)} ${code ? paint(c, check.status, code) : ''}${code && detail ? ' ' : ''}${detail}`.trimEnd(),
245
+ ` ${c.dim('on')} ${nodes.join(', ')}`,
246
+ ];
247
+ if (check.remedy?.action)
248
+ lines.push(` ${c.cyan('→')} ${check.remedy.action}`);
249
+ if (check.remedy?.command)
250
+ lines.push(` ${c.cyan('$')} ${check.remedy.command}`);
251
+ return lines;
252
+ }
253
+
254
+ /** Summaries start with an UPPER_SNAKE code when there is one. */
255
+ export function splitSummary(summary: string): {
256
+ code: string;
257
+ detail: string;
258
+ } {
259
+ const match = /^([A-Z][A-Z0-9_]{2,})(?:\s+(.*))?$/s.exec(summary.trim());
260
+ return match
261
+ ? { code: match[1]!, detail: match[2] ?? '' }
262
+ : { code: '', detail: summary };
263
+ }
264
+
265
+ function totals(
266
+ c: ChalkInstance,
267
+ counts: Record<Status, number>,
268
+ color: boolean
269
+ ): string {
270
+ return ORDER.map((status) => {
271
+ const text = `${color ? `${SYMBOL[status]} ` : ''}${counts[status]} ${LABEL[status]}`;
272
+ // A zero count is not a signal: never show red/yellow for "0 failing".
273
+ return counts[status] ? paint(c, status, text) : c.dim(text);
274
+ }).join(c.dim(' '));
275
+ }
276
+
277
+ const LABEL: Record<Status, string> = {
278
+ fail: 'failing',
279
+ warn: 'warnings',
280
+ skip: 'skipped',
281
+ pass: 'passing',
282
+ };
283
+
284
+ function count(checks: Check[]): Record<Status, number> {
285
+ const counts: Record<Status, number> = { pass: 0, warn: 0, fail: 0, skip: 0 };
286
+ for (const check of checks)
287
+ counts[
288
+ blocking(check) || check.status !== 'fail' ? check.status : 'warn'
289
+ ]++;
290
+ return counts;
291
+ }
292
+
293
+ function worst(checks: Check[]): Status | null {
294
+ if (!checks.length) return null;
295
+ if (checks.some(blocking)) return 'fail';
296
+ if (
297
+ checks.some((check) => check.status !== 'pass' && check.status !== 'skip')
298
+ )
299
+ return 'warn';
300
+ return checks.every((check) => check.status === 'skip') ? 'skip' : 'pass';
301
+ }
302
+
303
+ function paint(c: ChalkInstance, status: Status, text: string): string {
304
+ return status === 'fail'
305
+ ? c.red(text)
306
+ : status === 'warn'
307
+ ? c.yellow(text)
308
+ : status === 'skip'
309
+ ? c.dim(text)
310
+ : c.green(text);
311
+ }
312
+
313
+ export function duration(ms: number): string {
314
+ const seconds = Math.round(ms / 1000);
315
+ if (seconds < 60) return `${seconds}s`;
316
+ return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
317
+ }
@@ -0,0 +1,27 @@
1
+ import type { Check, FleetNode, Runner } from '../types';
2
+ import { sshArgs, sshFailure } from '../ssh';
3
+
4
+ /** Initial enrollment must establish sudo once. Routine convergence never asks for passwords. */
5
+ export async function authorizePrivilege(
6
+ node: FleetNode,
7
+ _script: string | undefined,
8
+ run: Runner
9
+ ): Promise<Check[]> {
10
+ const existing = await run([...sshArgs(node), 'sudo -n true'], {
11
+ timeoutMs: 15_000,
12
+ });
13
+ if (existing.ok) return [];
14
+ if (existing.reason !== 'exit' || !/sudo:/i.test(existing.stderr))
15
+ return [sshFailure(existing)];
16
+ return [
17
+ {
18
+ id: 'bootstrap:privilege',
19
+ status: 'fail',
20
+ summary: 'SUDO_AUTHORIZATION_REQUIRED',
21
+ remedy: {
22
+ action:
23
+ 'Use the source-owned ansible/ubuntu/authorize-sudo.sh once during initial administrator enrollment. Runtime convergence requires sudo -n and never prompts for or stores a password.',
24
+ },
25
+ },
26
+ ];
27
+ }
@@ -0,0 +1,109 @@
1
+ import { Chalk } from 'chalk';
2
+ import ora, { type Ora } from 'ora';
3
+ import type { ManagerNode } from '../manager-sync';
4
+ import { blocking } from '../types';
5
+ import { duration } from './fleet-sync-output';
6
+
7
+ /** Live progress on stderr. A terminal gets one spinner with an elapsed timer
8
+ * and a persisted line per finished step; anything else (pipes, CI, agents)
9
+ * gets timestamped lines plus a heartbeat each minute, never color.
10
+ */
11
+ export interface SyncProgress {
12
+ step(node: string, phase: string): void;
13
+ finished(node: ManagerNode): void;
14
+ stop(): void;
15
+ }
16
+
17
+ export function syncProgress(
18
+ enabled: boolean,
19
+ stream: NodeJS.WriteStream = process.stderr
20
+ ): SyncProgress {
21
+ if (!enabled) return { step() {}, finished() {}, stop() {} };
22
+ const live =
23
+ Boolean(stream.isTTY) &&
24
+ process.env['NO_COLOR'] === undefined &&
25
+ process.env['TERM'] !== 'dumb' &&
26
+ !process.env['CI'];
27
+ return live ? spinnerProgress(stream) : lineProgress(stream);
28
+ }
29
+
30
+ function spinnerProgress(stream: NodeJS.WriteStream): SyncProgress {
31
+ const c = new Chalk({ level: 1 });
32
+ let spinner: Ora | null = null;
33
+ let label = '';
34
+ let started = 0;
35
+ let timer: ReturnType<typeof setInterval> | null = null;
36
+ const text = () => `${label} ${c.dim(duration(Date.now() - started))}`;
37
+ const settle = () => {
38
+ if (timer) clearInterval(timer);
39
+ timer = null;
40
+ if (spinner?.isSpinning)
41
+ spinner.stopAndPersist({
42
+ symbol: c.dim('›'),
43
+ text: c.dim(`${label} ${duration(Date.now() - started)}`),
44
+ });
45
+ spinner = null;
46
+ };
47
+ return {
48
+ step(node, phase) {
49
+ settle();
50
+ label = `${c.bold(node)} ${phase}`;
51
+ started = Date.now();
52
+ spinner = ora({ text: text(), stream, discardStdin: false }).start();
53
+ timer = setInterval(() => {
54
+ if (spinner) spinner.text = text();
55
+ }, 1000);
56
+ },
57
+ finished(node) {
58
+ settle();
59
+ const failing = node.checks.filter(blocking).length;
60
+ const warnings = node.checks.filter((c) => c.status === 'warn').length;
61
+ const detail = [
62
+ failing ? c.red(`${failing} failing`) : '',
63
+ warnings ? c.yellow(`${warnings} warnings`) : '',
64
+ ]
65
+ .filter(Boolean)
66
+ .join(c.dim(', '));
67
+ const symbol = failing
68
+ ? c.red('✖')
69
+ : warnings
70
+ ? c.yellow('▲')
71
+ : c.green('✔');
72
+ stream.write(
73
+ `${symbol} ${c.bold(node.node)} ${detail || c.green('ready')} ${c.dim(duration(node.durationMs))}\n`
74
+ );
75
+ },
76
+ stop: settle,
77
+ };
78
+ }
79
+
80
+ function lineProgress(stream: NodeJS.WriteStream): SyncProgress {
81
+ let timer: ReturnType<typeof setInterval> | null = null;
82
+ const stamp = () => new Date().toISOString().slice(11, 19);
83
+ const clear = () => {
84
+ if (timer) clearInterval(timer);
85
+ timer = null;
86
+ };
87
+ return {
88
+ step(node, phase) {
89
+ clear();
90
+ const started = Date.now();
91
+ stream.write(`${stamp()} ${node} ${phase}\n`);
92
+ timer = setInterval(
93
+ () =>
94
+ stream.write(
95
+ `${stamp()} ${node} still ${phase.toLowerCase()} (${duration(Date.now() - started)})\n`
96
+ ),
97
+ 60_000
98
+ );
99
+ },
100
+ finished(node) {
101
+ clear();
102
+ const failing = node.checks.filter(blocking).length;
103
+ stream.write(
104
+ `${stamp()} ${node.node} ${failing ? `FAIL ${failing} failing` : 'DONE'} (${duration(node.durationMs)})\n`
105
+ );
106
+ },
107
+ stop: clear,
108
+ };
109
+ }