@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,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,134 @@
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_node is match('^[a-z][a-z0-9-]{0,62}$')
17
+ - beszel_hub_url is match('^https://[a-z0-9.-]+\.ts\.net:8443$')
18
+ - name: Inspect existing native service ownership
19
+ ansible.builtin.stat:
20
+ path: /etc/systemd/system/beszel-agent.service
21
+ checksum_algorithm: sha256
22
+ register: existing_service
23
+ - name: Read existing service ownership marker
24
+ ansible.builtin.slurp:
25
+ src: /etc/systemd/system/beszel-agent.service
26
+ register: existing_unit
27
+ when: existing_service.stat.exists
28
+ - name: Require explicit adoption of any previously unmanaged service
29
+ ansible.builtin.assert:
30
+ that:
31
+ - "not existing_service.stat.exists or (existing_unit.content | default('') | b64decode).startswith('# Managed by personal Fleet:')"
32
+ fail_msg: Unmanaged Beszel unit exists. Review a source-controlled migration before replacing it.
33
+ - name: Create unprivileged service account
34
+ ansible.builtin.user:
35
+ name: beszel
36
+ system: true
37
+ create_home: false
38
+ shell: /usr/sbin/nologin
39
+ - name: Create restricted configuration and version directories
40
+ ansible.builtin.file:
41
+ path: '{{ item.path }}'
42
+ state: directory
43
+ owner: root
44
+ group: '{{ item.group }}'
45
+ mode: '{{ item.mode }}'
46
+ loop:
47
+ - { path: /etc/personal-beszel, group: beszel, mode: '0750' }
48
+ - {
49
+ path: '/usr/local/lib/personal-beszel/0.21.1',
50
+ group: root,
51
+ mode: '0755',
52
+ }
53
+ - name: Deliver node-specific token through private stdin, never command arguments
54
+ ansible.builtin.copy:
55
+ dest: '/etc/personal-beszel/{{ item.name }}'
56
+ content: '{{ item.value }}'
57
+ owner: root
58
+ group: beszel
59
+ mode: '0640'
60
+ loop:
61
+ - { name: hub-key, value: '{{ beszel_public_key }}' }
62
+ - { name: agent-token, value: '{{ beszel_agent_token }}' }
63
+ no_log: true
64
+ notify: Restart agent
65
+ - name: Require credentials delivered through the approved secret owner
66
+ ansible.builtin.stat:
67
+ path: '/etc/personal-beszel/{{ item }}'
68
+ loop: [hub-key, agent-token]
69
+ register: credentials
70
+ no_log: true
71
+ - name: Fail closed without node-specific credentials
72
+ ansible.builtin.assert:
73
+ that: item.stat.exists and item.stat.isreg and item.stat.size > 0
74
+ fail_msg: Beszel credentials missing; resume enrollment through Fleet.
75
+ loop: '{{ credentials.results }}'
76
+ no_log: true
77
+ - name: Restrict credential permissions
78
+ ansible.builtin.file:
79
+ path: '/etc/personal-beszel/{{ item }}'
80
+ owner: root
81
+ group: beszel
82
+ mode: '0640'
83
+ loop: [hub-key, agent-token]
84
+ - name: Download pinned native artifact with signature-preserving HTTPS and checksum
85
+ ansible.builtin.get_url:
86
+ url: 'https://github.com/henrygd/beszel/releases/download/v{{ beszel_lock.version }}/beszel-agent_linux_{{ beszel_arch }}.tar.gz'
87
+ dest: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/agent.tar.gz'
88
+ checksum: 'sha256:{{ beszel_lock.agents[beszel_arch] }}'
89
+ mode: '0644'
90
+ timeout: 60
91
+ - name: Extract the pinned executable and repair file drift
92
+ ansible.builtin.unarchive:
93
+ src: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}/agent.tar.gz'
94
+ dest: '/usr/local/lib/personal-beszel/{{ beszel_lock.version }}'
95
+ remote_src: true
96
+ notify: Restart agent
97
+ - name: Install nonsecret node connection settings
98
+ ansible.builtin.copy:
99
+ dest: /etc/personal-beszel/agent.env
100
+ content: |
101
+ HUB_URL={{ beszel_hub_url }}
102
+ SYSTEM_NAME={{ beszel_node }}
103
+ owner: root
104
+ group: beszel
105
+ mode: '0640'
106
+ backup: true
107
+ notify: Restart agent
108
+ - name: Install source-owned systemd service
109
+ ansible.builtin.copy:
110
+ src: beszel-agent.service
111
+ backup: true
112
+ dest: /etc/systemd/system/beszel-agent.service
113
+ owner: root
114
+ group: root
115
+ mode: '0644'
116
+ notify: Restart agent
117
+ - name: Enable agent at boot
118
+ ansible.builtin.systemd_service:
119
+ name: beszel-agent.service
120
+ enabled: true
121
+ state: started
122
+ daemon_reload: true
123
+ - name: Complete any required agent restart before verification
124
+ ansible.builtin.meta: flush_handlers
125
+ - name: Independently check agent service postcondition
126
+ ansible.builtin.command:
127
+ argv: [systemctl, is-active, beszel-agent.service]
128
+ changed_when: false
129
+ handlers:
130
+ - name: Restart agent
131
+ ansible.builtin.systemd_service:
132
+ name: beszel-agent.service
133
+ state: restarted
134
+ 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
@@ -0,0 +1,74 @@
1
+ # Run on the manager only, through hub-darwin.yml from the verified checkout.
2
+ # Never copy the hub data to a worker or run a second copy of its identity.
3
+ name: personal-beszel
4
+ services:
5
+ hub:
6
+ image: henrygd/beszel:0.21.1@sha256:8514a69d9bbbcb74f371a3205561ee231fc0289739cea64e8131f341d695b770
7
+ restart: unless-stopped
8
+ ports:
9
+ - '127.0.0.1:8090:8090'
10
+ environment:
11
+ APP_URL: '${BESZEL_APP_URL:-${BESZEL_HUB_URL:?private HTTPS hub URL required}}'
12
+ DISABLE_SSH: 'true'
13
+ CHECK_UPDATES: 'false'
14
+ # Single-operator private hub: fleet-registered systems are visible to every login.
15
+ SHARE_ALL_SYSTEMS: 'true'
16
+ # Visitors are signed in automatically as this read-only viewer account
17
+ # (never the fleet admin), so the dashboard needs no login. Empty keeps
18
+ # the normal login page.
19
+ AUTO_LOGIN: '${BESZEL_AUTO_LOGIN:-}'
20
+ # First start only (empty data directory): Beszel creates the fleet login
21
+ # as a verified user and superuser. hub-darwin.yml sets these just once.
22
+ USER_EMAIL: '${BESZEL_BOOTSTRAP_EMAIL:-}'
23
+ USER_PASSWORD: '${BESZEL_BOOTSTRAP_PASSWORD:-}'
24
+ volumes:
25
+ - type: bind
26
+ source: '${BESZEL_DATA_DIR:?per-user hub data directory required}'
27
+ target: /beszel_data
28
+ bind:
29
+ create_host_path: false
30
+ logging:
31
+ driver: local
32
+ options:
33
+ max-size: '10m'
34
+ max-file: '3'
35
+ mem_limit: 512m
36
+ cpus: 1.0
37
+ pids_limit: 128
38
+ stop_grace_period: 30s
39
+ # Outbound-only Cloudflare Tunnel for the public dashboard hostname. The
40
+ # connector token is delivered from the manager's Vault session to a 0600
41
+ # per-user file by hub-darwin.yml.
42
+ cloudflared:
43
+ profiles: [public-endpoint]
44
+ image: cloudflare/cloudflared:2026.9.3@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c
45
+ user: '1000:1000'
46
+ restart: unless-stopped
47
+ command:
48
+ [
49
+ 'tunnel',
50
+ '--no-autoupdate',
51
+ 'run',
52
+ '--token-file',
53
+ '/run/secrets/tunnel-token',
54
+ ]
55
+ volumes:
56
+ - type: bind
57
+ source: '${BESZEL_TUNNEL_TOKEN_FILE:?per-user connector token file required}'
58
+ target: /run/secrets/tunnel-token
59
+ read_only: true
60
+ bind:
61
+ create_host_path: false
62
+ depends_on:
63
+ - hub
64
+ read_only: true
65
+ cap_drop: [ALL]
66
+ security_opt: ['no-new-privileges:true']
67
+ mem_limit: 256m
68
+ cpus: 0.5
69
+ pids_limit: 128
70
+ logging:
71
+ driver: local
72
+ options:
73
+ max-size: '5m'
74
+ max-file: '2'
@@ -0,0 +1,102 @@
1
+ ---
2
+ # The Beszel hub runs only on the manager (the human's macOS machine) under
3
+ # Docker Desktop, as that user: no become, per-user data and credential paths.
4
+ # Runtime variables arrive through a private 0600 variables file, never argv.
5
+ - name: Converge the Beszel hub on the manager
6
+ hosts: localhost
7
+ connection: local
8
+ become: false
9
+ gather_facts: true
10
+ vars:
11
+ beszel_lock: "{{ lookup('ansible.builtin.file', 'lock.json') | from_json }}"
12
+ beszel_compose:
13
+ - docker
14
+ - compose
15
+ - '-f'
16
+ - '{{ playbook_dir }}/compose.yml'
17
+ beszel_environment:
18
+ BESZEL_HUB_URL: '{{ beszel_hub_url }}'
19
+ BESZEL_APP_URL: '{{ beszel_public_url | default(beszel_hub_url) }}'
20
+ BESZEL_DATA_DIR: '{{ beszel_data_dir }}'
21
+ BESZEL_AUTO_LOGIN: '{{ beszel_auto_login | default("") }}'
22
+ BESZEL_TUNNEL_TOKEN_FILE: '{{ beszel_tunnel_dir }}/token'
23
+ tasks:
24
+ - name: Require the macOS manager and private dashboard policy
25
+ ansible.builtin.assert:
26
+ that:
27
+ - ansible_facts.os_family == 'Darwin'
28
+ - beszel_hub_url is match('^https://[a-z0-9.-]+\.ts\.net:8443$')
29
+ - beszel_data_dir is match('^/Users/[^/]+/Library/Application Support/personal-beszel$')
30
+ - beszel_tunnel_dir is match('^/Users/[^/]+/Library/Application Support/personal-beszel-tunnel$')
31
+ fail_msg: BESZEL_NOT_MANAGER; the hub runs only on the macOS manager with per-user data.
32
+ - name: Require a running Docker Desktop engine with Compose
33
+ ansible.builtin.command:
34
+ argv: [docker, compose, version]
35
+ changed_when: false
36
+ - name: Inspect an existing hub image before any upgrade
37
+ ansible.builtin.command:
38
+ argv:
39
+ [
40
+ docker,
41
+ inspect,
42
+ '--format',
43
+ '{{ "{{.Config.Image}}" }}',
44
+ personal-beszel-hub-1,
45
+ ]
46
+ changed_when: false
47
+ failed_when: false
48
+ register: existing_hub
49
+ - name: Require explicit maintenance for a different existing image
50
+ ansible.builtin.assert:
51
+ that: existing_hub.rc != 0 or existing_hub.stdout == beszel_lock.image
52
+ fail_msg: BESZEL_HUB_UPGRADE_PENDING; take and verify a backup before an explicit maintenance upgrade.
53
+ - name: Create per-user hub data and connector directories without replacing data
54
+ ansible.builtin.file:
55
+ path: '{{ item }}'
56
+ state: directory
57
+ mode: '0700'
58
+ loop:
59
+ - '{{ beszel_data_dir }}'
60
+ - '{{ beszel_tunnel_dir }}'
61
+ - name: Write the public dashboard connector token for the current user only
62
+ ansible.builtin.copy:
63
+ dest: '{{ beszel_tunnel_dir }}/token'
64
+ content: '{{ beszel_tunnel_token }}'
65
+ mode: '0600'
66
+ when: beszel_tunnel_token | default('') | length > 0
67
+ no_log: true
68
+ - name: Detect a hub that has never started (no database yet)
69
+ ansible.builtin.stat:
70
+ path: '{{ beszel_data_dir }}/data.db'
71
+ register: hub_database
72
+ - name: Start pinned stock Compose service
73
+ ansible.builtin.command:
74
+ argv: "{{ beszel_compose + ['up', '-d', '--pull', 'missing', 'hub'] }}"
75
+ # A fresh hub creates the fleet login itself on first start, so no one
76
+ # has to create the account by hand. Existing data is never changed.
77
+ environment: >-
78
+ {{ beszel_environment | combine({
79
+ 'BESZEL_BOOTSTRAP_EMAIL': beszel_fleet_identity,
80
+ 'BESZEL_BOOTSTRAP_PASSWORD': beszel_fleet_password
81
+ } if (not hub_database.stat.exists
82
+ and beszel_fleet_password | default('') | length > 0)
83
+ else {}) }}
84
+ no_log: true
85
+ register: hub_apply
86
+ changed_when: "'Started' in hub_apply.stderr or 'Created' in hub_apply.stderr or 'Recreated' in hub_apply.stderr"
87
+ - name: Start the public connector when its credential is present
88
+ ansible.builtin.command:
89
+ argv: "{{ beszel_compose + ['--profile', 'public-endpoint', 'up', '-d', '--pull', 'missing'] }}"
90
+ environment: '{{ beszel_environment }}'
91
+ when: beszel_tunnel_token | default('') | length > 0
92
+ register: tunnel_apply
93
+ changed_when: "'Started' in tunnel_apply.stderr or 'Created' in tunnel_apply.stderr or 'Recreated' in tunnel_apply.stderr"
94
+ - name: Independently verify the local hub health endpoint
95
+ ansible.builtin.uri:
96
+ url: http://127.0.0.1:8090/api/health
97
+ status_code: 200
98
+ return_content: false
99
+ retries: 10
100
+ delay: 2
101
+ register: hub_health
102
+ until: hub_health.status == 200
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "0.21.1",
3
+ "image": "henrygd/beszel:0.21.1@sha256:8514a69d9bbbcb74f371a3205561ee231fc0289739cea64e8131f341d695b770",
4
+ "agents": {
5
+ "amd64": "3b6ee592632ec03b9a43312e99a40203a7b3d83fe40dd0c25acda1eb915ea7c0",
6
+ "arm64": "a6ea544976b9a74398ac88d5da5bb96813d9a2e353a37bbfdb6a903fa8ba1838",
7
+ "darwin-arm64": "9246065f680cdb0af2885b7bf5876dc45b754019f2c90eb28550cf45db978eb8",
8
+ "darwin-amd64": "21f4a03fd90ae46869cfcbb69e21e8312f0f05deff49669d4602a3dee4866981"
9
+ },
10
+ "source": "https://github.com/henrygd/beszel/releases/tag/v0.21.1"
11
+ }
@@ -0,0 +1,13 @@
1
+ # Read-only health probe that `dev-fleet net check` pipes to each host over SSH (`sh -s`).
2
+ # Prints key=value lines; works on macOS and Linux (including WSL).
3
+ if [ "$(uname)" = Darwin ]; then
4
+ echo "ncpu=$(sysctl -n hw.ncpu)"
5
+ echo "load=$(sysctl -n vm.loadavg | awk '{print $2}')"
6
+ echo "memfree=$(memory_pressure -Q 2>/dev/null | awk -F': ' '/percentage/{gsub("%","",$2); print $2}')"
7
+ sysctl -n vm.swapusage | awk '{t=$3; u=$6; gsub("M","",t); gsub("M","",u); if (t>0) print "swap=" int(u*100/t); else print "swap=0"}'
8
+ else
9
+ echo "ncpu=$(nproc)"
10
+ echo "load=$(cut -d' ' -f1 /proc/loadavg)"
11
+ awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} /SwapTotal/{st=$2} /SwapFree/{sf=$2} END{print "memfree=" int(a*100/t); if (st>0) print "swap=" int((st-sf)*100/st); else print "swap=0"}' /proc/meminfo
12
+ fi
13
+ echo "diskmb=$(df -Pk "$HOME" | awk 'NR==2{print int($4/1024)}')"
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+ # Manager-led, explicitly approved initial privilege step. Run through a real
3
+ # SSH terminal so sudo reads the operator's password itself; never capture it.
4
+ set -eu
5
+ umask 077
6
+ case "$(uname -s)" in
7
+ Linux)
8
+ . /etc/os-release
9
+ [ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || exit 1 ;;
10
+ Darwin)
11
+ [ "$(sw_vers -productVersion | cut -d. -f1)" -ge 14 ] || exit 1 ;;
12
+ *) exit 1 ;;
13
+ esac
14
+ user=$(id -un)
15
+ case "$user" in root|''|*[!a-z0-9_-]*) exit 1;; esac
16
+ command -v sudo >/dev/null 2>&1 || exit 1
17
+ # The same operation lock used by remote bootstrap excludes another installer.
18
+ root="$HOME/.local/share/personal/fleet"
19
+ for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
20
+ [ ! -L "$path" ] || exit 1
21
+ done
22
+ mkdir -p "$root"
23
+ chmod 700 "$root"
24
+ [ ! -L "$root/remote-operation.lock" ] || exit 1
25
+ exec 9>"$root/remote-operation.lock"
26
+ if command -v flock >/dev/null 2>&1; then
27
+ flock -n 9 || exit 1
28
+ else
29
+ python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || exit 1
30
+ fi
31
+ sudo -v
32
+ file=$(mktemp)
33
+ trap 'rm -f "$file"' EXIT
34
+ trap 'exit 1' HUP INT TERM
35
+ printf '# Managed by personal Fleet: designated development user\n%s ALL=(ALL:ALL) NOPASSWD: ALL\n' "$user" >"$file"
36
+ sudo -n visudo -cf "$file" >/dev/null
37
+ # This dedicated path is owned by the same Workstation baseline. Preserve any
38
+ # existing fragment for recovery before installing the validated declaration.
39
+ target="/etc/sudoers.d/personal-fleet-$user"
40
+ if sudo -n test -e "$target"; then
41
+ sudo -n test ! -L "$target" || exit 1
42
+ sudo -n grep -qxF '# Managed by personal Fleet: designated development user' "$target" || {
43
+ printf '%s\n' 'SUDOERS_OWNERSHIP_CONFLICT: preserve the existing fragment and review its source before migration.' >&2
44
+ exit 1
45
+ }
46
+ sudo -n cp -p -- "$target" "$target.before-fleet-enrollment"
47
+ fi
48
+ group=root
49
+ [ "$(uname -s)" != Darwin ] || group=wheel
50
+ sudo -n install -o root -g "$group" -m 0440 -- "$file" "$target"
51
+ sudo -n true