@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,183 @@
1
+ import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
2
+ import { convergeManagerHub } from './manager-hub';
3
+ import { resolve } from 'node:path';
4
+ import { fleet } from '../fleet-config';
5
+ import { hubUrl, roleOf } from '../topology';
6
+ import { record } from '../config';
7
+ import { isRevision } from '../machine/state';
8
+ import { privateHub } from './beszel';
9
+ import { agentConnection, agentDisk } from './agent-evidence';
10
+ import { runLockedCommand as runCommand } from '../runner';
11
+ import type { Check, Runner } from '../types';
12
+
13
+ export async function applyAgent(
14
+ value: unknown,
15
+ root: string,
16
+ run: Runner,
17
+ options: { receipts?: string } = {}
18
+ ): Promise<Check[]> {
19
+ const request = validateRequest(value);
20
+ const definition = fleet;
21
+ await validateSource(request, root, run);
22
+ const role = roleOf(definition, request.node);
23
+ if (role === null) throw new Error('BESZEL_NOT_ENROLLED');
24
+ if (request.mode === 'hub') {
25
+ // The hub runs only on the manager; workers only ever run the agent.
26
+ if (role !== 'manager') throw new Error('BESZEL_NOT_HUB');
27
+ return convergeManagerHub(root, definition, run);
28
+ }
29
+ if (
30
+ typeof request.publicKey !== 'string' ||
31
+ typeof request.token !== 'string'
32
+ )
33
+ throw new Error('BESZEL_CREDENTIALS_REQUIRED');
34
+ // Agents connect only to the manager's private hub URL.
35
+ if (privateHub(request.hubUrl) !== privateHub(hubUrl(definition)))
36
+ throw new Error('BESZEL_HUB_MISMATCH');
37
+ return installAgent(request, root, run, options.receipts);
38
+ }
39
+
40
+ /** Workers (Linux) install a hardened systemd unit. The macOS manager's own
41
+ * agent is a per-user launch agent installed locally by fleet-sync
42
+ * (installManagerAgent), never through this remote entry point.
43
+ */
44
+ async function installAgent(
45
+ request: Request,
46
+ root: string,
47
+ run: Runner,
48
+ receipts: string | undefined
49
+ ): Promise<Check[]> {
50
+ // Measured before anything changes: a full disk is named, not guessed at.
51
+ const disk = await agentDisk(run);
52
+ if (disk.status !== 'pass') return [disk];
53
+ const monitoring = resolve(root, 'ansible/monitoring');
54
+ const result = await runAnsibleWithReceipt(
55
+ run,
56
+ 'agent.yml',
57
+ {
58
+ cwd: monitoring,
59
+ timeoutMs: 300_000,
60
+ input: JSON.stringify({
61
+ beszel_node: request.node,
62
+ beszel_hub_url: privateHub(request.hubUrl),
63
+ beszel_public_key: request.publicKey,
64
+ beszel_agent_token: request.token,
65
+ }),
66
+ },
67
+ {
68
+ name: 'beszel-agent',
69
+ sources: [monitoring],
70
+ verify: () => agentActive(run),
71
+ ...(receipts ? { directory: receipts } : {}),
72
+ }
73
+ );
74
+ if (!result.ok)
75
+ return [
76
+ disk,
77
+ {
78
+ id: 'beszel:installation',
79
+ status: 'fail',
80
+ summary: 'BESZEL_INSTALLATION_INCOMPLETE',
81
+ remedy: {
82
+ action:
83
+ 'Inspect sudo, signed artifact access, free disk and existing service ownership. Preserve the native agent DATA_DIR and rerun the manager.',
84
+ },
85
+ },
86
+ ];
87
+ const ready = await agentActive(run);
88
+ return [
89
+ disk,
90
+ {
91
+ id: 'beszel:service',
92
+ status: ready ? 'pass' : 'fail',
93
+ summary: ready
94
+ ? 'Native Beszel service is enabled and active; hub metrics remain a separate gate.'
95
+ : 'BESZEL_SERVICE_STOPPED',
96
+ },
97
+ // How the hub answered the agent, from the agent's side: a rejected token
98
+ // or key is told apart from a hub that is merely down.
99
+ ...(ready ? [await agentConnection(run)] : []),
100
+ ];
101
+ }
102
+ async function agentActive(run: Runner): Promise<boolean> {
103
+ return (
104
+ (await run(['systemctl', 'is-enabled', 'beszel-agent.service'])).ok &&
105
+ (await run(['systemctl', 'is-active', 'beszel-agent.service'])).ok
106
+ );
107
+ }
108
+ interface Request {
109
+ node: string;
110
+ revision: string;
111
+ hubUrl: string;
112
+ mode?: unknown;
113
+ publicKey?: unknown;
114
+ token?: unknown;
115
+ }
116
+ function validateRequest(value: unknown): Request {
117
+ if (
118
+ !record(value) ||
119
+ typeof value['node'] !== 'string' ||
120
+ !isRevision(value['revision']) ||
121
+ typeof value['hubUrl'] !== 'string'
122
+ )
123
+ throw new Error('BESZEL_INPUT_INVALID');
124
+ return value as unknown as Request;
125
+ }
126
+ async function validateSource(
127
+ request: Request,
128
+ root: string,
129
+ run: Runner
130
+ ): Promise<void> {
131
+ for (const [args, expected] of [
132
+ [['git', 'rev-parse', 'HEAD'], request.revision],
133
+ [['git', 'rev-parse', '--show-toplevel'], resolve(root)],
134
+ [['git', 'status', '--porcelain'], ''],
135
+ ] as const) {
136
+ const observed = await run([...args], { cwd: root });
137
+ if (!observed.ok || observed.stdout.trim() !== expected)
138
+ throw new Error('BESZEL_SOURCE_MISMATCH');
139
+ }
140
+ }
141
+ async function main(): Promise<Check[]> {
142
+ let raw = '';
143
+ for await (const chunk of process.stdin) {
144
+ raw += String(chunk);
145
+ if (raw.length > 16_384) throw new Error('BESZEL_INPUT_LIMIT');
146
+ }
147
+ return applyAgent(JSON.parse(raw), process.cwd(), runCommand);
148
+ }
149
+ /** Turns a known refusal into structured evidence; anything else is redacted. */
150
+ export function refusal(error: unknown): Check[] {
151
+ const code = error instanceof Error ? error.message : '';
152
+ const action = REFUSALS[code];
153
+ return [
154
+ action
155
+ ? {
156
+ id: 'beszel:operation',
157
+ status: 'fail',
158
+ summary: code,
159
+ remedy: { action },
160
+ }
161
+ : {
162
+ id: 'beszel:operation',
163
+ status: 'fail',
164
+ summary: 'BESZEL_OPERATION_INCOMPLETE',
165
+ },
166
+ ];
167
+ }
168
+ const REFUSALS: Record<string, string> = {
169
+ BESZEL_NOT_HUB:
170
+ 'The Beszel hub runs only on the manager. Workers run only the agent; converge the hub from the manager with fleet-sync.',
171
+ BESZEL_NOT_ENROLLED:
172
+ 'Declare this machine in fleet.json (the manager or a worker) before enrolling monitoring.',
173
+ BESZEL_SOURCE_MISMATCH:
174
+ 'The checkout is not the clean, expected revision. Rerun fleet-sync so the reviewed source is installed first.',
175
+ BESZEL_HUB_MISMATCH:
176
+ "Agents connect only to the manager's private hub URL from fleet.json. Rerun fleet-sync from an up-to-date checkout.",
177
+ BESZEL_CREDENTIALS_REQUIRED:
178
+ 'Agent enrollment credentials were not delivered. Rerun fleet-sync from the manager.',
179
+ };
180
+ if (import.meta.main) {
181
+ const checks = await main().catch(refusal);
182
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
183
+ }
@@ -0,0 +1,198 @@
1
+ import { fleet } from '../fleet-config';
2
+ import lock from '../../ansible/monitoring/lock.json';
3
+ import type { Check } from '../types';
4
+
5
+ export interface BeszelEvidence {
6
+ dashboardUrl: string;
7
+ checks: Check[];
8
+ receivedAt: string | null;
9
+ }
10
+ export type BeszelRequest = (path: string) => Promise<unknown>;
11
+ type Row = Record<string, unknown>;
12
+ function object(value: unknown): value is Row {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+ function items(value: unknown): Row[] {
16
+ if (
17
+ !object(value) ||
18
+ !Array.isArray(value['items']) ||
19
+ !value['items'].every(object)
20
+ )
21
+ throw new Error('BESZEL_PROTOCOL');
22
+ return value['items'];
23
+ }
24
+ function check(
25
+ id: string,
26
+ status: Check['status'],
27
+ summary: string,
28
+ action: string
29
+ ): Check {
30
+ return { id: `beszel:${id}`, status, summary, remedy: { action } };
31
+ }
32
+ /** Public dashboard hostname served through the Cloudflare
33
+ * tunnel `beszel`. Agents and the manager keep using the private hub URL.
34
+ */
35
+ export const publicHubUrl = (): string => fleet.services.monitoring.publicUrl;
36
+
37
+ export function privateHub(value: string): string {
38
+ const url = new URL(value);
39
+ if (
40
+ url.protocol !== 'https:' ||
41
+ !url.hostname.endsWith('.ts.net') ||
42
+ url.port !== '8443' ||
43
+ url.username ||
44
+ url.password ||
45
+ url.search ||
46
+ url.hash ||
47
+ url.pathname !== '/'
48
+ )
49
+ throw new Error('BESZEL_PRIVATE_URL');
50
+ return url.origin;
51
+ }
52
+
53
+ /** Read-only PocketBase endpoints, verified against the upstream Beszel
54
+ * release `ansible/monitoring/lock.json` pins.
55
+ * Never return raw records: they may contain tokens, hosts or user information.
56
+ */
57
+ export async function verifyBeszel(
58
+ hub: string,
59
+ node: string,
60
+ request: BeszelRequest,
61
+ now = Date.now()
62
+ ): Promise<BeszelEvidence> {
63
+ const dashboardUrl = privateHub(hub);
64
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(node)) throw new Error('BESZEL_NODE_ID');
65
+ const result: BeszelEvidence = { dashboardUrl, checks: [], receivedAt: null };
66
+ try {
67
+ const info = await request('/api/beszel/info');
68
+ if (!object(info) || info['v'] !== lock.version) {
69
+ result.checks.push(
70
+ check(
71
+ 'version',
72
+ 'fail',
73
+ 'BESZEL_VERSION_MISMATCH',
74
+ 'Apply the pinned hub release before using its versioned API contract.'
75
+ )
76
+ );
77
+ return result;
78
+ }
79
+ result.checks.push(
80
+ check(
81
+ 'version',
82
+ 'pass',
83
+ `Authenticated hub reports ${lock.version}.`,
84
+ 'None.'
85
+ )
86
+ );
87
+ const filter = encodeURIComponent(`name="${node}"`);
88
+ const records = items(
89
+ await request(
90
+ `/api/collections/systems/records?filter=${filter}&perPage=2`
91
+ )
92
+ );
93
+ if (records.length !== 1) {
94
+ result.checks.push(
95
+ check(
96
+ 'registration',
97
+ 'fail',
98
+ records.length ? 'BESZEL_DUPLICATE_NODE' : 'BESZEL_NODE_MISSING',
99
+ 'Review the canonical node in the hub. Preserve its fingerprint and history; do not create a replacement identity.'
100
+ )
101
+ );
102
+ return result;
103
+ }
104
+ await verifySample(records[0]!, result, request, now);
105
+ } catch {
106
+ result.checks.push(
107
+ check(
108
+ 'api',
109
+ 'fail',
110
+ 'BESZEL_API_UNVERIFIED',
111
+ 'Verify private reachability and a valid hub account with access to this node. Raw HTTP bodies are withheld.'
112
+ )
113
+ );
114
+ }
115
+ return result;
116
+ }
117
+
118
+ /** No redirects, browser, token minting, retries or response logging. */
119
+ export function beszelReader(
120
+ hub: string,
121
+ token: string,
122
+ fetcher: typeof fetch = fetch
123
+ ): BeszelRequest {
124
+ const origin = privateHub(hub);
125
+ return async (path) => {
126
+ if (!path.startsWith('/api/')) throw new Error('BESZEL_API_PATH');
127
+ const response = await fetcher(`${origin}${path}`, {
128
+ headers: { Authorization: token },
129
+ redirect: 'error',
130
+ signal: AbortSignal.timeout(10_000),
131
+ });
132
+ if (!response.ok) throw new Error('BESZEL_API_UNVERIFIED');
133
+ const text = await response.text();
134
+ if (text.length > 256_000) throw new Error('BESZEL_RESPONSE_LIMIT');
135
+ return JSON.parse(text) as unknown;
136
+ };
137
+ }
138
+
139
+ async function verifySample(
140
+ record: Row,
141
+ result: BeszelEvidence,
142
+ request: BeszelRequest,
143
+ now: number
144
+ ): Promise<void> {
145
+ const id = record['id'];
146
+ if (typeof id !== 'string' || !/^[a-z0-9]{15}$/.test(id))
147
+ throw new Error('BESZEL_PROTOCOL');
148
+ result.checks.push(
149
+ check(
150
+ 'registration',
151
+ 'pass',
152
+ 'Exactly one canonical node record is visible to this account.',
153
+ 'None.'
154
+ )
155
+ );
156
+ result.checks.push(
157
+ check(
158
+ 'connection',
159
+ record['status'] === 'up' ? 'pass' : 'fail',
160
+ record['status'] === 'up'
161
+ ? 'Hub reports node connected.'
162
+ : 'BESZEL_AGENT_DISCONNECTED',
163
+ 'Inspect agent service, private hub connectivity and node-specific token; preserve its DATA_DIR.'
164
+ )
165
+ );
166
+ await checkFreshness(id, result, request, now);
167
+ }
168
+ async function checkFreshness(
169
+ id: string,
170
+ result: BeszelEvidence,
171
+ request: BeszelRequest,
172
+ now: number
173
+ ): Promise<void> {
174
+ const stats = items(
175
+ await request(
176
+ `/api/collections/system_stats/records?filter=${encodeURIComponent(`system="${id}" && type="1m"`)}&sort=-created&perPage=1&fields=created`
177
+ )
178
+ );
179
+ const created = stats[0]?.['created'];
180
+ const timestamp = typeof created === 'string' ? Date.parse(created) : NaN;
181
+ const fresh =
182
+ Number.isFinite(timestamp) &&
183
+ now - timestamp <= 180_000 &&
184
+ now >= timestamp - 30_000;
185
+ result.receivedAt = Number.isFinite(timestamp)
186
+ ? new Date(timestamp).toISOString()
187
+ : null;
188
+ result.checks.push(
189
+ check(
190
+ 'freshness',
191
+ fresh ? 'pass' : 'fail',
192
+ fresh
193
+ ? `Hub received sample ${result.receivedAt}; within 180 seconds.`
194
+ : 'BESZEL_METRICS_STALE',
195
+ 'Check hub and node clocks, agent connection and disk capacity. Rerun after the next sampling interval.'
196
+ )
197
+ );
198
+ }
@@ -0,0 +1,111 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { record } from '../config';
3
+ import { privateHub } from './beszel';
4
+ import lock from '../../ansible/monitoring/lock.json';
5
+
6
+ export type HubRequest = (path: string, body?: object) => Promise<unknown>;
7
+ export interface AgentEnrollment {
8
+ systemId: string;
9
+ hubUrl: string;
10
+ publicKey: string;
11
+ token: string;
12
+ }
13
+ function rows(value: unknown): Record<string, unknown>[] {
14
+ if (
15
+ !record(value) ||
16
+ !Array.isArray(value['items']) ||
17
+ !value['items'].every(record)
18
+ )
19
+ throw new Error('BESZEL_PROTOCOL');
20
+ return value['items'];
21
+ }
22
+ function identifier(value: unknown): string {
23
+ if (typeof value !== 'string' || !/^[a-z0-9]{15}$/.test(value))
24
+ throw new Error('BESZEL_PROTOCOL');
25
+ return value;
26
+ }
27
+ /** Mutating API adapter: call only after fleet approval and under manager lock.
28
+ * Uses the same systems/fingerprints API as upstream Add System, never its DB.
29
+ * The returned token is secret material: pass only through private SSH stdin.
30
+ */
31
+ export async function enrollBeszel(
32
+ input: { node: string; hub: string; userId: string; host: string },
33
+ request: HubRequest
34
+ ): Promise<AgentEnrollment> {
35
+ const hubUrl = privateHub(input.hub);
36
+ if (
37
+ !/^[a-z][a-z0-9-]{0,62}$/.test(input.node) ||
38
+ !/^[a-z0-9.-]+\.ts\.net$/.test(input.host)
39
+ )
40
+ throw new Error('BESZEL_NODE_INVALID');
41
+ const info = await hubInfo(request);
42
+ const systemId = await ensureSystem(input, request);
43
+ const fingerprints = rows(
44
+ await request(
45
+ `/api/collections/fingerprints/records?filter=${encodeURIComponent(`system="${systemId}"`)}&perPage=2`
46
+ )
47
+ );
48
+ if (fingerprints.length > 1) throw new Error('BESZEL_DUPLICATE_FINGERPRINT');
49
+ let token = fingerprints[0]?.['token'];
50
+ if (fingerprints.length === 0) {
51
+ token = randomUUID();
52
+ await request('/api/collections/fingerprints/records', {
53
+ id: createHash('sha256')
54
+ .update(`personal:fingerprint:${systemId}`)
55
+ .digest('hex')
56
+ .slice(0, 15),
57
+ system: systemId,
58
+ token,
59
+ });
60
+ }
61
+ if (
62
+ typeof token !== 'string' ||
63
+ !/^[a-zA-Z0-9_-][a-zA-Z0-9_-]{15,63}$/.test(token)
64
+ )
65
+ throw new Error('BESZEL_TOKEN_INVALID');
66
+ return { systemId, hubUrl, publicKey: info['key'], token };
67
+ }
68
+ async function ensureSystem(
69
+ input: { node: string; userId: string; host: string },
70
+ request: HubRequest
71
+ ): Promise<string> {
72
+ const systems = rows(
73
+ await request(
74
+ `/api/collections/systems/records?filter=${encodeURIComponent(`name="${input.node}"`)}&perPage=2`
75
+ )
76
+ );
77
+ if (systems.length > 1) throw new Error('BESZEL_DUPLICATE_NODE');
78
+ if (systems[0]) {
79
+ if (systems[0]['host'] !== input.host)
80
+ throw new Error('BESZEL_HOST_CONFLICT');
81
+ return identifier(systems[0]['id']);
82
+ }
83
+ const id = createHash('sha256')
84
+ .update(`personal:system:${input.node}`)
85
+ .digest('hex')
86
+ .slice(0, 15);
87
+ const created = await request('/api/collections/systems/records', {
88
+ id,
89
+ name: input.node,
90
+ host: input.host,
91
+ port: '45876',
92
+ users: [identifier(input.userId)],
93
+ });
94
+ if (!record(created) || created['id'] !== id)
95
+ throw new Error('BESZEL_PROTOCOL');
96
+ return id;
97
+ }
98
+
99
+ async function hubInfo(
100
+ request: HubRequest
101
+ ): Promise<Record<string, unknown> & { key: string }> {
102
+ const info = await request('/api/beszel/info');
103
+ if (
104
+ !record(info) ||
105
+ info['v'] !== lock.version ||
106
+ typeof info['key'] !== 'string' ||
107
+ !info['key'].startsWith('ssh-ed25519 ')
108
+ )
109
+ throw new Error('BESZEL_VERSION_OR_KEY');
110
+ return info as Record<string, unknown> & { key: string };
111
+ }
@@ -0,0 +1,114 @@
1
+ import { privateHub } from './beszel';
2
+ import type { HubRequest } from './enroll';
3
+ import { record } from '../config';
4
+ import { fleet, secret } from '../fleet-config';
5
+
6
+ /** The normal (non-superuser) Beszel account the manager enrolls nodes with.
7
+ * Its password is the fleet's monitoring-account-password secret. A fresh hub on the
8
+ * manager creates the account itself on first start (hub-darwin.yml).
9
+ */
10
+ export const fleetIdentity = (): string =>
11
+ fleet.services.monitoring.managerAccount;
12
+
13
+ /** Read-only Beszel user the hub signs every visitor in as (AUTO_LOGIN), so the
14
+ * dashboard needs no login. Create it once in the hub (Settings > Users, role
15
+ * "Read-only"); it holds no secret and must never be an admin. If the account
16
+ * does not exist the hub simply shows its normal login page.
17
+ */
18
+ export const publicViewerIdentity = (): string =>
19
+ fleet.services.monitoring.viewerAccount;
20
+
21
+ /** Accept the plain password, or a legacy JSON identity/password pair. */
22
+ export function fleetCredentials(secret: string): {
23
+ identity: string;
24
+ password: string;
25
+ } {
26
+ const value = secret.trim();
27
+ if (!value) throw new Error('BESZEL_ACCOUNT_INVALID');
28
+ if (value.startsWith('{')) {
29
+ let parsed: unknown;
30
+ try {
31
+ parsed = JSON.parse(value);
32
+ } catch {
33
+ throw new Error('BESZEL_ACCOUNT_INVALID');
34
+ }
35
+ if (
36
+ !record(parsed) ||
37
+ typeof parsed['identity'] !== 'string' ||
38
+ typeof parsed['password'] !== 'string'
39
+ )
40
+ throw new Error('BESZEL_ACCOUNT_INVALID');
41
+ return { identity: parsed['identity'], password: parsed['password'] };
42
+ }
43
+ return { identity: fleetIdentity(), password: value };
44
+ }
45
+
46
+ /** The fleet login from the manager's Vault session, or null when absent.
47
+ * Used to create the account on a hub's first start; never printed.
48
+ */
49
+ export async function fleetAccount(): Promise<{
50
+ identity: string;
51
+ password: string;
52
+ } | null> {
53
+ const password = await secret('monitoring-account-password');
54
+ try {
55
+ return password ? fleetCredentials(password) : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Reads the existing account password only. This never creates credentials. The normal Beszel account must have fleet-node access.
62
+ */
63
+ export async function hubAccess(
64
+ hub: string
65
+ ): Promise<{ userId: string; request: HubRequest }> {
66
+ const url = privateHub(hub);
67
+ const password = await secret('monitoring-account-password');
68
+ if (!password) throw new Error('BESZEL_ACCOUNT_REQUIRED');
69
+ const credentials = fleetCredentials(password);
70
+ const login = await call(
71
+ url,
72
+ '/api/collections/users/auth-with-password',
73
+ '',
74
+ credentials
75
+ );
76
+ if (
77
+ !record(login) ||
78
+ typeof login['token'] !== 'string' ||
79
+ !record(login['record']) ||
80
+ typeof login['record']['id'] !== 'string'
81
+ )
82
+ throw new Error('BESZEL_AUTH_REQUIRED');
83
+ const token = login['token'];
84
+ return {
85
+ userId: login['record']['id'],
86
+ request: (path, body) => call(url, path, token, body),
87
+ };
88
+ }
89
+ async function call(
90
+ url: string,
91
+ path: string,
92
+ token: string,
93
+ body?: object
94
+ ): Promise<unknown> {
95
+ if (!path.startsWith('/api/')) throw new Error('BESZEL_API_PATH');
96
+ const response = await fetch(`${url}${path}`, {
97
+ method: body ? 'POST' : 'GET',
98
+ headers: { Authorization: token, 'Content-Type': 'application/json' },
99
+ ...(body ? { body: JSON.stringify(body) } : {}),
100
+ redirect: 'error',
101
+ signal: AbortSignal.timeout(10_000),
102
+ });
103
+ if (!response.ok) throw new Error(failure(response.status, path));
104
+ return response.json() as Promise<unknown>;
105
+ }
106
+
107
+ /** PocketBase answers a rejected password login with 400, not 401. */
108
+ export function failure(status: number, path: string): string {
109
+ if (status === 400 && path.endsWith('/auth-with-password'))
110
+ return 'BESZEL_ACCOUNT_REQUIRED';
111
+ return status === 401 || status === 403
112
+ ? 'BESZEL_AUTH_REQUIRED'
113
+ : 'BESZEL_API_UNAVAILABLE';
114
+ }