@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,215 @@
1
+ # Beszel owner and recovery
2
+
3
+ The [README](../../README.md) and the [manager guide](../../docs/ubuntu-sync.md)
4
+ are the normal entry points: `devfleet fleet-sync`, run on the manager. This
5
+ directory holds the pinned Compose hub, the native agent unit and their
6
+ playbooks. `src/monitoring/` enrolls machines and measures registration,
7
+ connection, freshness and backups. `lock.json` pins the Beszel release, the
8
+ agent archives and the agent executables by digest. Never run an unpinned
9
+ upstream installer alongside these services.
10
+
11
+ ## Manager and workers
12
+
13
+ The fleet has exactly two roles (`fleet.json`):
14
+
15
+ - **Manager** — the human's macOS machine. It runs fleet-sync locally and hosts the
16
+ singleton hub (`src/monitoring/manager-hub.ts`, `convergeManagerHub`).
17
+ dev-fleet keeps it awake on AC power. Nothing is installed on it over SSH.
18
+ - **Workers** — the stateless Linux boxes. They only run the native agent
19
+ (`agent.yml`, a hardened systemd unit) and connect to the manager's private hub
20
+ URL. A worker refuses hub mode (`BESZEL_NOT_HUB`: the hub runs only on the
21
+ manager) and an agent refuses any other hub URL (`BESZEL_HUB_MISMATCH`).
22
+
23
+ The manager is always a monitored system on its own hub. After the hub converges,
24
+ fleet-sync enrolls the manager like a worker and installs its agent locally with
25
+ `agent-darwin.yml`: the same pinned binary (`lock.json` carries the darwin
26
+ checksums) as a per-user launchd agent with `KeepAlive`
27
+ (`~/Library/LaunchAgents/dev.devfleet.beszel-agent.plist`, state under
28
+ `~/Library/Application Support/personal-beszel-agent`). It needs no sudo, so
29
+ fleet-sync stays unattended; it is less sandboxed than the Linux unit. It refuses
30
+ to run beside a leftover root LaunchDaemon with the same label
31
+ (`BESZEL_LEGACY_DAEMON`).
32
+
33
+ ## Hub on the manager
34
+
35
+ `hub-darwin.yml` runs as the signed-in user (no sudo) under Docker Desktop, which
36
+ must be running (`BESZEL_DOCKER_DESKTOP_REQUIRED`); enable _Start Docker Desktop
37
+ when you sign in_. It refuses to replace a different existing image
38
+ (`BESZEL_HUB_UPGRADE_PENDING`). Paths are per-user and `0700`:
39
+
40
+ - data: `~/Library/Application Support/personal-beszel` (bind-mounted as
41
+ `/beszel_data`; it is in the human's normal home backups, but use the stock
42
+ backups below for consistent copies);
43
+ - connector token: `~/Library/Application Support/personal-beszel-tunnel/token`
44
+ (`0600`).
45
+
46
+ The dashboard is `https://<manager-MagicDNS-name>:8443`
47
+ (`hubUrl(definition)`), used by agents and checks. CLI output only ever prints
48
+ the stable public dashboard, `https://beszel.example.com`; tailnet addresses
49
+ are never printed. The Compose port is
50
+ loopback-only; Tailscale Serve (`tailscale serve --bg --https=8443
51
+ http://127.0.0.1:8090`, using `tailscale` on PATH or the app's
52
+ `/Applications/Tailscale.app/Contents/MacOS/Tailscale`) provides private HTTPS.
53
+ No Funnel or inbound listener is configured. Convergence passes `beszel:hub` only
54
+ after `/api/health` answers both locally and at the private URL. Before enrolling
55
+ agents, fleet-sync requires that URL to answer; otherwise workers report
56
+ `BESZEL_HUB_UNREACHABLE` (keep the manager awake and Docker Desktop running).
57
+
58
+ The same hub is also served at `https://beszel.example.com` through an
59
+ outbound-only Cloudflare Tunnel: the inventory's
60
+ `cloudflare.additional_tunnels.beszel` entry makes OpenTofu own the tunnel, its
61
+ proxied DNS record and the isolated `secret/beszel/tunnel` Vault document. The
62
+ hub converge reads that document with the manager's own Vault session (the
63
+ human's session; never printed or placed in argv), writes it to the per-user
64
+ token file and starts the pinned `cloudflared` Compose profile. Until the
65
+ credential is readable the step warns (`BESZEL_TUNNEL_CREDENTIAL_UNAVAILABLE`)
66
+ and the private dashboard is unaffected. Agents and the manager keep using the
67
+ private URL; `fleet-status --workers` reports the public hostname as a
68
+ warning-level `beszel:public-endpoint` check.
69
+
70
+ The dashboard needs no login: the hub runs with `AUTO_LOGIN` set to the
71
+ read-only user `public-viewer@example.com`, so every visitor (public or
72
+ tailnet) is signed in as that account and cannot change systems, alerts or
73
+ settings. Each hub converge (`fleet-sync`) creates that user through the hub's
74
+ loopback API using the fleet admin login from Vault (random unused password,
75
+ never printed), re-reads it to prove the role, and demotes it to read-only if it
76
+ ever had another role (`beszel:public-viewer`). If the admin login is
77
+ unavailable the step warns and the hub shows its normal login page; create the
78
+ user by hand (Settings > Users, role **Read-only**) as a fallback. Never point
79
+ `AUTO_LOGIN` at an admin account. The fleet admin login is still needed to change anything.
80
+
81
+ ## The agent on a worker
82
+
83
+ Native agents use outbound authenticated WebSockets, per-node tokens and
84
+ persistent identity. They do not need inbound SSH monitoring ports, Docker socket
85
+ access or journals.
86
+
87
+ - **Unprivileged.** The unit runs as the `beszel` system account with no other
88
+ group, no capability, no journal and no Docker socket
89
+ (`features/monitoring/agent-privileges.feature`).
90
+ `SKIP_SYSTEMD_LOGS=true` deliberately excludes potentially sensitive service
91
+ logs. System services may be visible; user T3 service information is not
92
+ certified by that fact. Sensors, SMART, Docker stats and user-service metrics
93
+ stay unavailable until their specific permissions are reviewed: nothing grants
94
+ them by default.
95
+ - **Repeatable.** `agent.yml` compares the installed executable with the digest
96
+ of the pinned build (`agentBinaries` in `lock.json`). An unchanged agent is left
97
+ running; one an interrupted upgrade or an edit left behind is removed,
98
+ extracted again from the checksummed archive and restarted once. Edited unit
99
+ or connection settings are restored the same way.
100
+ - **Boots without a login.** It is a system unit (`WantedBy=multi-user.target`),
101
+ so it starts at boot with nobody signed in.
102
+ - **Measured from the worker's side** (`src/monitoring/agent-evidence.ts`):
103
+
104
+ | Check | Summary | Meaning |
105
+ | ----------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------- |
106
+ | `beszel:disk` | `BESZEL_DISK_FULL`, `BESZEL_DISK_UNVERIFIED` | Less than 256 MiB free where the agent installs; nothing was changed |
107
+ | `beszel:service` | `BESZEL_SERVICE_STOPPED` | The unit is not enabled and active |
108
+ | `beszel:hub-connection` | `BESZEL_AGENT_TOKEN_REJECTED` | The hub answered the agent's token with 401 |
109
+ | | `BESZEL_AGENT_KEY_REJECTED` | The hub did not prove the key the agent was given |
110
+ | | `BESZEL_HUB_UNAVAILABLE_TO_AGENT` (warn) | The private route answered 502–504: the hub behind it is down |
111
+ | | `BESZEL_AGENT_CONNECTION_PENDING`, `…_LOG_UNAVAILABLE` (skip) | No outcome yet, or the log could not be read |
112
+
113
+ The outcome is the newest of a few fixed texts the pinned agent logs, taken
114
+ from its source; nothing of the log is reported. The hub's own view
115
+ (registration, `BESZEL_AGENT_DISCONNECTED`, `BESZEL_METRICS_STALE`) is
116
+ measured separately by `fleet-status --workers`.
117
+
118
+ Accounts need no manual step. On a hub's first start (empty data directory),
119
+ `hub-darwin.yml` passes Beszel's `USER_EMAIL`/`USER_PASSWORD` once, so the hub
120
+ creates `fleet-manager@example.com` as a verified user and as its superuser,
121
+ with the password OpenTofu generates as `BESZEL_FLEET_AUTH` in
122
+ `secret/personal/prd`. Later runs never pass it, and existing data is never
123
+ changed. Sign in to the dashboard with that account.
124
+ The hub runs with `SHARE_ALL_SYSTEMS=true`, so systems that account registers are
125
+ visible to every hub login.
126
+ A legacy JSON `identity`/`password` value is still accepted. The manager's existing
127
+ Vault credential must renew unattended. Superuser credentials are not distributed
128
+ to nodes or needed for enrollment. Revocation produces action-required results.
129
+
130
+ ## Settings kept only in the hub's UI
131
+
132
+ Beszel keeps network monitors, alert rules and backup settings in its own data,
133
+ set in its UI. dev-fleet does not write them: there is no reconciler and no
134
+ endpoint beyond the stock API (`features/monitoring/stock-api.feature`). They
135
+ are preserved by the hub's backups. Set them once, as the fleet account:
136
+
137
+ - **Network monitors.** Add them per system from that system's page. Probe from
138
+ a few chosen origins (for example the manager to each worker) rather than an
139
+ all-to-all mesh, so a failed check names a path you can reason about.
140
+ - **Alerts.** Prefer sustained thresholds over instant ones: CPU, memory and
141
+ disk above a level for several minutes, disk capacity, a system going offline,
142
+ and temperatures where the sensor is actually reported. Do not add a
143
+ notification destination until one you are allowed to use exists.
144
+ - **Service alerts are not promised.** Alerting on a failed systemd service has
145
+ not been verified against the pinned release; the agent watches only the
146
+ services in `SERVICE_PATTERNS`, and none of them is relied on for alerting.
147
+
148
+ ## Stock backup and restore
149
+
150
+ 1. Open the hub's private PocketBase administration route `/_/` and authenticate
151
+ as its superuser. Keep that credential separate from fleet enrollment access.
152
+ 2. Open **Settings → Backups**. Create and download a backup before maintenance.
153
+ Set a schedule, how many to keep, and S3-compatible storage there, using
154
+ storage you already approved. Do not provision a bucket or reuse unrelated
155
+ credentials to bypass access gates.
156
+ 3. Protect downloaded archives as secrets: they contain registrations, tokens,
157
+ account data, alert configuration and history. Use the existing approved
158
+ encrypted backup destination; a backup on the same disk is not disaster recovery.
159
+ 4. Restore the archive into an isolated instance of the same pinned release with
160
+ outbound agent connections blocked. Use the stock upload/restore action, never
161
+ database edits. Restoring restarts the hub. Verify original node IDs, fingerprints,
162
+ history and settings before selecting a replacement host. Fence the old hub
163
+ before production recovery; never operate two copies of its identity.
164
+ 5. Run the manager command and require new metric timestamps after recovery.
165
+ Old historical samples do not prove agent connectivity.
166
+
167
+ Every hub converge in `fleet-sync` reads these settings back as the hub's
168
+ superuser (`GET /api/settings`, `GET /api/backups`; `src/monitoring/hub-backup.ts`)
169
+ and reports `beszel:backup`. It only warns, and never names the storage:
170
+
171
+ | Summary | Meaning |
172
+ | -------------------------------- | -------------------------------------------------------- |
173
+ | `BESZEL_BACKUP_UNSCHEDULED` | No backup schedule is set |
174
+ | `BESZEL_BACKUP_OFFHOST_REQUIRED` | Backups stay on the manager's disk |
175
+ | `BESZEL_BACKUP_STALE` | A schedule is set but no backup is newer than 48 hours |
176
+ | `BESZEL_BACKUP_UNVERIFIED` | The settings could not be read (no fleet login, refused) |
177
+
178
+ ## Moving the hub to the manager
179
+
180
+ The hub previously ran on worker `worker-one` (`/var/lib/personal-beszel`,
181
+ Compose project `personal-beszel`). Move it once, as an operator, in this order;
182
+ never run two copies of its identity or two tunnel connectors:
183
+
184
+ 1. On the old hub host, create and download a stock backup (**Settings → Backups**) and keep
185
+ it as a secret.
186
+ 2. Fence the old hub on that host: `sudo docker compose -p personal-beszel down`
187
+ (stops the hub and its `cloudflared` connector; keep `/var/lib/personal-beszel`
188
+ untouched until the move is verified), then
189
+ `sudo tailscale serve --https=8443 off`.
190
+ 3. On the manager, with Docker Desktop running, run fleet-sync. It starts a fresh
191
+ hub, which creates the fleet account itself, and enrolls every worker. To keep
192
+ history instead, restore the backup afterwards through the stock **Backups**
193
+ upload/restore action as the superuser.
194
+ 4. Confirm fresh samples from every worker in the dashboard.
195
+ 5. Only after verification, remove the old host's hub data and its `beszel-hub-read`
196
+ Vault grant (OpenTofu).
197
+
198
+ `tests/upstream.py` exercises real pinned binaries with disposable loopback data:
199
+ authenticated registration, fresh metrics, restart identity preservation, no
200
+ duplicate record, the supported PocketBase backup/restore API, and the recovery
201
+ drill above: a downloaded backup restored into a second, isolated hub after the
202
+ first is fenced, keeping node IDs, fingerprints, history and settings. It does
203
+ not contact live nodes, prove Tailscale routing, verify systemd reboot, or use an
204
+ off-host storage service. Those remain operational checks:
205
+
206
+ - From a second tailnet machine, `devfleet fleet-status --workers` reports
207
+ `beszel:registration`, `beszel:connection` and `beszel:freshness` passing for
208
+ every worker.
209
+ - `beszel:backup` passes on the manager once off-host storage is configured.
210
+ - After a worker reboots, the same checks pass again without anyone signing in.
211
+
212
+ References: [agent](https://beszel.dev/guide/agent-installation),
213
+ [systemd](https://beszel.dev/guide/systemd),
214
+ [network monitors](https://beszel.dev/guide/network-monitors),
215
+ [backup API](https://pocketbase.io/docs/api-backups/).
@@ -0,0 +1,198 @@
1
+ ---
2
+ # The manager's own Beszel agent, so the manager is always a monitored system
3
+ # on its own hub. It runs as the signed-in user (no become, like hub-darwin.yml)
4
+ # as an always-on per-user launchd agent: fleet-sync stays unattended and never
5
+ # needs the manager's sudo password. Credentials arrive through private
6
+ # variables, never argv.
7
+ - name: Converge the manager's native Beszel agent as a per-user launchd agent
8
+ hosts: localhost
9
+ connection: local
10
+ become: false
11
+ gather_facts: true
12
+ vars_files:
13
+ - ../macos/policy.json
14
+ vars:
15
+ beszel_lock: "{{ lookup('ansible.builtin.file', 'lock.json') | from_json }}"
16
+ beszel_arch: "{{ {'arm64': 'darwin-arm64', 'x86_64': 'darwin-amd64'}.get(ansible_facts.architecture, 'unsupported') }}"
17
+ beszel_home: '{{ ansible_facts.env.HOME }}'
18
+ beszel_base: '{{ beszel_home }}/Library/Application Support/personal-beszel-agent'
19
+ beszel_root: '{{ beszel_base }}/{{ beszel_lock.version }}'
20
+ beszel_secrets: '{{ beszel_base }}/credentials'
21
+ beszel_data: '{{ beszel_base }}/data'
22
+ beszel_plist: '{{ beszel_home }}/Library/LaunchAgents/{{ beszelDaemon }}.plist'
23
+ beszel_domain: 'gui/{{ ansible_facts.user_uid }}'
24
+ tasks:
25
+ - name: Require supported platform and private hub policy
26
+ ansible.builtin.assert:
27
+ that:
28
+ - ansible_facts.os_family == 'Darwin'
29
+ - beszel_arch in beszel_lock.agents
30
+ - beszel_node is match('^[a-z][a-z0-9-]{0,62}$')
31
+ - beszel_hub_url is match('^https://[a-z0-9.-]+\.ts\.net:8443$')
32
+ - name: Inspect a legacy system-wide daemon with the same label
33
+ ansible.builtin.stat:
34
+ path: '/Library/LaunchDaemons/{{ beszelDaemon }}.plist'
35
+ register: legacy_daemon
36
+ - name: Refuse to run two agents for one system
37
+ ansible.builtin.assert:
38
+ that: not legacy_daemon.stat.exists
39
+ fail_msg: BESZEL_LEGACY_DAEMON; remove the root LaunchDaemon once (sudo launchctl bootout system/{{ beszelDaemon }}) before the per-user agent replaces it.
40
+ - name: Inspect existing agent ownership
41
+ ansible.builtin.stat:
42
+ path: '{{ beszel_plist }}'
43
+ register: existing_plist
44
+ - name: Read existing agent ownership marker
45
+ ansible.builtin.slurp:
46
+ src: '{{ beszel_plist }}'
47
+ register: existing_agent
48
+ when: existing_plist.stat.exists
49
+ # The marker's package path is informational: ownership must survive the
50
+ # owning directory being moved.
51
+ - name: Require explicit adoption of any previously unmanaged agent
52
+ ansible.builtin.assert:
53
+ that:
54
+ - "not existing_plist.stat.exists or 'Managed by personal Fleet:' in (existing_agent.content | default('') | b64decode)"
55
+ fail_msg: Unmanaged Beszel launch agent exists. Review a source-controlled migration before replacing it.
56
+ - name: Create private configuration, version and state directories
57
+ ansible.builtin.file:
58
+ path: '{{ item }}'
59
+ state: directory
60
+ mode: '0700'
61
+ loop:
62
+ - '{{ beszel_base }}'
63
+ - '{{ beszel_root }}'
64
+ - '{{ beszel_secrets }}'
65
+ - '{{ beszel_data }}'
66
+ - '{{ beszel_home }}/Library/LaunchAgents'
67
+ - name: Deliver node-specific credentials through private variables, never command arguments
68
+ ansible.builtin.copy:
69
+ dest: '{{ beszel_secrets }}/{{ item.name }}'
70
+ content: '{{ item.value }}'
71
+ mode: '0600'
72
+ loop:
73
+ - { name: hub-key, value: '{{ beszel_public_key }}' }
74
+ - { name: agent-token, value: '{{ beszel_agent_token }}' }
75
+ no_log: true
76
+ notify: Restart agent
77
+ - name: Download pinned native artifact with checksum verification
78
+ ansible.builtin.get_url:
79
+ url: 'https://github.com/henrygd/beszel/releases/download/v{{ beszel_lock.version }}/beszel-agent_{{ beszel_arch | replace("-", "_") }}.tar.gz'
80
+ dest: '{{ beszel_root }}/agent.tar.gz'
81
+ checksum: 'sha256:{{ beszel_lock.agents[beszel_arch] }}'
82
+ mode: '0644'
83
+ timeout: 60
84
+ register: agent_archive
85
+ - name: Inspect the extracted executable
86
+ ansible.builtin.stat:
87
+ path: '{{ beszel_root }}/beszel-agent'
88
+ register: agent_binary
89
+ # ansible.builtin.unarchive requires GNU tar; macOS ships BSD tar, which
90
+ # extracts this checksum-verified archive natively.
91
+ - name: Extract the pinned executable when new or missing
92
+ ansible.builtin.command:
93
+ argv:
94
+ [
95
+ /usr/bin/tar,
96
+ -xzf,
97
+ '{{ beszel_root }}/agent.tar.gz',
98
+ -C,
99
+ '{{ beszel_root }}',
100
+ beszel-agent,
101
+ ]
102
+ when: agent_archive.changed or not agent_binary.stat.exists or not agent_binary.stat.executable
103
+ changed_when: true
104
+ notify: Restart agent
105
+ - name: Install the source-owned always-on launch agent
106
+ ansible.builtin.copy:
107
+ dest: '{{ beszel_plist }}'
108
+ mode: '0644'
109
+ backup: true
110
+ content: |
111
+ <?xml version="1.0" encoding="UTF-8"?>
112
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113
+ <!-- Managed by personal Fleet: ansible/monitoring -->
114
+ <plist version="1.0"><dict>
115
+ <key>Label</key><string>{{ beszelDaemon }}</string>
116
+ <key>ProgramArguments</key><array>
117
+ <string>{{ beszel_root }}/beszel-agent</string>
118
+ </array>
119
+ <key>EnvironmentVariables</key><dict>
120
+ <key>HUB_URL</key><string>{{ beszel_hub_url }}</string>
121
+ <key>SYSTEM_NAME</key><string>{{ beszel_node }}</string>
122
+ <key>DATA_DIR</key><string>{{ beszel_data }}</string>
123
+ <key>KEY_FILE</key><string>{{ beszel_secrets }}/hub-key</string>
124
+ <key>TOKEN_FILE</key><string>{{ beszel_secrets }}/agent-token</string>
125
+ <key>DISABLE_SSH</key><string>true</string>
126
+ </dict>
127
+ <key>RunAtLoad</key><true/>
128
+ <key>KeepAlive</key><true/>
129
+ <key>ProcessType</key><string>Background</string>
130
+ <key>ThrottleInterval</key><integer>20</integer>
131
+ <key>Umask</key><integer>63</integer>
132
+ <key>StandardOutPath</key><string>{{ beszel_data }}/agent.log</string>
133
+ <key>StandardErrorPath</key><string>{{ beszel_data }}/agent.log</string>
134
+ </dict></plist>
135
+ notify: Reload agent definition
136
+ - name: Inspect whether the agent is loaded
137
+ ansible.builtin.command:
138
+ argv: [/bin/launchctl, print, '{{ beszel_domain }}/{{ beszelDaemon }}']
139
+ changed_when: false
140
+ failed_when: false
141
+ register: agent_loaded
142
+ - name: Load the agent at login and now
143
+ ansible.builtin.command:
144
+ argv:
145
+ [
146
+ /bin/launchctl,
147
+ bootstrap,
148
+ '{{ beszel_domain }}',
149
+ '{{ beszel_plist }}',
150
+ ]
151
+ when: agent_loaded.rc != 0
152
+ changed_when: true
153
+ - name: Complete any required agent restart before verification
154
+ ansible.builtin.meta: flush_handlers
155
+ - name: Independently check the agent postcondition
156
+ ansible.builtin.command:
157
+ argv: [/bin/launchctl, print, '{{ beszel_domain }}/{{ beszelDaemon }}']
158
+ changed_when: false
159
+ register: agent_state
160
+ retries: 5
161
+ delay: 2
162
+ until: "'state = running' in agent_state.stdout"
163
+ handlers:
164
+ # launchd caches a loaded definition: a changed plist needs a reload.
165
+ - name: Unload the stale agent definition
166
+ ansible.builtin.command:
167
+ argv:
168
+ [/bin/launchctl, bootout, '{{ beszel_domain }}/{{ beszelDaemon }}']
169
+ listen: Reload agent definition
170
+ failed_when: false
171
+ changed_when: true
172
+ - name: Load the current agent definition
173
+ ansible.builtin.command:
174
+ argv:
175
+ [
176
+ /bin/launchctl,
177
+ bootstrap,
178
+ '{{ beszel_domain }}',
179
+ '{{ beszel_plist }}',
180
+ ]
181
+ listen: Reload agent definition
182
+ register: agent_reload
183
+ # bootout completes asynchronously; retry until the label is free.
184
+ until: agent_reload.rc == 0
185
+ retries: 5
186
+ delay: 2
187
+ changed_when: true
188
+ - name: Restart agent
189
+ ansible.builtin.command:
190
+ argv:
191
+ [
192
+ /bin/launchctl,
193
+ kickstart,
194
+ -k,
195
+ '{{ beszel_domain }}/{{ beszelDaemon }}',
196
+ ]
197
+ failed_when: false
198
+ changed_when: true
@@ -0,0 +1,172 @@
1
+ ---
2
+ - name: Converge the native Beszel agent without replacing runtime identity
3
+ hosts: localhost
4
+ connection: local
5
+ become: true
6
+ gather_facts: true
7
+ vars:
8
+ beszel_lock: "{{ lookup('ansible.builtin.file', 'lock.json') | from_json }}"
9
+ beszel_arch: "{{ {'x86_64': 'amd64', 'aarch64': 'arm64'}.get(ansible_facts.architecture, 'unsupported') }}"
10
+ tasks:
11
+ - name: Require supported platform and private hub policy
12
+ ansible.builtin.assert:
13
+ that:
14
+ - ansible_facts.distribution == 'Ubuntu'
15
+ - beszel_arch in beszel_lock.agents
16
+ - beszel_arch in beszel_lock.agentBinaries
17
+ - beszel_node is match('^[a-z][a-z0-9-]{0,62}$')
18
+ - beszel_hub_url is match('^https://[a-z0-9.-]+\.ts\.net:8443$')
19
+ - name: Inspect existing native service ownership
20
+ ansible.builtin.stat:
21
+ path: /etc/systemd/system/beszel-agent.service
22
+ checksum_algorithm: sha256
23
+ register: existing_service
24
+ - name: Read existing service ownership marker
25
+ ansible.builtin.slurp:
26
+ src: /etc/systemd/system/beszel-agent.service
27
+ register: existing_unit
28
+ when: existing_service.stat.exists
29
+ - name: Require explicit adoption of any previously unmanaged service
30
+ ansible.builtin.assert:
31
+ that:
32
+ - "not existing_service.stat.exists or (existing_unit.content | default('') | b64decode).startswith('# Managed by personal Fleet:')"
33
+ fail_msg: Unmanaged Beszel unit exists. Review a source-controlled migration before replacing it.
34
+ - name: Create unprivileged service account
35
+ ansible.builtin.user:
36
+ name: beszel
37
+ system: true
38
+ create_home: false
39
+ shell: /usr/sbin/nologin
40
+ - name: Create restricted configuration and version directories
41
+ ansible.builtin.file:
42
+ path: '{{ item.path }}'
43
+ state: directory
44
+ owner: root
45
+ group: '{{ item.group }}'
46
+ mode: '{{ item.mode }}'
47
+ loop:
48
+ - { path: /etc/personal-beszel, group: beszel, mode: '0750' }
49
+ - {
50
+ path: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}',
51
+ group: root,
52
+ mode: '0755',
53
+ }
54
+ - name: Deliver node-specific token through private stdin, never command arguments
55
+ ansible.builtin.copy:
56
+ dest: '/etc/personal-beszel/{{ item.name }}'
57
+ content: '{{ item.value }}'
58
+ owner: root
59
+ group: beszel
60
+ mode: '0640'
61
+ loop:
62
+ - { name: hub-key, value: '{{ beszel_public_key }}' }
63
+ - { name: agent-token, value: '{{ beszel_agent_token }}' }
64
+ no_log: true
65
+ notify: Restart agent
66
+ - name: Require credentials delivered through the approved secret owner
67
+ ansible.builtin.stat:
68
+ path: '/etc/personal-beszel/{{ item }}'
69
+ loop: [hub-key, agent-token]
70
+ register: credentials
71
+ no_log: true
72
+ - name: Fail closed without node-specific credentials
73
+ ansible.builtin.assert:
74
+ that: item.stat.exists and item.stat.isreg and item.stat.size > 0
75
+ fail_msg: Beszel credentials missing; resume enrollment through Fleet.
76
+ loop: '{{ credentials.results }}'
77
+ no_log: true
78
+ - name: Restrict credential permissions
79
+ ansible.builtin.file:
80
+ path: '/etc/personal-beszel/{{ item }}'
81
+ owner: root
82
+ group: beszel
83
+ mode: '0640'
84
+ loop: [hub-key, agent-token]
85
+ - name: Download pinned native artifact with signature-preserving HTTPS and checksum
86
+ ansible.builtin.get_url:
87
+ url: 'https://github.com/henrygd/beszel/releases/download/v{{ beszel_lock.version }}/beszel-agent_linux_{{ beszel_arch }}.tar.gz'
88
+ dest: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/agent.tar.gz'
89
+ checksum: 'sha256:{{ beszel_lock.agents[beszel_arch] }}'
90
+ mode: '0644'
91
+ timeout: 60
92
+ # The executable is compared with the digest of the pinned build, so an
93
+ # unchanged agent is left alone (no restart) and one that an interrupted
94
+ # upgrade or an edit left behind is replaced.
95
+ - name: Inspect the installed executable
96
+ ansible.builtin.stat:
97
+ path: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/beszel-agent'
98
+ checksum_algorithm: sha256
99
+ follow: false
100
+ register: agent_binary
101
+ - name: Remove an executable that is not the pinned build
102
+ ansible.builtin.file:
103
+ path: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/beszel-agent'
104
+ state: absent
105
+ when: >-
106
+ agent_binary.stat.exists and (not agent_binary.stat.isreg
107
+ or agent_binary.stat.checksum != beszel_lock.agentBinaries[beszel_arch])
108
+ - name: Extract the pinned executable when it is missing
109
+ ansible.builtin.unarchive:
110
+ src: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/agent.tar.gz'
111
+ dest: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}'
112
+ remote_src: true
113
+ include: [beszel-agent]
114
+ creates: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/beszel-agent'
115
+ notify: Restart agent
116
+ - name: Inspect the extracted executable
117
+ ansible.builtin.stat:
118
+ path: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/beszel-agent'
119
+ checksum_algorithm: sha256
120
+ follow: false
121
+ register: agent_extracted
122
+ - name: Require the pinned build before the service may start it
123
+ ansible.builtin.assert:
124
+ that:
125
+ - agent_extracted.stat.exists
126
+ - agent_extracted.stat.isreg
127
+ - agent_extracted.stat.checksum == beszel_lock.agentBinaries[beszel_arch]
128
+ fail_msg: The extracted Beszel agent does not match the pinned build.
129
+ - name: Keep the executable owned by root and not writable by the agent
130
+ ansible.builtin.file:
131
+ path: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/beszel-agent'
132
+ owner: root
133
+ group: root
134
+ mode: '0755'
135
+ - name: Install nonsecret node connection settings
136
+ ansible.builtin.copy:
137
+ dest: /etc/personal-beszel/agent.env
138
+ content: |
139
+ HUB_URL={{ beszel_hub_url }}
140
+ SYSTEM_NAME={{ beszel_node }}
141
+ owner: root
142
+ group: beszel
143
+ mode: '0640'
144
+ backup: true
145
+ notify: Restart agent
146
+ - name: Install source-owned systemd service
147
+ ansible.builtin.copy:
148
+ src: beszel-agent.service
149
+ backup: true
150
+ dest: /etc/systemd/system/beszel-agent.service
151
+ owner: root
152
+ group: root
153
+ mode: '0644'
154
+ notify: Restart agent
155
+ - name: Enable agent at boot
156
+ ansible.builtin.systemd_service:
157
+ name: beszel-agent.service
158
+ enabled: true
159
+ state: started
160
+ daemon_reload: true
161
+ - name: Complete any required agent restart before verification
162
+ ansible.builtin.meta: flush_handlers
163
+ - name: Independently check agent service postcondition
164
+ ansible.builtin.command:
165
+ argv: [systemctl, is-active, beszel-agent.service]
166
+ changed_when: false
167
+ handlers:
168
+ - name: Restart agent
169
+ ansible.builtin.systemd_service:
170
+ name: beszel-agent.service
171
+ state: restarted
172
+ daemon_reload: true
@@ -0,0 +1,42 @@
1
+ # Managed by personal Fleet: ansible/monitoring
2
+ [Unit]
3
+ Description=Personal Fleet Beszel agent
4
+ Wants=network-online.target tailscaled.service
5
+ After=network-online.target tailscaled.service
6
+ StartLimitIntervalSec=0
7
+
8
+ [Service]
9
+ Type=simple
10
+ User=beszel
11
+ Group=beszel
12
+ EnvironmentFile=/etc/personal-beszel/agent.env
13
+ Environment=DATA_DIR=/var/lib/beszel-agent
14
+ Environment=KEY_FILE=/etc/personal-beszel/hub-key
15
+ Environment=TOKEN_FILE=/etc/personal-beszel/agent-token
16
+ Environment=DISABLE_SSH=true
17
+ Environment=SKIP_SYSTEMD_LOGS=true
18
+ Environment=SERVICE_PATTERNS=ssh.service,tailscaled.service,t3code-desktop.service,personal-fleet-sync.service
19
+ ExecStart=/usr/local/lib/personal-beszel/0.21.1/beszel-agent
20
+ StateDirectory=beszel-agent
21
+ StateDirectoryMode=0700
22
+ Restart=always
23
+ RestartSec=20
24
+ TimeoutStopSec=20
25
+ NoNewPrivileges=true
26
+ ProtectSystem=strict
27
+ ProtectHome=true
28
+ PrivateTmp=true
29
+ ProtectKernelTunables=true
30
+ ProtectKernelModules=true
31
+ ProtectControlGroups=true
32
+ RestrictSUIDSGID=true
33
+ LockPersonality=true
34
+ MemoryMax=256M
35
+ CPUQuota=25%
36
+ TasksMax=128
37
+ UMask=0077
38
+ LogRateLimitIntervalSec=60
39
+ LogRateLimitBurst=100
40
+
41
+ [Install]
42
+ WantedBy=multi-user.target