@crvouga/dev-fleet 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/python3
2
+ """Recover disk space by deleting only what a build or install can recreate.
3
+
4
+ Managed by personal fleet-sync (tasks/resources.yml) as
5
+ personal-fleet-disk-gc.service, started hourly by personal-fleet-disk-gc.timer.
6
+
7
+ What it deletes:
8
+ * dependency and build directories (node_modules, .turbo, dist, Rust target,
9
+ ...) inside git checkouts under the home directory, only when git says the
10
+ directory is ignored, the checkout has been idle for --idle-days and no
11
+ process is running inside it;
12
+ * Docker images and build cache that no container uses and that are older
13
+ than --docker-age-hours (never containers or volumes);
14
+ * the apt package cache.
15
+ When a watched filesystem has less than --pressure-mib free, idle checkouts
16
+ qualify after --pressure-idle-hours, Docker leftovers after
17
+ --pressure-docker-age-hours, and aged entries of each --pressure-cache
18
+ directory are removed too.
19
+
20
+ It never deletes a checkout, a tracked file, uncommitted work, a container or
21
+ a volume. Every decision is one JSON line on stdout.
22
+
23
+ Usage: disk-gc.py --home H [--path P ...] [--idle-days N] [--pressure-mib N]
24
+ [--pressure-idle-hours N] [--docker-age-hours N]
25
+ [--pressure-docker-age-hours N] [--pressure-cache DIR ...]
26
+ [--max-depth N] [--proc DIR] [--home-only] [--dry-run]
27
+
28
+ --home-only skips the machine-wide Docker and apt steps.
29
+ """
30
+ import argparse
31
+ import json
32
+ import os
33
+ import shutil
34
+ import stat
35
+ import subprocess
36
+ import sys
37
+ import time
38
+ from pathlib import Path
39
+
40
+ MIB = 1024 * 1024
41
+ HOUR = 3600
42
+ # Directory names that hold only installed dependencies or build output.
43
+ REGENERABLE = frozenset({
44
+ 'node_modules', '.cache', '.turbo', '.next', '.nuxt', '.svelte-kit', '.astro',
45
+ '.vite', '.parcel-cache', '.expo', '.gradle', 'dist', 'build', 'coverage',
46
+ '.pytest_cache', '.mypy_cache', '.ruff_cache', '__pycache__', '.venv',
47
+ })
48
+ # Directory names that are regenerable only beside the manifest that builds them.
49
+ REGENERABLE_BESIDE = {'target': 'Cargo.toml'}
50
+ # Hidden directories under the home directory that still hold checkouts.
51
+ HIDDEN_ROOTS = frozenset({'.t3'})
52
+ GIT = ['git', '-c', 'safe.directory=*']
53
+
54
+
55
+ def log(**event):
56
+ print(json.dumps(event, sort_keys=True), flush=True)
57
+
58
+
59
+ def free_bytes(path):
60
+ st = os.statvfs(path)
61
+ return st.f_bavail * st.f_frsize
62
+
63
+
64
+ def mtime(path):
65
+ try:
66
+ return os.lstat(path).st_mtime
67
+ except OSError:
68
+ return 0.0
69
+
70
+
71
+ def is_real_dir(path):
72
+ try:
73
+ return stat.S_ISDIR(os.lstat(path).st_mode)
74
+ except OSError:
75
+ return False
76
+
77
+
78
+ def find_checkouts(home, max_depth):
79
+ """Git checkouts (repositories and worktrees) under home, outermost only."""
80
+ found = []
81
+
82
+ def walk(directory, depth):
83
+ if os.path.lexists(os.path.join(directory, '.git')):
84
+ found.append(directory)
85
+ return
86
+ if depth >= max_depth:
87
+ return
88
+ try:
89
+ entries = list(os.scandir(directory))
90
+ except OSError:
91
+ return
92
+ for entry in entries:
93
+ if not entry.is_dir(follow_symlinks=False):
94
+ continue
95
+ name = entry.name
96
+ if name in REGENERABLE or name in REGENERABLE_BESIDE:
97
+ continue
98
+ if name.startswith('.') and not (depth == 0 and name in HIDDEN_ROOTS):
99
+ continue
100
+ walk(entry.path, depth + 1)
101
+
102
+ walk(home, 0)
103
+ return sorted(found)
104
+
105
+
106
+ def git_dir(checkout):
107
+ """The checkout's git directory, following a worktree's .git file."""
108
+ dot_git = os.path.join(checkout, '.git')
109
+ if is_real_dir(dot_git):
110
+ return dot_git
111
+ try:
112
+ text = Path(dot_git).read_text()
113
+ except OSError:
114
+ return None
115
+ if not text.startswith('gitdir:'):
116
+ return None
117
+ target = text[len('gitdir:'):].strip()
118
+ target = os.path.normpath(os.path.join(checkout, target))
119
+ return target if is_real_dir(target) else None
120
+
121
+
122
+ def run_git(checkout, args, data=None):
123
+ try:
124
+ out = subprocess.run(GIT + ['-C', checkout] + args, input=data,
125
+ capture_output=True, timeout=600)
126
+ except (OSError, subprocess.TimeoutExpired):
127
+ return None
128
+ return out
129
+
130
+
131
+ def candidates(checkout):
132
+ """Regenerable directories in a checkout, outermost only, never through a link."""
133
+ found = []
134
+ for current, dirs, files in os.walk(checkout):
135
+ keep = []
136
+ for name in dirs:
137
+ path = os.path.join(current, name)
138
+ if name == '.git' or os.path.islink(path):
139
+ continue
140
+ if os.path.lexists(os.path.join(path, '.git')):
141
+ continue # a nested repository answers for itself
142
+ beside = REGENERABLE_BESIDE.get(name)
143
+ if name in REGENERABLE or (beside and beside in files):
144
+ found.append(path)
145
+ continue
146
+ keep.append(name)
147
+ dirs[:] = keep
148
+ return found
149
+
150
+
151
+ def ignored(checkout, paths):
152
+ """The subset of paths git ignores; tracked or unignored paths are kept."""
153
+ if not paths:
154
+ return []
155
+ rel = [os.path.relpath(p, checkout) for p in paths]
156
+ out = run_git(checkout, ['check-ignore', '--stdin', '-z'],
157
+ data=('\0'.join(rel) + '\0').encode())
158
+ if out is None or out.returncode not in (0, 1):
159
+ return []
160
+ hits = {item for item in out.stdout.decode(errors='replace').split('\0') if item}
161
+ return [p for p, r in zip(paths, rel) if r in hits or r.rstrip('/') in hits]
162
+
163
+
164
+ def git_activity(checkout, gdir):
165
+ """The latest commit, checkout or index refresh: cheap, read first."""
166
+ marks = [checkout, os.path.join(gdir, 'index'), os.path.join(gdir, 'HEAD'),
167
+ os.path.join(gdir, 'logs', 'HEAD')]
168
+ return max(mtime(p) for p in marks)
169
+
170
+
171
+ def last_activity(checkout, gdir, dirs):
172
+ """The latest sign of work: git activity, changed files, build output writes."""
173
+ latest = max([git_activity(checkout, gdir)] + [mtime(p) for p in dirs])
174
+ out = run_git(checkout, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
175
+ if out is None or out.returncode != 0:
176
+ return None
177
+ entries = out.stdout.decode(errors='replace').split('\0')
178
+ for entry in entries:
179
+ if len(entry) > 3:
180
+ latest = max(latest, mtime(os.path.join(checkout, entry[3:])))
181
+ return latest
182
+
183
+
184
+ def busy_roots(proc):
185
+ """Working directories of every running process."""
186
+ roots = set()
187
+ try:
188
+ pids = [p for p in os.listdir(proc) if p.isdigit()]
189
+ except OSError:
190
+ return roots
191
+ for pid in pids:
192
+ try:
193
+ roots.add(os.readlink(os.path.join(proc, pid, 'cwd')))
194
+ except OSError:
195
+ continue
196
+ return roots
197
+
198
+
199
+ def in_use(checkout, roots):
200
+ prefix = checkout.rstrip('/') + '/'
201
+ return any(r == checkout or r.startswith(prefix) for r in roots)
202
+
203
+
204
+ def size_of(path):
205
+ total = 0
206
+ for current, dirs, files in os.walk(path):
207
+ for name in dirs + files:
208
+ try:
209
+ total += os.lstat(os.path.join(current, name)).st_blocks * 512
210
+ except OSError:
211
+ continue
212
+ return total
213
+
214
+
215
+ def remove(path, dry_run):
216
+ """Deletes one directory without following links; returns bytes freed."""
217
+ if not is_real_dir(path):
218
+ return 0
219
+ size = size_of(path)
220
+ if not dry_run:
221
+ shutil.rmtree(path, ignore_errors=True)
222
+ if os.path.lexists(path):
223
+ return 0
224
+ return size
225
+
226
+
227
+ def collect_checkouts(home, idle_seconds, max_depth, proc, now, dry_run):
228
+ roots = busy_roots(proc)
229
+ freed = 0
230
+ for checkout in find_checkouts(home, max_depth):
231
+ gdir = git_dir(checkout)
232
+ if gdir is None:
233
+ log(event='checkout_skipped', checkout=checkout, reason='no_git_dir')
234
+ continue
235
+ if in_use(checkout, roots) or now - git_activity(checkout, gdir) < idle_seconds:
236
+ continue
237
+ dirs = ignored(checkout, candidates(checkout))
238
+ if not dirs:
239
+ continue
240
+ latest = last_activity(checkout, gdir, dirs)
241
+ if latest is None:
242
+ log(event='checkout_skipped', checkout=checkout, reason='git_status_failed')
243
+ continue
244
+ if now - latest < idle_seconds:
245
+ continue
246
+ for path in dirs:
247
+ got = remove(path, dry_run)
248
+ freed += got
249
+ log(event='removed', path=path, mib=got // MIB, dry_run=dry_run)
250
+ return freed
251
+
252
+
253
+ def collect_cache(directory, age_seconds, now, dry_run):
254
+ """Removes top-level cache entries not written for age_seconds."""
255
+ freed = 0
256
+ try:
257
+ entries = list(os.scandir(directory))
258
+ except OSError:
259
+ return 0
260
+ for entry in entries:
261
+ if now - mtime(entry.path) < age_seconds:
262
+ continue
263
+ if entry.is_dir(follow_symlinks=False):
264
+ got = remove(entry.path, dry_run)
265
+ elif entry.is_file(follow_symlinks=False):
266
+ got = entry.stat(follow_symlinks=False).st_blocks * 512
267
+ if not dry_run:
268
+ try:
269
+ os.unlink(entry.path)
270
+ except OSError:
271
+ got = 0
272
+ else:
273
+ continue
274
+ freed += got
275
+ if freed:
276
+ log(event='cache_pruned', cache=directory, mib=freed // MIB, dry_run=dry_run)
277
+ return freed
278
+
279
+
280
+ def command(argv, dry_run, name):
281
+ if shutil.which(argv[0]) is None:
282
+ return
283
+ if dry_run:
284
+ log(event='would_run', step=name)
285
+ return
286
+ try:
287
+ out = subprocess.run(argv, capture_output=True, timeout=1800)
288
+ log(event='ran', step=name, ok=out.returncode == 0)
289
+ except (OSError, subprocess.TimeoutExpired) as exc:
290
+ log(event='ran', step=name, ok=False, error=type(exc).__name__)
291
+
292
+
293
+ def main(argv):
294
+ parser = argparse.ArgumentParser()
295
+ parser.add_argument('--home', required=True)
296
+ parser.add_argument('--path', action='append', default=[])
297
+ parser.add_argument('--idle-days', type=float, default=7)
298
+ parser.add_argument('--pressure-mib', type=int, default=15360)
299
+ parser.add_argument('--pressure-idle-hours', type=float, default=12)
300
+ parser.add_argument('--docker-age-hours', type=int, default=168)
301
+ parser.add_argument('--pressure-docker-age-hours', type=int, default=24)
302
+ parser.add_argument('--pressure-cache', action='append', default=[])
303
+ parser.add_argument('--max-depth', type=int, default=4)
304
+ parser.add_argument('--proc', default='/proc')
305
+ parser.add_argument('--home-only', action='store_true')
306
+ parser.add_argument('--dry-run', action='store_true')
307
+ args = parser.parse_args(argv)
308
+ home = os.path.realpath(args.home)
309
+ paths = args.path or ['/', home]
310
+ now = time.time()
311
+ before = {p: free_bytes(p) for p in paths}
312
+ pressure = min(before.values()) < args.pressure_mib * MIB
313
+ idle = args.pressure_idle_hours * HOUR if pressure else args.idle_days * 24 * HOUR
314
+ docker_age = args.pressure_docker_age_hours if pressure else args.docker_age_hours
315
+ log(event='started', pressure=pressure, idle_hours=idle / HOUR,
316
+ docker_age_hours=docker_age, dry_run=args.dry_run,
317
+ free_mib={p: v // MIB for p, v in before.items()})
318
+
319
+ freed = collect_checkouts(home, idle, args.max_depth, args.proc, now, args.dry_run)
320
+ if pressure:
321
+ for cache in args.pressure_cache:
322
+ freed += collect_cache(os.path.join(home, cache), idle, now, args.dry_run)
323
+ if not args.home_only:
324
+ command(['docker', 'image', 'prune', '--all', '--force', '--filter',
325
+ f'until={docker_age}h'], args.dry_run, 'docker_images')
326
+ command(['docker', 'builder', 'prune', '--all', '--force', '--filter',
327
+ f'until={docker_age}h'], args.dry_run, 'docker_build_cache')
328
+ command(['apt-get', 'clean'], args.dry_run, 'apt_cache')
329
+
330
+ after = {p: free_bytes(p) for p in paths}
331
+ log(event='finished', checkout_mib=freed // MIB, dry_run=args.dry_run,
332
+ free_mib={p: v // MIB for p, v in after.items()})
333
+
334
+
335
+ if __name__ == '__main__':
336
+ main(sys.argv[1:])
@@ -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,52 @@
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
+ "ansible-core": "2.20.1-1"
21
+ },
22
+ "tailscale": {
23
+ "version": "1.104.5",
24
+ "keySha256": "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"
25
+ },
26
+ "aptLockTimeoutSeconds": 120,
27
+ "minimumFreeDiskMiB": 4096,
28
+ "minimumFreeInodes": 10000,
29
+ "alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
30
+ "resourceSafety": {
31
+ "swapFileMiB": 8192,
32
+ "userMemoryMax": "90%",
33
+ "agentMemoryMax": "30%",
34
+ "agentSwapMax": "2G",
35
+ "journalMaxUse": "1G",
36
+ "journalKeepFree": "4G",
37
+ "tmpMaxSize": "25%",
38
+ "tmpAge": "2d",
39
+ "varTmpAge": "14d",
40
+ "diskGuardFloorMiB": 3072,
41
+ "diskGuardWriteMiB": 32,
42
+ "diskGuardIntervalSeconds": 5,
43
+ "diskGcIdleDays": 7,
44
+ "diskGcPressureMiB": 15360,
45
+ "diskGcPressureIdleHours": 12,
46
+ "diskGcDockerAgeHours": 168,
47
+ "diskGcPressureDockerAgeHours": 24,
48
+ "diskGcPressureCaches": [".bun/install/cache"],
49
+ "dockerLogMaxSize": "50m",
50
+ "dockerLogMaxFiles": "3"
51
+ }
52
+ }
@@ -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
+ }