@crvouga/dev-fleet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +157 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,73 @@
1
+ import { userInfo } from 'node:os';
2
+ import { resolve } from 'node:path';
3
+ import macos from '../../ansible/macos/policy.json';
4
+ import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
5
+ import { privateHub } from './beszel';
6
+ import type { AgentEnrollment } from './enroll';
7
+ import type { Check, Runner } from '../types';
8
+
9
+ /** Installs the manager's own Beszel agent locally (never over SSH) as a
10
+ * per-user launchd agent, so the manager is always a system on its own hub.
11
+ * No sudo: fleet-sync on the manager stays unattended.
12
+ */
13
+ export async function installManagerAgent(
14
+ root: string,
15
+ { node, enrollment }: { node: string; enrollment: AgentEnrollment },
16
+ run: Runner,
17
+ { uid = userInfo().uid, directory }: { uid?: number; directory?: string } = {}
18
+ ): Promise<Check[]> {
19
+ const monitoring = resolve(root, 'ansible/monitoring');
20
+ const result = await runAnsibleWithReceipt(
21
+ run,
22
+ 'agent-darwin.yml',
23
+ {
24
+ cwd: monitoring,
25
+ timeoutMs: 300_000,
26
+ input: JSON.stringify({
27
+ beszel_node: node,
28
+ beszel_hub_url: privateHub(enrollment.hubUrl),
29
+ beszel_public_key: enrollment.publicKey,
30
+ beszel_agent_token: enrollment.token,
31
+ }),
32
+ },
33
+ {
34
+ name: 'beszel-manager-agent',
35
+ sources: [monitoring, resolve(root, 'ansible/macos')],
36
+ verify: async () => agentRunning(run, uid),
37
+ ...(directory ? { directory } : {}),
38
+ }
39
+ );
40
+ if (!result.ok)
41
+ return [
42
+ {
43
+ id: 'beszel:installation',
44
+ status: 'fail',
45
+ summary: 'BESZEL_INSTALLATION_INCOMPLETE',
46
+ remedy: {
47
+ action:
48
+ 'Inspect signed artifact access, free disk and ~/Library/LaunchAgents on the manager. A leftover root daemon must be removed once (sudo launchctl bootout system/' +
49
+ macos.beszelDaemon +
50
+ '). Agent data under ~/Library/Application Support/personal-beszel-agent is preserved.',
51
+ },
52
+ },
53
+ ];
54
+ const running = await agentRunning(run, uid);
55
+ return [
56
+ {
57
+ id: 'beszel:service',
58
+ status: running ? 'pass' : 'fail',
59
+ summary: running
60
+ ? 'The manager runs its own Beszel agent as an always-on launch agent; hub metrics remain a separate gate.'
61
+ : 'BESZEL_SERVICE_STOPPED',
62
+ },
63
+ ];
64
+ }
65
+
66
+ async function agentRunning(run: Runner, uid: number): Promise<boolean> {
67
+ const state = await run([
68
+ '/bin/launchctl',
69
+ 'print',
70
+ `gui/${uid}/${macos.beszelDaemon}`,
71
+ ]);
72
+ return state.ok && /state = running/.test(state.stdout);
73
+ }
@@ -0,0 +1,232 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { delimiter, join, resolve } from 'node:path';
4
+ import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
5
+ import { failure, inspectRoute, publicEndpointCheck, tunnelToken } from './hub';
6
+ import { BESZEL_PUBLIC_VIEWER_IDENTITY, fleetAccount } from './hub-access';
7
+ import { ensurePublicViewer, type HubLogin } from './hub-viewer';
8
+ import type { FleetDefinition } from '../definition';
9
+ import { hostId, hubUrl } from '../topology';
10
+ import { PUBLIC_HUB_URL, privateHub } from './beszel';
11
+ import type { Check, Runner } from '../types';
12
+
13
+ /** Bundled CLI of the Tailscale macOS app when no `tailscale` is on PATH. */
14
+ export const TAILSCALE_APP_CLI =
15
+ '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
16
+ const LOCAL_HUB = 'http://127.0.0.1:8090';
17
+
18
+ /** Per-user hub paths on the manager. Data stays in the human's home (and its
19
+ * normal backups); it is never copied to a worker.
20
+ */
21
+ export function managerHubPaths(home = homedir()): {
22
+ data: string;
23
+ tunnel: string;
24
+ } {
25
+ const support = join(home, 'Library', 'Application Support');
26
+ return {
27
+ data: join(support, 'personal-beszel'),
28
+ tunnel: join(support, 'personal-beszel-tunnel'),
29
+ };
30
+ }
31
+
32
+ /** `tailscale` on PATH (Homebrew or the app's installed CLI), else the app
33
+ * bundle's executable; null when neither exists.
34
+ */
35
+ export function tailscaleCommand(
36
+ path = process.env['PATH'] ?? '',
37
+ exists: (candidate: string) => boolean = existsSync
38
+ ): string | null {
39
+ for (const directory of path.split(delimiter).filter(Boolean)) {
40
+ const candidate = join(directory, 'tailscale');
41
+ if (exists(candidate)) return candidate;
42
+ }
43
+ return exists(TAILSCALE_APP_CLI) ? TAILSCALE_APP_CLI : null;
44
+ }
45
+
46
+ export interface ManagerHubOptions {
47
+ home?: string;
48
+ tailscale?: string | null;
49
+ /** Fleet login for a fresh hub; defaults to the manager's Vault session. */
50
+ account?: () => Promise<HubLogin | null>;
51
+ /** Receipt directory for skipping an unchanged hub apply. */
52
+ receipts?: string;
53
+ /** Ensures the read-only AUTO_LOGIN user; defaults to the loopback hub API. */
54
+ viewer?: (login: HubLogin | null) => Promise<Check>;
55
+ }
56
+
57
+ /** Converges the singleton Beszel hub locally on the manager: pinned Compose
58
+ * under Docker Desktop, the connector token from the manager's own Vault
59
+ * session (private variables file, never argv or output), a private Tailscale
60
+ * Serve route, then independent health checks locally and over the tailnet.
61
+ */
62
+ export async function convergeManagerHub(
63
+ root: string,
64
+ definition: FleetDefinition,
65
+ run: Runner,
66
+ options: ManagerHubOptions = {}
67
+ ): Promise<Check[]> {
68
+ const hub = privateHub(hubUrl(definition));
69
+ const refused = await preconditions(definition, run);
70
+ if (refused) return refused;
71
+ const paths = managerHubPaths(options.home);
72
+ const tunnel = await tunnelToken(run);
73
+ const account = await (options.account ?? fleetAccount)();
74
+ const monitoring = resolve(root, 'ansible/monitoring');
75
+ const result = await runAnsibleWithReceipt(
76
+ run,
77
+ 'hub-darwin.yml',
78
+ {
79
+ cwd: monitoring,
80
+ input: JSON.stringify(hubVariables(hub, paths, tunnel, account)),
81
+ timeoutMs: 300_000,
82
+ },
83
+ {
84
+ name: 'beszel-hub',
85
+ sources: [monitoring],
86
+ // Health is verified independently below on every run.
87
+ verify: async () =>
88
+ (
89
+ await run([
90
+ 'curl',
91
+ '-fsS',
92
+ '--max-time',
93
+ '5',
94
+ '-o',
95
+ '/dev/null',
96
+ `${LOCAL_HUB}/api/health`,
97
+ ])
98
+ ).ok,
99
+ ...(options.receipts ? { directory: options.receipts } : {}),
100
+ }
101
+ );
102
+ if (!result.ok)
103
+ return failure(
104
+ 'BESZEL_HUB_INSTALLATION_INCOMPLETE',
105
+ 'Inspect Docker Desktop, the Compose image, file sharing for ~/Library/Application Support and disk space. Existing data is never deleted.'
106
+ );
107
+ const route = await servePrivateRoute(
108
+ hub,
109
+ run,
110
+ options.tailscale === undefined ? tailscaleCommand() : options.tailscale
111
+ );
112
+ if (route) return route;
113
+ const health = await verifyHealth(hub, run);
114
+ if (health) return health;
115
+ return [
116
+ {
117
+ id: 'beszel:hub',
118
+ status: 'pass',
119
+ summary: `Pinned hub on the manager is healthy locally and at ${hub}; authentication and agent metrics are separate checks.`,
120
+ },
121
+ publicEndpointCheck(tunnel !== null),
122
+ await ensureViewer(options, account),
123
+ ];
124
+ }
125
+
126
+ function hubVariables(
127
+ hub: string,
128
+ paths: { data: string; tunnel: string },
129
+ tunnel: string | null,
130
+ account: HubLogin | null
131
+ ): Record<string, string> {
132
+ return {
133
+ beszel_hub_url: hub,
134
+ beszel_public_url: PUBLIC_HUB_URL,
135
+ beszel_auto_login: BESZEL_PUBLIC_VIEWER_IDENTITY,
136
+ beszel_data_dir: paths.data,
137
+ beszel_tunnel_dir: paths.tunnel,
138
+ ...(tunnel ? { beszel_tunnel_token: tunnel } : {}),
139
+ ...(account
140
+ ? {
141
+ beszel_fleet_identity: account.identity,
142
+ beszel_fleet_password: account.password,
143
+ }
144
+ : {}),
145
+ };
146
+ }
147
+
148
+ function ensureViewer(
149
+ options: ManagerHubOptions,
150
+ account: HubLogin | null
151
+ ): Promise<Check> {
152
+ return (options.viewer ?? ensurePublicViewer)(account);
153
+ }
154
+
155
+ /** Runs only on the manager, and only with Docker Desktop's engine up. */
156
+ async function preconditions(
157
+ definition: FleetDefinition,
158
+ run: Runner
159
+ ): Promise<Check[] | null> {
160
+ const local = await run(['/usr/sbin/scutil', '--get', 'LocalHostName']);
161
+ if (!local.ok || hostId(local.stdout.trim()) !== definition.manager.id)
162
+ return failure(
163
+ 'BESZEL_NOT_MANAGER',
164
+ `The Beszel hub runs only on the manager (${definition.manager.id}). Run fleet-sync from the manager; workers only run the agent.`
165
+ );
166
+ const docker = await run(
167
+ ['docker', 'info', '--format', '{{.ServerVersion}}'],
168
+ { timeoutMs: 20_000 }
169
+ );
170
+ if (!docker.ok || docker.stdout.trim() === '')
171
+ return failure(
172
+ 'BESZEL_DOCKER_DESKTOP_REQUIRED',
173
+ 'Start Docker Desktop on the manager (and enable "Start Docker Desktop when you sign in"), then rerun fleet-sync. Existing hub data is never deleted.'
174
+ );
175
+ return null;
176
+ }
177
+
178
+ async function servePrivateRoute(
179
+ hub: string,
180
+ run: Runner,
181
+ cli: string | null
182
+ ): Promise<Check[] | null> {
183
+ if (!cli)
184
+ return failure(
185
+ 'BESZEL_TAILSCALE_REQUIRED',
186
+ `Install and sign in to Tailscale on the manager (Homebrew CLI or ${TAILSCALE_APP_CLI}).`
187
+ );
188
+ const status = await run([cli, 'serve', 'status', '--json']);
189
+ if (!status.ok)
190
+ return failure(
191
+ 'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
192
+ 'Inspect Tailscale Serve permission and existing private routes before retry.'
193
+ );
194
+ const route = inspectRoute(status.stdout, hub);
195
+ if (Array.isArray(route)) return route;
196
+ if (!route) {
197
+ const serve = [cli, 'serve', '--bg', '--https=8443', LOCAL_HUB] as const;
198
+ const applied =
199
+ (await run([...serve], { timeoutMs: 20_000 })).ok ||
200
+ (await run(['sudo', '-n', ...serve], { timeoutMs: 20_000 })).ok;
201
+ if (!applied)
202
+ return failure(
203
+ 'BESZEL_PRIVATE_ROUTE_REQUIRED',
204
+ 'Allow Tailscale Serve (HTTPS certificates) for this tailnet and approve it in the Tailscale app if asked; no tailnet policy or public ingress is changed.'
205
+ );
206
+ }
207
+ const verified = await run([cli, 'serve', 'status', '--json']);
208
+ if (!verified.ok || inspectRoute(verified.stdout, hub) !== true)
209
+ return failure(
210
+ 'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
211
+ 'The requested route was not independently observed after application. Inspect Serve permissions and configuration; installer success is insufficient.'
212
+ );
213
+ return null;
214
+ }
215
+
216
+ async function verifyHealth(hub: string, run: Runner): Promise<Check[] | null> {
217
+ const probe = (url: string) =>
218
+ run(['curl', '-fsS', '--max-time', '10', '-o', '/dev/null', url], {
219
+ timeoutMs: 15_000,
220
+ });
221
+ if (!(await probe(`${LOCAL_HUB}/api/health`)).ok)
222
+ return failure(
223
+ 'BESZEL_HUB_UNHEALTHY',
224
+ 'The hub container did not answer /api/health locally. Inspect Docker Desktop and the personal-beszel Compose project; preserve its data.'
225
+ );
226
+ if (!(await probe(`${hub}/api/health`)).ok)
227
+ return failure(
228
+ 'BESZEL_HUB_UNREACHABLE',
229
+ `The hub is healthy locally but not at ${hub}. Inspect Tailscale on the manager and its Serve route.`
230
+ );
231
+ return null;
232
+ }
@@ -0,0 +1,65 @@
1
+ import type { Check, FleetNode, Runner } from '../types';
2
+ import { shellQuote, sshArgs, sshFailure } from '../ssh';
3
+ import { validCheck, cleanCheck } from '../protocol';
4
+ import { record } from '../config';
5
+ import { isRevision } from '../machine/state';
6
+ import type { AgentEnrollment } from './enroll';
7
+ import { remoteLockPrelude } from '../remote-lock';
8
+
9
+ /** Installs the Beszel agent on a worker. Only node-specific agent credentials
10
+ * cross SSH; hub account credentials stay on the manager. Workers never host
11
+ * the hub (it runs only on the manager), so there is no remote hub mode.
12
+ * The installed source and remote operation lock are reused.
13
+ */
14
+ export async function installRemoteAgent(
15
+ node: FleetNode,
16
+ revision: string,
17
+ enrollment: AgentEnrollment,
18
+ run: Runner
19
+ ): Promise<Check[]> {
20
+ if (!isRevision(revision)) throw new Error('BESZEL_REVISION');
21
+ const root = `"$HOME/.local/share/personal/fleet/revisions/${revision}"`;
22
+ const locked = shellQuote(
23
+ JSON.stringify({
24
+ version: 1,
25
+ checks: [
26
+ {
27
+ id: 'beszel:lock',
28
+ status: 'fail',
29
+ summary: 'FLEET_REMOTE_LOCKED',
30
+ remedy: {
31
+ action:
32
+ 'Another remote operation owns the lock. Inspect its state and retry after it finishes; never delete the lock or start a competing installer.',
33
+ },
34
+ },
35
+ ],
36
+ })
37
+ );
38
+ const command = `cd ${root} && { ${remoteLockPrelude()} || { printf '%s\\n' ${locked}; exit 0; }; } && trap '' HUP && exec "$HOME/.local/bin/mise" exec --locked bun -- bun ${shellQuote('src/monitoring/apply.ts')}`;
39
+ const result = await run([...sshArgs(node), command], {
40
+ input: JSON.stringify({ ...enrollment, node: node.id, revision }),
41
+ timeoutMs: 360_000,
42
+ });
43
+ if (!result.ok) return [sshFailure(result)];
44
+ try {
45
+ const value: unknown = JSON.parse(result.stdout);
46
+ if (
47
+ record(value) &&
48
+ value['version'] === 1 &&
49
+ Array.isArray(value['checks']) &&
50
+ value['checks'].length > 0 &&
51
+ value['checks'].length <= 20 &&
52
+ value['checks'].every(validCheck)
53
+ )
54
+ return value['checks'].map(cleanCheck);
55
+ } catch {
56
+ /* Never expose native output or credentials. */
57
+ }
58
+ return [
59
+ {
60
+ id: 'beszel:protocol',
61
+ status: 'fail',
62
+ summary: 'BESZEL_REMOTE_EVIDENCE_INVALID',
63
+ },
64
+ ];
65
+ }
@@ -0,0 +1,83 @@
1
+ import { type FleetDefinition, type MachineDefinition } from './definition';
2
+ import { record } from './config';
3
+ import { FleetUsageError } from './errors';
4
+
5
+ export interface NetworkHost {
6
+ alias: string;
7
+ tailscaleName: string;
8
+ sshPort: number;
9
+ user: string;
10
+ identityFile: string;
11
+ os: 'linux' | 'macOS' | 'windows';
12
+ t3Port: number;
13
+ }
14
+ export function networkProjection(
15
+ definition: FleetDefinition,
16
+ identities: Record<string, string>
17
+ ): NetworkHost[] {
18
+ return definition.workers.flatMap((node) =>
19
+ node.network
20
+ ? [
21
+ {
22
+ alias: node.ssh,
23
+ ...node.network,
24
+ os: node.os,
25
+ identityFile: identities[node.id] ?? '',
26
+ },
27
+ ]
28
+ : []
29
+ );
30
+ }
31
+ function compatible(
32
+ node: MachineDefinition | undefined,
33
+ host: Record<string, unknown>
34
+ ): node is MachineDefinition {
35
+ return Boolean(
36
+ node?.network &&
37
+ Object.entries(node.network).every(([key, value]) => host[key] === value) &&
38
+ node.os === host['os']
39
+ );
40
+ }
41
+ /** Immutable migration input supplies old manager paths only, never desired hosts. */
42
+ export function legacyIdentities(
43
+ definition: FleetDefinition,
44
+ raw: unknown
45
+ ): Record<string, string> {
46
+ if (!record(raw) || !Array.isArray(raw['hosts']))
47
+ throw new FleetUsageError('Invalid legacy Workstation inventory.');
48
+ const result: Record<string, string> = {};
49
+ for (const host of raw['hosts']) {
50
+ if (!record(host))
51
+ throw new FleetUsageError('Invalid legacy network host.');
52
+ const node = definition.workers.find((node) => node.ssh === host['alias']);
53
+ // A retired host has no manager identity to migrate into this fleet.
54
+ if (!node) continue;
55
+ if (!compatible(node, host))
56
+ throw new FleetUsageError(
57
+ 'Legacy Workstation and canonical Fleet definitions conflict. Review IDs, aliases and network settings before migration.'
58
+ );
59
+ const identity = host['identityFile'];
60
+ if (
61
+ typeof identity !== 'string' ||
62
+ !/^(~\/|\/)[^\x00-\x1f\x7f]+$/.test(identity)
63
+ )
64
+ throw new FleetUsageError('Legacy identity path is invalid.');
65
+ if (result[node.id] !== undefined)
66
+ throw new FleetUsageError('Duplicate legacy machine assignment.');
67
+ result[node.id] = identity;
68
+ }
69
+ return result;
70
+ }
71
+ /** `legacy` is the consumer's earlier inventory, when it has one: it only
72
+ * supplies identity paths the fleet configuration does not hold.
73
+ */
74
+ export function loadNetworkDefinition(
75
+ definition: FleetDefinition,
76
+ legacy: unknown,
77
+ overrides?: Record<string, string>
78
+ ): NetworkHost[] {
79
+ return networkProjection(
80
+ definition,
81
+ overrides ?? legacyIdentities(definition, legacy)
82
+ );
83
+ }