@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,89 @@
1
+ ---
2
+ - name: Apply reviewed Ubuntu development-server policy
3
+ hosts: localhost
4
+ connection: local
5
+ become: true
6
+ gather_facts: true
7
+ vars_files:
8
+ - policy.json
9
+ pre_tasks:
10
+ - name: Require the supported common release and an explicit non-root user
11
+ ansible.builtin.assert:
12
+ that:
13
+ - ansible_facts.distribution == 'Ubuntu'
14
+ - ansible_facts.distribution_version == release
15
+ - ansible_facts.architecture in ['x86_64', 'aarch64']
16
+ - development_user is match('^[a-z_][a-z0-9_-]{0,31}$')
17
+ - development_user != 'root'
18
+ fail_msg: Unsupported platform or user; no distribution upgrade is permitted.
19
+ - name: Read the designated existing account
20
+ ansible.builtin.getent:
21
+ database: passwd
22
+ key: '{{ development_user }}'
23
+ - name: Refuse a different user identity or home
24
+ ansible.builtin.assert:
25
+ that:
26
+ - ansible_facts.getent_passwd[development_user][1] | int == development_uid | int
27
+ - ansible_facts.getent_passwd[development_user][4] == development_home
28
+ # Memory and disk safety must land even when the disk is nearly full.
29
+ - ansible.builtin.import_tasks: tasks/resources.yml
30
+ - name: Inspect capacity before package and service changes
31
+ ansible.builtin.command:
32
+ argv:
33
+ - /usr/bin/python3
34
+ - '-c'
35
+ - |
36
+ import json, os
37
+ print(json.dumps([{"path": p, "freeMiB": (v.f_bavail * v.f_frsize) // (1024 * 1024), "freeInodes": v.f_favail} for p in ['/', '/var', os.environ['FLEET_HOME']] for v in [os.statvfs(p)]]))
38
+ environment:
39
+ FLEET_HOME: '{{ development_home }}'
40
+ changed_when: false
41
+ register: capacity
42
+ - name: Preserve management capacity instead of installing into an exhausted disk
43
+ ansible.builtin.assert:
44
+ that:
45
+ - item.freeMiB >= minimumFreeDiskMiB
46
+ - item.freeInodes >= minimumFreeInodes
47
+ fail_msg: RESOURCE_CAPACITY_REQUIRED; inspect disk space and inodes on the reported filesystem. Preserve developer work; no automatic cleanup or process killing.
48
+ loop: '{{ capacity.stdout | from_json }}'
49
+ - name: Inspect installed packages
50
+ ansible.builtin.package_facts:
51
+ manager: apt
52
+ - name: Preserve Docker installations owned by a different distribution
53
+ ansible.builtin.assert:
54
+ that: "'docker-ce' not in ansible_facts.packages and 'containerd.io' not in ansible_facts.packages"
55
+ fail_msg: DOCKER_OWNER_CONFLICT; migrate the existing Docker owner explicitly before applying the Ubuntu-native baseline.
56
+ - name: Inspect existing service ownership
57
+ ansible.builtin.service_facts:
58
+ - name: Wait for legitimate package transactions before inspecting dpkg
59
+ ansible.builtin.command:
60
+ argv:
61
+ - /usr/bin/python3
62
+ - '-c'
63
+ - |
64
+ import fcntl
65
+ files = [open(path, 'a') for path in ['/var/lib/dpkg/lock-frontend', '/var/lib/dpkg/lock']]
66
+ for handle in files:
67
+ fcntl.lockf(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
68
+ register: apt_idle
69
+ changed_when: false
70
+ retries: 24
71
+ delay: 5
72
+ until: apt_idle.rc == 0
73
+ - name: Inspect package database consistency
74
+ ansible.builtin.command: dpkg --audit
75
+ changed_when: false
76
+ register: dpkg_audit
77
+ - name: Require package database recovery before modifying packages
78
+ ansible.builtin.assert:
79
+ that: dpkg_audit.stdout | trim == ''
80
+ fail_msg: APT_STATE_BROKEN; inspect dpkg --audit and use reviewed package recovery. Never remove package lock files.
81
+ tasks:
82
+ - ansible.builtin.import_tasks: tasks/packages.yml
83
+ - ansible.builtin.import_tasks: tasks/docker.yml
84
+ - ansible.builtin.import_tasks: tasks/services.yml
85
+ - ansible.builtin.import_tasks: tasks/power.yml
86
+ - ansible.builtin.import_tasks: tasks/user.yml
87
+ - ansible.builtin.import_tasks: tasks/ssh.yml
88
+ - ansible.builtin.import_tasks: tasks/credentials.yml
89
+ when: development_source is defined
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/python3
2
+ """Stop the user process group that is filling the disk before it runs out.
3
+
4
+ Managed by personal fleet-sync (tasks/resources.yml) as
5
+ personal-fleet-disk-guard.service. Every interval it measures free space on the
6
+ watched filesystems and the bytes each leaf cgroup under user.slice wrote. When
7
+ any watched filesystem is below the floor, it kills only the single cgroup that
8
+ wrote the most during the last interval, and only if that is at least the write
9
+ threshold. It never deletes files, never touches system services and never
10
+ kills idle work. Free space must be recovered by a person.
11
+
12
+ Usage: disk-guard.py --path / --path /home/me [--floor-mib N] [--write-mib N]
13
+ [--interval S] [--cgroup-root DIR] [--dry-run]
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ MIB = 1024 * 1024
23
+
24
+
25
+ def leaf_writes(root):
26
+ """Bytes written so far by every leaf cgroup below root, from io.stat."""
27
+ writes = {}
28
+ for current, dirs, files in os.walk(root):
29
+ if dirs or 'io.stat' not in files:
30
+ continue
31
+ try:
32
+ text = Path(current, 'io.stat').read_text()
33
+ except OSError:
34
+ continue
35
+ total = 0
36
+ for line in text.splitlines():
37
+ for field in line.split()[1:]:
38
+ key, _, value = field.partition('=')
39
+ if key == 'wbytes' and value.isdigit():
40
+ total += int(value)
41
+ writes[current] = total
42
+ return writes
43
+
44
+
45
+ def choose_victim(previous, current, low, threshold):
46
+ """The leaf that wrote the most since the previous sample, if it is a runaway."""
47
+ if not low:
48
+ return None
49
+ best, best_delta = None, 0
50
+ for path, total in current.items():
51
+ delta = total - previous.get(path, total)
52
+ if delta > best_delta:
53
+ best, best_delta = path, delta
54
+ if best is None or best_delta < threshold:
55
+ return None
56
+ return best, best_delta
57
+
58
+
59
+ def free_bytes(path):
60
+ st = os.statvfs(path)
61
+ return st.f_bavail * st.f_frsize
62
+
63
+
64
+ def log(**event):
65
+ print(json.dumps(event, sort_keys=True), flush=True)
66
+
67
+
68
+ def main(argv):
69
+ parser = argparse.ArgumentParser()
70
+ parser.add_argument('--path', action='append', required=True)
71
+ parser.add_argument('--floor-mib', type=int, default=3072)
72
+ parser.add_argument('--write-mib', type=int, default=32)
73
+ parser.add_argument('--interval', type=float, default=5.0)
74
+ parser.add_argument('--cgroup-root', default='/sys/fs/cgroup/user.slice')
75
+ parser.add_argument('--dry-run', action='store_true')
76
+ args = parser.parse_args(argv)
77
+ floor, threshold = args.floor_mib * MIB, args.write_mib * MIB
78
+ log(event='started', paths=args.path, floor_mib=args.floor_mib,
79
+ write_mib=args.write_mib, interval=args.interval, dry_run=args.dry_run)
80
+ previous = leaf_writes(args.cgroup_root)
81
+ while True:
82
+ time.sleep(args.interval)
83
+ try:
84
+ current = leaf_writes(args.cgroup_root)
85
+ free = {path: free_bytes(path) for path in args.path}
86
+ low = [path for path, value in free.items() if value < floor]
87
+ victim = choose_victim(previous, current, bool(low), threshold)
88
+ previous = current
89
+ if victim is None:
90
+ continue
91
+ path, delta = victim
92
+ event = dict(event='runaway_writer', cgroup=path,
93
+ wrote_mib=delta // MIB, low=low,
94
+ free_mib={p: v // MIB for p, v in free.items()})
95
+ if not args.dry_run:
96
+ Path(path, 'cgroup.kill').write_text('1\n')
97
+ event['killed'] = True
98
+ log(**event)
99
+ except OSError as exc:
100
+ log(event='check_failed', error=type(exc).__name__)
101
+
102
+
103
+ if __name__ == '__main__':
104
+ main(sys.argv[1:])
@@ -0,0 +1,43 @@
1
+ {
2
+ "version": 1,
3
+ "release": "26.04",
4
+ "architectures": ["amd64", "arm64"],
5
+ "snapshot": "20261101T000000Z",
6
+ "packages": {
7
+ "sudo": "1.9.17p2-1ubuntu3.2",
8
+ "openssh-server": "1:10.2p1-2ubuntu3.8",
9
+ "dconf-cli": "0.49.0-4",
10
+ "docker.io": "29.1.3-0ubuntu4.1",
11
+ "docker-compose-v2": "2.40.3+ds1-0ubuntu1",
12
+ "docker-buildx": "0.30.1-0ubuntu1",
13
+ "build-essential": "12.12ubuntu2.26.04.2",
14
+ "ca-certificates": "20260816ubuntu0.26.04.2",
15
+ "curl": "8.18.0-1ubuntu2.8",
16
+ "git": "1:2.53.0-1ubuntu1.1",
17
+ "python3": "3.14.3-0ubuntu2",
18
+ "unzip": "6.0-29ubuntu1",
19
+ "xz-utils": "5.8.3-1ubuntu0.1"
20
+ },
21
+ "aptLockTimeoutSeconds": 120,
22
+ "minimumFreeDiskMiB": 4096,
23
+ "minimumFreeInodes": 10000,
24
+ "serviceStartLimitBurst": 5,
25
+ "serviceStartLimitIntervalSeconds": 300,
26
+ "alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
27
+ "resourceSafety": {
28
+ "swapFileMiB": 8192,
29
+ "userMemoryMax": "90%",
30
+ "agentMemoryMax": "30%",
31
+ "agentSwapMax": "2G",
32
+ "journalMaxUse": "1G",
33
+ "journalKeepFree": "4G",
34
+ "tmpMaxSize": "25%",
35
+ "tmpAge": "2d",
36
+ "varTmpAge": "14d",
37
+ "diskGuardFloorMiB": 3072,
38
+ "diskGuardWriteMiB": 32,
39
+ "diskGuardIntervalSeconds": 5,
40
+ "dockerLogMaxSize": "50m",
41
+ "dockerLogMaxFiles": "3"
42
+ }
43
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ - name: Enable recovery for an already verified T3 desktop app
3
+ hosts: localhost
4
+ connection: local
5
+ become: true
6
+ gather_facts: false
7
+ tasks:
8
+ - name: Verify the desktop unit runs before adding any recovery trigger
9
+ ansible.builtin.command:
10
+ argv: [systemctl, --user, is-active, t3code-desktop.service]
11
+ become_user: '{{ development_user }}'
12
+ environment:
13
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
14
+ changed_when: false
15
+ - ansible.builtin.import_tasks: tasks/recovery.yml
@@ -0,0 +1,7 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "organization": "Personal developer fleet",
4
+ "limits": {
5
+ "updates": { "mode": "off", "allowNightly": true }
6
+ }
7
+ }
@@ -0,0 +1,220 @@
1
+ ---
2
+ # The worker's only T3 process: the pinned desktop app, which embeds the server
3
+ # on 127.0.0.1:3773. GDM logs the designated user in automatically and the user
4
+ # unit starts the app with the graphical session, so the UI is on screen and the
5
+ # server is up after every boot. Starting or migrating is left to fleet-sync.
6
+ #
7
+ # The upstream .deb declares libxss1, which Ubuntu 26.04 no longer ships and the
8
+ # binary does not link. Instead of patching the package or leaving apt with an
9
+ # unmet dependency, the verified package payload is unpacked to its own path and
10
+ # its real runtime libraries and bundled AppArmor profile are installed.
11
+ - name: Install the pinned T3 Code desktop app and start it with the session
12
+ hosts: localhost
13
+ connection: local
14
+ become: true
15
+ gather_facts: false
16
+ vars:
17
+ t3_desktop_dir: /opt/T3 Code (Nightly)
18
+ # The package's Depends without libxss1 (see above).
19
+ t3_desktop_libraries:
20
+ - libasound2t64
21
+ - libatspi2.0-0t64
22
+ - libgbm1
23
+ - libgtk-3-0t64
24
+ - libnotify4
25
+ - libnss3
26
+ - libsecret-1-0
27
+ - libuuid1
28
+ - libxtst6
29
+ - xdg-utils
30
+ tasks:
31
+ - name: Create the package cache
32
+ ansible.builtin.file:
33
+ path: /var/cache/personal-fleet
34
+ state: directory
35
+ owner: root
36
+ group: root
37
+ mode: '0755'
38
+ - name: Download the pinned desktop package
39
+ ansible.builtin.get_url:
40
+ url: '{{ t3_desktop_url }}'
41
+ dest: '/var/cache/personal-fleet/{{ t3_desktop_name }}'
42
+ checksum: 'sha256:{{ t3_desktop_sha256 }}'
43
+ mode: '0644'
44
+ timeout: 300
45
+ - name: Install the desktop runtime libraries
46
+ ansible.builtin.apt:
47
+ name: '{{ t3_desktop_libraries }}'
48
+ state: present
49
+ lock_timeout: 120
50
+ - name: Read the unpacked app version
51
+ ansible.builtin.command:
52
+ argv: [cat, '{{ t3_desktop_dir }}/.personal-fleet-version']
53
+ register: t3_desktop_installed
54
+ changed_when: false
55
+ failed_when: false
56
+ - name: Unpack the pinned app when its version differs
57
+ when: t3_desktop_installed.stdout | default('') != t3_desktop_name
58
+ block:
59
+ - name: Clear the staging directory
60
+ ansible.builtin.file:
61
+ path: /opt/.t3code-staging
62
+ state: absent
63
+ - name: Unpack the verified package payload
64
+ ansible.builtin.command:
65
+ argv:
66
+ - dpkg-deb
67
+ - -x
68
+ - '/var/cache/personal-fleet/{{ t3_desktop_name }}'
69
+ - /opt/.t3code-staging
70
+ changed_when: true
71
+ - name: Record the unpacked package
72
+ ansible.builtin.copy:
73
+ dest: '/opt/.t3code-staging{{ t3_desktop_dir }}/.personal-fleet-version'
74
+ content: '{{ t3_desktop_name }}'
75
+ mode: '0644'
76
+ - name: Remove the copy kept from the previous update
77
+ ansible.builtin.file:
78
+ path: '{{ t3_desktop_dir }}.previous'
79
+ state: absent
80
+ - name: Inspect the current app directory
81
+ ansible.builtin.stat:
82
+ path: '{{ t3_desktop_dir }}'
83
+ register: t3_desktop_current
84
+ - name: Keep the current app until the next update (a running app keeps its files)
85
+ ansible.builtin.command:
86
+ argv:
87
+ [mv, -T, '{{ t3_desktop_dir }}', '{{ t3_desktop_dir }}.previous']
88
+ when: t3_desktop_current.stat.exists
89
+ changed_when: true
90
+ - name: Move the pinned app into place
91
+ ansible.builtin.command:
92
+ argv:
93
+ - mv
94
+ - -T
95
+ - '/opt/.t3code-staging{{ t3_desktop_dir }}'
96
+ - '{{ t3_desktop_dir }}'
97
+ changed_when: true
98
+ - name: Add the launcher entry and icon
99
+ ansible.builtin.copy:
100
+ src: '/opt/.t3code-staging/usr/share/{{ item }}'
101
+ dest: '/usr/share/{{ item }}'
102
+ remote_src: true
103
+ mode: '0644'
104
+ loop:
105
+ - applications/t3code.desktop
106
+ - icons/hicolor/512x512/apps/t3code.png
107
+ - name: Remove the staging directory
108
+ ansible.builtin.file:
109
+ path: /opt/.t3code-staging
110
+ state: absent
111
+ - name: Install the AppArmor profile the package ships
112
+ ansible.builtin.copy:
113
+ src: '{{ t3_desktop_dir }}/resources/apparmor-profile'
114
+ dest: /etc/apparmor.d/t3code
115
+ remote_src: true
116
+ mode: '0644'
117
+ register: t3_desktop_apparmor
118
+ - name: Load the AppArmor profile so the app sandbox can start
119
+ ansible.builtin.command:
120
+ argv:
121
+ [
122
+ apparmor_parser,
123
+ --replace,
124
+ --write-cache,
125
+ --skip-read-cache,
126
+ /etc/apparmor.d/t3code,
127
+ ]
128
+ when: t3_desktop_apparmor.changed
129
+ changed_when: true
130
+ - name: Inspect the GDM configuration
131
+ ansible.builtin.stat:
132
+ path: /etc/gdm3/custom.conf
133
+ register: gdm_config
134
+ - name: Log the designated user in automatically
135
+ ansible.builtin.lineinfile:
136
+ path: /etc/gdm3/custom.conf
137
+ regexp: '^#?\s*{{ item.key }}\s*='
138
+ line: '{{ item.key }} = {{ item.value }}'
139
+ insertafter: '^\[daemon\]'
140
+ loop:
141
+ - { key: AutomaticLoginEnable, value: 'true' }
142
+ - { key: AutomaticLogin, value: '{{ development_user }}' }
143
+ when: gdm_config.stat.exists
144
+ - name: Create the user unit directory
145
+ ansible.builtin.file:
146
+ path: '{{ development_home }}/.config/systemd/user'
147
+ state: directory
148
+ owner: '{{ development_user }}'
149
+ mode: '0700'
150
+ - name: Declare the desktop app as a session service
151
+ ansible.builtin.copy:
152
+ dest: '{{ development_home }}/.config/systemd/user/t3code-desktop.service'
153
+ owner: '{{ development_user }}'
154
+ mode: '0600'
155
+ content: |
156
+ # Managed by personal fleet-sync: ansible/ubuntu/t3-desktop.yml
157
+ [Unit]
158
+ Description=T3 Code desktop (UI and server)
159
+ PartOf=graphical-session.target
160
+ After=graphical-session.target
161
+ StartLimitIntervalSec=300
162
+ StartLimitBurst=5
163
+ [Service]
164
+ Type=simple
165
+ WorkingDirectory=%h
166
+ Environment=T3CODE_HOME=%h/.t3
167
+ # /tmp is RAM with a per-user quota that agent and build caches
168
+ # exhaust; agents use disk-backed /var/tmp (aged by tasks/resources.yml).
169
+ Environment=TMPDIR=/var/tmp
170
+ # Native Wayland hangs before the first window on Asahi (16K pages);
171
+ # XWayland works on every GNOME worker, so use it everywhere.
172
+ ExecStart="{{ t3_desktop_dir }}/t3code" --ozone-platform=x11
173
+ KillMode=mixed
174
+ Restart=always
175
+ RestartSec=5
176
+ CPUWeight=1000
177
+ IOWeight=1000
178
+ MemoryLow=1536M
179
+ OOMScoreAdjust=100
180
+ ManagedOOMPreference=avoid
181
+ [Install]
182
+ WantedBy=graphical-session.target
183
+ # Repoint recovery now: a timer still running an older revision's script
184
+ # would restart the headless server right after its approved migration.
185
+ - ansible.builtin.import_tasks: tasks/recovery.yml
186
+ - name: Enable the desktop app for every graphical login
187
+ become: true
188
+ become_user: '{{ development_user }}'
189
+ environment:
190
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
191
+ ansible.builtin.systemd_service:
192
+ scope: user
193
+ name: t3code-desktop.service
194
+ daemon_reload: true
195
+ enabled: true
196
+ # One T3 process per worker: the desktop app. The former headless server
197
+ # (`t3 service install`) is retired; absent on machines that never had it.
198
+ - name: Retire the former headless T3 server
199
+ become: true
200
+ become_user: '{{ development_user }}'
201
+ environment:
202
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
203
+ ansible.builtin.systemd_service:
204
+ scope: user
205
+ name: t3code.service
206
+ state: stopped
207
+ enabled: false
208
+ failed_when: false
209
+ # Starting needs the auto-login graphical session, which a machine has only
210
+ # after its first reboot; the running state is measured after this play.
211
+ - name: Start the desktop app without restarting a running one
212
+ become: true
213
+ become_user: '{{ development_user }}'
214
+ environment:
215
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
216
+ ansible.builtin.systemd_service:
217
+ scope: user
218
+ name: t3code-desktop.service
219
+ state: started
220
+ failed_when: false
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+ # Keep the worker T3 desktop app (UI and server) up and responsive. Run every minute by
3
+ # personal-worker-recovery.timer (managed by fleet-sync, tasks/recovery.yml).
4
+ # Stopped or failed: start it. Running but its local API has not answered for
5
+ # three checks in a row: restart it, since a hung server serves no turns.
6
+ set -u
7
+ url=http://127.0.0.1:3773/.well-known/t3/environment
8
+ state="${XDG_RUNTIME_DIR:?}/personal-t3-health-failures"
9
+ limit=3
10
+
11
+ # The former headless server still runs until its approved migration; never
12
+ # start a second T3 next to it.
13
+ if systemctl --user is-active --quiet t3code.service; then
14
+ echo "Headless T3 still runs; desktop migration pending."
15
+ exit 0
16
+ fi
17
+ if ! systemctl --user is-active --quiet t3code-desktop.service; then
18
+ rm -f "$state"
19
+ echo "T3 is not running; starting it."
20
+ exec systemctl --user start t3code-desktop.service
21
+ fi
22
+ if curl --fail --silent --max-time 10 --output /dev/null "$url"; then
23
+ rm -f "$state"
24
+ exit 0
25
+ fi
26
+ failures=$(($(cat "$state" 2>/dev/null || echo 0) + 1))
27
+ echo "$failures" > "$state"
28
+ echo "T3 local API did not answer ($failures/$limit)."
29
+ if [ "$failures" -ge "$limit" ]; then
30
+ rm -f "$state"
31
+ echo "T3 is unresponsive; restarting it."
32
+ exec systemctl --user restart t3code-desktop.service
33
+ fi
@@ -0,0 +1,76 @@
1
+ ---
2
+ - name: Stage the native T3 policy and make the running app load it
3
+ hosts: localhost
4
+ connection: local
5
+ become: true
6
+ gather_facts: false
7
+ tasks:
8
+ - name: Inspect existing administrator policy
9
+ ansible.builtin.stat:
10
+ path: /etc/t3code/policy.json
11
+ register: policy_file
12
+ - name: Inspect Fleet ownership marker
13
+ ansible.builtin.stat:
14
+ path: /etc/t3code/.personal-fleet-policy
15
+ register: policy_owner
16
+ - name: Refuse to overwrite an unrelated administrator policy
17
+ ansible.builtin.assert:
18
+ that: not policy_file.stat.exists or policy_owner.stat.exists
19
+ fail_msg: T3_POLICY_OWNER_CONFLICT; review and merge the existing administrator policy in Git before adoption.
20
+ - name: Create administrator-owned policy directory
21
+ ansible.builtin.file:
22
+ path: /etc/t3code
23
+ state: directory
24
+ owner: root
25
+ group: root
26
+ mode: '0755'
27
+ - name: Record policy ownership before resumable installation
28
+ ansible.builtin.copy:
29
+ dest: /etc/t3code/.personal-fleet-policy
30
+ content: "ansible/ubuntu/t3/policy.json\n"
31
+ owner: root
32
+ group: root
33
+ mode: '0644'
34
+ - name: Stage validated policy with backup
35
+ ansible.builtin.copy:
36
+ content: "{{ t3_policy_document | default(lookup('ansible.builtin.file', 't3/policy.json') | from_json) | to_nice_json }}"
37
+ validate: '{{ t3_executable | quote }} policy check %s'
38
+ dest: /etc/t3code/policy.json
39
+ owner: root
40
+ group: root
41
+ mode: '0644'
42
+ backup: true
43
+ # T3 reads its policy once, at startup. A running app that has not loaded
44
+ # the staged policy is restarted (this ends its running turns) so every
45
+ # node offers the same providers after every sync.
46
+ - name: Check whether the desktop app is running
47
+ become: true
48
+ become_user: '{{ development_user }}'
49
+ environment:
50
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
51
+ ansible.builtin.command:
52
+ argv: [systemctl, --user, is-active, --quiet, t3code-desktop.service]
53
+ register: t3_desktop_active
54
+ changed_when: false
55
+ failed_when: false
56
+ - name: Ask the running T3 which policy it loaded
57
+ become: true
58
+ become_user: '{{ development_user }}'
59
+ ansible.builtin.command:
60
+ argv: ['{{ t3_executable }}', policy, show]
61
+ register: t3_policy_loaded
62
+ changed_when: false
63
+ failed_when: false
64
+ when: t3_desktop_active.rc == 0
65
+ - name: Restart T3 so it loads the staged policy
66
+ become: true
67
+ become_user: '{{ development_user }}'
68
+ environment:
69
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
70
+ ansible.builtin.systemd_service:
71
+ scope: user
72
+ name: t3code-desktop.service
73
+ state: restarted
74
+ when:
75
+ - t3_desktop_active.rc == 0
76
+ - t3_policy_loaded.stdout is not search('loaded this policy\.\s*$')
@@ -0,0 +1,28 @@
1
+ ---
2
+ # The worker's T3 is reached over the tailnet only: Tailscale Serve terminates
3
+ # HTTPS on port 443 and proxies to the loopback T3 server. Never Funnel. A
4
+ # port 443 that already has any owner is left alone (reported, never reset).
5
+ - name: Serve the worker T3 privately over the tailnet
6
+ hosts: localhost
7
+ connection: local
8
+ become: true
9
+ gather_facts: false
10
+ tasks:
11
+ - name: Require the declared loopback target
12
+ ansible.builtin.assert:
13
+ that: "t3_route_target is match('^http://127\\.0\\.0\\.1:[0-9]+$')"
14
+
15
+ - name: Read this node's Serve configuration
16
+ ansible.builtin.command:
17
+ argv: [tailscale, serve, status, --json]
18
+ register: t3_serve_status
19
+ changed_when: false
20
+ - name: Publish T3 on tailnet HTTPS port 443
21
+ ansible.builtin.command:
22
+ argv: [tailscale, serve, --bg, --https=443, '{{ t3_route_target }}']
23
+ vars:
24
+ serve: "{{ t3_serve_status.stdout | default('{}', true) | from_json }}"
25
+ when:
26
+ - "'443' not in (serve.TCP | default({}))"
27
+ - "(serve.Web | default({})) | select('search', ':443$') | list | length == 0"
28
+ changed_when: true