@crvouga/dev-fleet 0.7.0 → 0.9.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 (206) hide show
  1. package/README.md +88 -7
  2. package/agents/audit.md +18 -18
  3. package/ansible/agents/agents.yml +134 -0
  4. package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
  5. package/ansible/agents/templates/claude.settings.json.j2 +3 -0
  6. package/ansible/agents/templates/codex.config.toml.j2 +11 -0
  7. package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
  8. package/ansible/agents/templates/grok.config.toml.j2 +19 -0
  9. package/ansible/agents/templates/opencode.json.j2 +24 -0
  10. package/ansible/agents/templates/receipt.json.j2 +10 -0
  11. package/ansible/ansible.cfg +14 -0
  12. package/ansible/github/git.yml +82 -0
  13. package/ansible/macos/manager-privilege.yml +83 -0
  14. package/ansible/macos/manager.yml +164 -9
  15. package/ansible/macos/policy.json +13 -0
  16. package/ansible/macos/t3-policy.yml +5 -3
  17. package/ansible/macos/vault-renew.yml +85 -0
  18. package/ansible/macos/worker-host-key.yml +33 -0
  19. package/ansible/monitoring/agent-darwin.yml +3 -0
  20. package/ansible/monitoring/agent.yml +2 -0
  21. package/ansible/monitoring/hub-darwin.yml +3 -0
  22. package/ansible/monitoring/hub-route-darwin.yml +1 -0
  23. package/ansible/net/remote-probe.sh +4 -1
  24. package/ansible/platforms.json +2 -2
  25. package/ansible/pyproject.toml +5 -0
  26. package/ansible/ubuntu/baseline.yml +39 -13
  27. package/ansible/ubuntu/disk-gc.py +6 -2
  28. package/ansible/ubuntu/firewall.nft.j2 +50 -0
  29. package/ansible/ubuntu/policy.json +125 -6
  30. package/ansible/ubuntu/reboot.yml +89 -0
  31. package/ansible/ubuntu/recovery.yml +13 -3
  32. package/ansible/ubuntu/t3/policy.json +3 -0
  33. package/ansible/ubuntu/t3-desktop.yml +23 -2
  34. package/ansible/ubuntu/t3-headless.yml +128 -0
  35. package/ansible/ubuntu/t3-health.sh +24 -10
  36. package/ansible/ubuntu/t3-idle.py +37 -2
  37. package/ansible/ubuntu/t3-policy.yml +4 -1
  38. package/ansible/ubuntu/t3-route.yml +1 -0
  39. package/ansible/ubuntu/tailscale.yml +54 -8
  40. package/ansible/ubuntu/tasks/browser.yml +3 -0
  41. package/ansible/ubuntu/tasks/containers-off.yml +43 -0
  42. package/ansible/ubuntu/tasks/docker.yml +53 -6
  43. package/ansible/ubuntu/tasks/dpkg.yml +4 -0
  44. package/ansible/ubuntu/tasks/features.yml +22 -0
  45. package/ansible/ubuntu/tasks/firewall.yml +172 -0
  46. package/ansible/ubuntu/tasks/install.yml +2 -1
  47. package/ansible/ubuntu/tasks/packages.yml +4 -2
  48. package/ansible/ubuntu/tasks/release.yml +6 -0
  49. package/ansible/ubuntu/tasks/resources.yml +6 -4
  50. package/ansible/ubuntu/tasks/services.yml +2 -2
  51. package/ansible/ubuntu/tasks/sizing.yml +49 -0
  52. package/ansible/ubuntu/tasks/snapshot.yml +14 -3
  53. package/ansible/ubuntu/tasks/ssh.yml +61 -4
  54. package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
  55. package/ansible/ubuntu/tasks/user.yml +1 -0
  56. package/ansible/ubuntu/worker-account.yml +117 -0
  57. package/ansible/uv.lock +265 -0
  58. package/bin/devfleet.mjs +42 -4
  59. package/bootstrap/fleet-remote-bootstrap.sh +91 -25
  60. package/bootstrap/global-config.ts +29 -3
  61. package/bootstrap/package-smoke.ts +127 -0
  62. package/bootstrap/pins.ts +189 -0
  63. package/bootstrap/release-notes.ts +88 -0
  64. package/bootstrap/release-version.ts +41 -7
  65. package/bootstrap/worker-features.ts +114 -0
  66. package/bootstrap/worker-seed.yaml +65 -0
  67. package/bootstrap/worker-tools.ts +39 -9
  68. package/cliproxy/README.md +1 -1
  69. package/cliproxy/docs/health-workflow.example.yml +20 -143
  70. package/docs/authentication.md +21 -11
  71. package/docs/consumer-setup.md +74 -58
  72. package/docs/convergence.md +67 -0
  73. package/docs/declared.md +61 -34
  74. package/docs/machine-setup.md +73 -7
  75. package/docs/manager-setup.md +34 -0
  76. package/docs/operations.md +64 -9
  77. package/docs/releasing.md +49 -1
  78. package/docs/runbooks.md +112 -7
  79. package/docs/state.md +23 -11
  80. package/docs/support-matrix.md +40 -4
  81. package/docs/topology.md +82 -12
  82. package/docs/ubuntu-sync.md +98 -42
  83. package/docs/worker-contract.md +122 -19
  84. package/docs/worker-requirements.md +107 -0
  85. package/examples/fleet.json +1 -0
  86. package/examples/workers-from-infrastructure/main.tf +132 -0
  87. package/examples/workers-from-infrastructure/worker-records.json +14 -0
  88. package/llms.txt +3 -1
  89. package/mise.lock +29 -0
  90. package/mise.toml +5 -3
  91. package/package.json +2 -2
  92. package/schema/check-ids.json +1991 -162
  93. package/schema/check-ids.schema.json +34 -0
  94. package/schema/fleet.schema.json +349 -10
  95. package/schema/output/check.schema.json +1 -1
  96. package/schema/output/fleet-state-definition.schema.json +14 -0
  97. package/schema/output/fleet-sync-schedule.schema.json +26 -0
  98. package/schema/output/fleet-sync.schema.json +18 -0
  99. package/schema/output/report.schema.json +3 -2
  100. package/schema/worker-record.schema.json +76 -0
  101. package/src/agents/converge.ts +140 -24
  102. package/src/agents/files.ts +2 -32
  103. package/src/agents/native-verify.ts +34 -3
  104. package/src/agents/secrets.ts +39 -4
  105. package/src/check-owners.ts +62 -2
  106. package/src/check-registry.ts +630 -0
  107. package/src/cli/agent-guide.ts +3 -3
  108. package/src/cli/cli.ts +4 -0
  109. package/src/cli/fleet-manifest.ts +108 -0
  110. package/src/cli/fleet-node-lifecycle.ts +704 -0
  111. package/src/cli/fleet-node-park.ts +238 -0
  112. package/src/cli/fleet-node-seed.ts +167 -0
  113. package/src/cli/fleet-state.ts +2 -1
  114. package/src/cli/fleet-sync-checkout.ts +7 -0
  115. package/src/cli/fleet-sync-enrollment.ts +193 -32
  116. package/src/cli/fleet-sync-forward.ts +6 -0
  117. package/src/cli/fleet-sync-manager.ts +123 -8
  118. package/src/cli/fleet-sync-output.ts +31 -11
  119. package/src/cli/fleet-sync-plan.ts +65 -9
  120. package/src/cli/fleet-sync-records.ts +113 -0
  121. package/src/cli/fleet-sync-remote.ts +172 -15
  122. package/src/cli/fleet-sync-schedule.ts +14 -4
  123. package/src/cli/fleet-sync-state.ts +31 -10
  124. package/src/cli/fleet-sync-vault.ts +152 -26
  125. package/src/cli/fleet-sync-worker.ts +214 -0
  126. package/src/cli/fleet-sync.ts +172 -21
  127. package/src/cli/fleet-validate.ts +277 -0
  128. package/src/cli/fleet-watch.ts +7 -1
  129. package/src/cli/fleet.ts +87 -5
  130. package/src/cli/managed-health.ts +25 -1
  131. package/src/cli/register-fleet.ts +251 -4
  132. package/src/cli/registry.ts +47 -0
  133. package/src/cli/tailnet.ts +43 -10
  134. package/src/cli/ui-data.ts +3 -1
  135. package/src/cli/worker-usage-report.ts +63 -0
  136. package/src/clock.ts +42 -0
  137. package/src/definition.ts +762 -232
  138. package/src/diagnostics.ts +5 -2
  139. package/src/exposure.ts +102 -0
  140. package/src/fleet-config.ts +3 -1
  141. package/src/git-identity.ts +76 -3
  142. package/src/ide/ide.ts +5 -0
  143. package/src/ide/t3.ts +12 -2
  144. package/src/input-hashes.ts +6 -0
  145. package/src/machine/machine-class.ts +59 -0
  146. package/src/machine/platform.ts +90 -11
  147. package/src/machine/state.ts +70 -16
  148. package/src/machine/support-matrix.ts +11 -9
  149. package/src/machine-health.ts +28 -5
  150. package/src/manager-health.ts +148 -2
  151. package/src/manager-sync.ts +89 -8
  152. package/src/monitoring/beszel.ts +8 -8
  153. package/src/monitoring/enroll.ts +8 -1
  154. package/src/monitoring/hub-access.ts +10 -4
  155. package/src/monitoring/systems.ts +138 -0
  156. package/src/network-definition.ts +13 -4
  157. package/src/node-converge-linux.ts +136 -27
  158. package/src/node-converge.ts +13 -3
  159. package/src/platform/ansible-receipt.ts +17 -2
  160. package/src/platform/ansible.ts +40 -4
  161. package/src/platform/manager-host.ts +25 -8
  162. package/src/platform/resource-verify.ts +18 -7
  163. package/src/platform/t3-policy.ts +24 -10
  164. package/src/platform/t3-service.ts +109 -34
  165. package/src/platform/ubuntu-release.ts +24 -0
  166. package/src/platform/ubuntu-verify.ts +31 -3
  167. package/src/platform/ubuntu-worker.ts +58 -0
  168. package/src/platform/worker-platform.ts +72 -0
  169. package/src/protocol.ts +1 -1
  170. package/src/public-reachability.ts +186 -0
  171. package/src/remote-bootstrap.ts +50 -12
  172. package/src/remote-operation.ts +18 -4
  173. package/src/repair-plan.ts +23 -12
  174. package/src/report.ts +2 -1
  175. package/src/schedule-check.ts +21 -2
  176. package/src/secrets.ts +7 -0
  177. package/src/service-auth.ts +55 -48
  178. package/src/ssh-connection.ts +141 -0
  179. package/src/ssh.ts +63 -24
  180. package/src/state/store.ts +4 -2
  181. package/src/state/sync.ts +35 -6
  182. package/src/state/workers.ts +140 -0
  183. package/src/t3/pairing.ts +59 -0
  184. package/src/t3/peer-link.ts +333 -0
  185. package/src/t3/version.ts +1 -1
  186. package/src/tailnet-cleanup.ts +185 -0
  187. package/src/tailnet-control.ts +346 -0
  188. package/src/tailscale.ts +1 -1
  189. package/src/toolchain-checks.ts +11 -1
  190. package/src/types.ts +4 -2
  191. package/src/worker-account.ts +63 -0
  192. package/src/worker-activity.ts +303 -0
  193. package/src/worker-capacity.ts +31 -0
  194. package/src/worker-credentials.ts +201 -0
  195. package/src/worker-exposure.ts +388 -0
  196. package/src/worker-features.ts +201 -0
  197. package/src/worker-host-checks.ts +71 -20
  198. package/src/worker-manifest.ts +224 -0
  199. package/src/worker-parking.ts +70 -0
  200. package/src/worker-policy.ts +38 -36
  201. package/src/worker-readiness.ts +15 -3
  202. package/src/worker-reboot.ts +129 -0
  203. package/src/worker-records.ts +305 -0
  204. package/src/worker-seed.ts +143 -0
  205. package/src/worker-sizing.ts +76 -0
  206. package/src/cli/schedule-logs.ts +0 -36
@@ -1,6 +1,7 @@
1
1
  > This document was written for the fleet dev-fleet was extracted from. Paths and
2
2
  > commands were updated; some statements still describe that fleet and are being
3
- > revised. The README is authoritative.
3
+ > revised. The README is authoritative, and what a machine must offer to be a
4
+ > worker is listed only in [worker requirements](worker-requirements.md).
4
5
 
5
6
  # Declarative remote workers
6
7
 
@@ -12,11 +13,16 @@ devfleet fleet-status --workers
12
13
  ```
13
14
 
14
15
  The manager needs Node/npm, Git, SSH, Tailscale, and its own authorized T3 link.
15
- Initially each worker (an Ubuntu release the [support matrix](support-matrix.md)
16
- lists) needs a
17
- designated non-root account, trusted SSH access and administrator authorization. Standard Ubuntu cloud-image users
16
+ What each worker must offer beforehand is listed once, in
17
+ [worker requirements](worker-requirements.md). Standard Ubuntu cloud-image users
18
18
  already commonly have noninteractive sudo; otherwise establish the repository's
19
- `authorize-sudo.sh` policy once during administrator enrollment. Routine commands
19
+ `authorize-sudo.sh` policy once during administrator enrollment. An image that
20
+ lets only root in (many rented servers) needs no hands either: when the
21
+ account `workers[].network.user` names cannot log in yet, or the SSH alias
22
+ logs in as root, `fleet-sync` creates that account once over the root login
23
+ (`ansible/ubuntu/worker-account.yml`: the key root was reached with and the
24
+ fleet's sudo rule) and continues as it. The developer toolchain never runs as
25
+ root, and the SSH policy turns root login off afterwards. Routine commands
20
26
  never ask for or store an SSH/sudo password. Unknown host keys, a different device
21
27
  identity, or missing privilege stop with a named action rather than bypassing trust.
22
28
 
@@ -27,18 +33,110 @@ Ansible policy. Developer checkouts and dirty work stay intact. A published CLI
27
33
  uses its source provenance; a source project action explicitly selects checkout
28
34
  HEAD. Private rosters contain connection paths and identities, not weaker policy.
29
35
 
30
- | Requirement | Declaration and enforcement |
31
- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
- | One consistent dependency set | `mise.toml` worker profile, `mise.lock`, Ubuntu snapshot/package pins; exact versions and deployed input hashes checked live |
33
- | Common service access | Canonical required capabilities; declared GitHub account, worker's own renewable Vault session, authenticated singleton gateway catalog |
34
- | One gateway host | The CLI proxy runs only on the manager; ownership/audit/recovery code rejects any install on a worker and missing singleton state |
35
- | T3 connected and persistent | Pinned desktop app as the only T3 process, auto-login session unit enabled/active with restart policy, bounded recovery timer, private endpoint, manager link matched to SSH-observed environment identity |
36
- | No idle sleep and session survival | systemd sleep masks, logind policy, designated-user lingering; running policy and boot timing checked, not inferred from files alone |
37
- | Recovery after power loss | Required boot services and recovery timers; recognized firmware restore-after-AC settings inspected when exposed; macOS `autorestart` where supported |
38
- | Always-on services | `Restart=always` drop-ins for ssh, tailscaled, docker and beszel-agent (launchd `KeepAlive` on macOS); health requires the running unit to report that policy |
39
- | Central dashboard | Pinned Beszel hub on the manager, every worker enrolled with its own token; health verifies hub reachability, registration, connection and fresh samples |
40
- | No routine password prompts | Verified BatchMode SSH, `sudo -n`, source-owned NOPASSWD policy; missing initial authorization is a failure |
41
- | Keep service authorization alive | Native service credentials stay per-node; Vault self-renewal timer and GitHub recovery from authorized Vault credentials; expiring T3 links are reported before expiry |
36
+ | Requirement | Declaration and enforcement |
37
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | One consistent dependency set | `mise.toml` worker profile, `mise.lock`, Ubuntu snapshot/package pins; exact versions and deployed input hashes checked live |
39
+ | Common service access | Canonical required capabilities; declared GitHub account, worker's own renewable Vault session, authenticated singleton gateway catalog |
40
+ | One gateway host | The CLI proxy runs only on the manager; ownership/audit/recovery code rejects any install on a worker and missing singleton state |
41
+ | T3 connected and persistent | One pinned T3 process: the desktop app in the auto-login session, or on a headless worker the pinned server as a lingering user unit; enabled/active with restart policy, bounded recovery timer, private endpoint, manager link matched to SSH-observed environment identity |
42
+ | No idle sleep and session survival | systemd sleep masks, logind policy, designated-user lingering; running policy and boot timing checked, not inferred from files alone |
43
+ | Recovery after power loss | Required boot services and recovery timers; recognized firmware restore-after-AC settings inspected when exposed (not applicable on a virtual machine); macOS `autorestart` where supported |
44
+ | Always-on services | `Restart=always` drop-ins for ssh, tailscaled, docker (with `containers`) and beszel-agent (launchd `KeepAlive` on macOS); health requires the running unit to report that policy |
45
+ | Central dashboard | Pinned Beszel hub on the manager, every worker enrolled with its own token; health verifies hub reachability, registration, connection and fresh samples |
46
+ | No routine password prompts | Verified BatchMode SSH, `sudo -n`, source-owned NOPASSWD policy; missing initial authorization is a failure |
47
+ | Keep service authorization alive | Native service credentials stay per-node; Vault self-renewal timer and GitHub recovery from authorized Vault credentials; expiring T3 links are reported before expiry |
48
+
49
+ ## Worker features
50
+
51
+ What a worker is built with beyond the base is declared in `fleet.json`:
52
+ fleet-wide under `worker.features`, or for one worker under its own
53
+ `features`. Each block may name a `preset` first; its own fields override
54
+ it. A fleet that names no feature gets what every worker had before features
55
+ existed.
56
+
57
+ | Feature | Values | Default | What it decides |
58
+ | ------------ | --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `containers` | `true`, `false` | `true` | Docker: the Ubuntu packages, `docker.service`, the developer's `docker` group membership, the `docker-cli` tool and the Docker step of the disk GC |
60
+ | `session` | `desktop`, `headless` | `desktop` | How T3 runs: the desktop app in an auto-login graphical session, or the pinned server as a lingering user service with no display manager |
61
+ | `preset` | `editing` | none | `editing`: no containers, a headless session and only the base tools (it adds no tool profile) |
62
+
63
+ What each feature installs is listed once, under `features` in
64
+ `ansible/ubuntu/policy.json`. A worker without it installs none of it, and its
65
+ checks (`worker:package:*`, `worker:service:docker`, `worker:docker-run`,
66
+ `tool:docker-cli`) report **not applicable** with the reason. Turning
67
+ `containers` off on an existing worker removes what dev-fleet installed for it:
68
+ the packages (not purged: images and volumes under `/var/lib/docker` stay
69
+ until a person deletes them), the restart drop-in and the developer's group
70
+ membership. `fleet-sync --dry-run` lists what each worker will lose (`plan`,
71
+ under `removals`), and `worker:features` reports what the run removed and
72
+ fails with `WORKER_FEATURE_REMOVAL_INCOMPLETE` while any of it remains. A
73
+ headless worker installs no desktop-only package but removes none: a desktop
74
+ the machine came with may depend on them.
75
+
76
+ ## The class of a machine
77
+
78
+ The probe records each worker's class before anything is installed
79
+ (`platform:machine-class`): **physical**, **virtual** (systemd-detect-virt
80
+ names a hypervisor) or unsupported (a container or WSL2, refused by
81
+ `platform:environment`). A virtual machine has no lid, no battery and no
82
+ firmware its guest can read, and its provider restarts it after a power loss,
83
+ so `worker:power-loss-recovery` and `ubuntu:lid-policy` report **not
84
+ applicable**, with the reason and the worker's optional `powerRecoveryNote`
85
+ (for example "provider restarts instances"). The sleep masks are still
86
+ checked: a guest can be suspended by its own idle policy. On a physical
87
+ machine nothing changes. A not-applicable check is counted apart, shown with
88
+ its reason in text and in `--json`, and never fails a run, not even with
89
+ `--strict`.
90
+
91
+ ## Size
92
+
93
+ The minimum size of a worker for the editing workload is stated in
94
+ [worker requirements](worker-requirements.md), from `sizing.minimumWorker` in
95
+ `ansible/ubuntu/policy.json`. The probe measures it before anything is
96
+ installed and refuses a smaller machine with `WORKER_SIZE_BELOW_MINIMUM`
97
+ (`platform:worker-size`); the baseline asserts the same before it installs a
98
+ package.
99
+
100
+ The disk dev-fleet itself installs is measured on every pull request: the
101
+ fresh-box CI job converges a stock Ubuntu image of each accepted release on
102
+ amd64 and arm64, measures what each part takes, and fails when a part
103
+ outgrows its budget in `sizing.footprintMiB`
104
+ (`features/fresh-box/fresh-box.feature`). Measured on Ubuntu 26.04:
105
+
106
+ | Part | amd64 | arm64 | Budget |
107
+ | -------------------------------------------- | ------------ | ------------ | -------- |
108
+ | Base tools with the agent CLIs, and `go` | 5184 MiB | 4830 MiB | 6144 MiB |
109
+ | The fleet's checkouts (up to four revisions) | 189 MiB each | 189 MiB each | 1024 MiB |
110
+ | Tool profile `web` (its tools) | | 135 MiB | 256 MiB |
111
+ | Tool profile `rust` | | 1562 MiB | 2048 MiB |
112
+ | Tool profile `jvm` | 437 MiB | 303 MiB | 640 MiB |
113
+
114
+ What is not measured there, and stays an estimate: Ubuntu Server itself
115
+ (about 3 to 5 GiB), the system packages of the baseline, the headless
116
+ Chromium the `web` profile installs as packages, the swap file (half of
117
+ memory), and the engine and images of `containers`. The minimum adds those
118
+ to the measured parts with room for the disk floors below.
119
+
120
+ The disk and memory lines of the worker policy scale with the machine: each
121
+ is a share of the root filesystem or of memory between a lower and an upper
122
+ bound (`sizing.thresholds`), and the upper bounds are the fixed numbers every
123
+ worker had before.
124
+
125
+ | Line | Share | Lower | Upper |
126
+ | ---------------------------------- | ------------- | -------- | --------- |
127
+ | Swap file (created only when none) | 50% of memory | 1024 MiB | 8192 MiB |
128
+ | Free disk the baseline keeps | 5% of disk | 2048 MiB | 4096 MiB |
129
+ | Disk guard floor | 3% of disk | 1024 MiB | 3072 MiB |
130
+ | Disk GC pressure line | 15% of disk | 4096 MiB | 15360 MiB |
131
+ | `worker:capacity:disk` warns under | 10% of disk | 3072 MiB | 10240 MiB |
132
+ | `worker:capacity:disk` fails under | 2% of disk | 1024 MiB | 2048 MiB |
133
+
134
+ `worker:agent-sessions` reports how many agent sessions run and how many the
135
+ memory policy supports at once: the memory user workloads may use (90%), less
136
+ what T3 keeps for itself (1536 MiB), in sessions of an estimated 1024 MiB each
137
+ (`sizing.agentSessionMiB`, also an estimate). More sessions than that warn
138
+ with `AGENT_SESSION_CAPACITY_EXHAUSTED`; each agent is still capped at 30% of
139
+ memory, so the largest is stopped under pressure rather than the machine.
42
140
 
43
141
  ## Package pins and security updates
44
142
 
@@ -64,12 +162,17 @@ one:
64
162
  A package run cut off by a power loss is finished by the next sync: the
65
163
  baseline waits for the APT lock, runs `dpkg --configure -a` once (bounded to
66
164
  ten minutes) and audits again; only a database still inconsistent stops with
67
- `APT_STATE_BROKEN`.
165
+ `APT_STATE_BROKEN`. When the cut-off run left one of the bootstrap's own
166
+ packages unconfigured (an upgrade of `python3` or `git`, say), the bootstrap
167
+ makes the same repair before it installs what it lacks, whoever started the
168
+ run, waiting up to two minutes for a package process that holds the lock;
169
+ only a database still inconsistent stops it with `DPKG_INTERRUPTED`.
68
170
 
69
171
  Health does not mutate configuration, renew tokens, log in, install software, or
70
172
  make billable inference requests. It audits the immutable desired worker deployment,
71
173
  not an unrelated developer branch. Every declared worker must be enrolled and
72
- reachable. Extra service hosts are listed as outside scope. Required failures,
174
+ reachable, unless it is parked (`devfleet fleet-node-park`): a parked worker
175
+ is listed as parked and never contacted. Extra service hosts are listed as outside scope. Required failures,
73
176
  warnings and unverified checks prevent a ready result; explicitly non-required
74
177
  checks remain visible without inventing a pass.
75
178
 
@@ -0,0 +1,107 @@
1
+ # What a machine must offer to be a worker
2
+
3
+ The one list of what a machine needs before it can be a worker, wherever it
4
+ runs: a computer at home, a virtual machine on your own host or an instance
5
+ rented from any provider. Everything else, `fleet-sync` installs. No vendor is
6
+ required; any provider that boots a stock Ubuntu image with SSH and cloud-init
7
+ user data works the same way.
8
+
9
+ Each requirement that a check enforces names the check (from
10
+ [schema/check-ids.json](../schema/check-ids.json)) and the failure code it
11
+ reports (each has an entry in the [runbooks](runbooks.md)). A dash means the
12
+ check reports no code of its own; its summary and remedy say what failed.
13
+ `features/rules/worker-requirements.feature` fails when a check or code here
14
+ does not exist, when a check marked as a worker requirement in the registry
15
+ has no row here, or when the image, architecture and size rows disagree with
16
+ `ansible/ubuntu/policy.json`.
17
+
18
+ ## The machine
19
+
20
+ | Requirement | How to check it | Check | Failure code |
21
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------- | -------------------------------- |
22
+ | A stock Ubuntu image of an accepted release: **26.04** (resolute) or **24.04** (noble). No distribution upgrade is ever made. | `. /etc/os-release; echo $VERSION_ID` | `ubuntu:release` | `UBUNTU_BASELINE_MISMATCH` |
23
+ | An accepted architecture: **amd64** (x86_64) or **arm64** (aarch64). | `uname -m` | `ubuntu:release` | `UBUNTU_BASELINE_MISMATCH` |
24
+ | A machine of its own: physical, or a full virtual machine (KVM, Xen, VMware, Hyper-V, or any hypervisor `systemd-detect-virt --vm` names). A container (Docker, LXC, systemd-nspawn, OpenVZ) or WSL2 is refused. | `systemd-detect-virt` | `platform:environment` | `WORKER_ENVIRONMENT_UNSUPPORTED` |
25
+ | At least **2 CPUs**, **3584 MiB** of memory (a 4 GiB instance) and a **20480 MiB** root filesystem. What dev-fleet installs is measured in CI; the operating system is estimated. | `nproc; free -m; df -m /` | `platform:worker-size` | `WORKER_SIZE_BELOW_MINIMUM` |
26
+ | Survives one reboot: the first converge ends with a reboot while no agent runs, and the worker must answer SSH again within 15 minutes. | `devfleet fleet-status --workers` after the first run | `worker:reboot` | `WORKER_REBOOT_FAILED` |
27
+
28
+ The minimum size is `sizing.minimumWorker` in `ansible/ubuntu/policy.json`;
29
+ [the worker contract](worker-contract.md#size) explains the estimate and what
30
+ grows with the machine. The accepted releases and architectures are `releases`
31
+ in the same file; [the support matrix](support-matrix.md) lists every
32
+ platform and why the others are refused.
33
+
34
+ ## Access
35
+
36
+ | Requirement | How to check it | Check | Failure code |
37
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------- | ----------------------------------- |
38
+ | SSH with a key, from the manager, without a password. | `ssh -o BatchMode=yes WORKER true` | `ssh:auth` | - |
39
+ | A host key the manager knows before the first connection: generated by `devfleet fleet-node-seed`, given as `hostKey` in a worker record, or pinned with `devfleet fleet-node-replace --host-key`. | `devfleet fleet-status --workers` | `ssh:host-key` | - |
40
+ | A non-root account that may use `sudo` without a password, as `workers[].network.user` names it. An image that lets only root in is fine: `fleet-sync` creates the account over the root login once. | `ssh WORKER 'id -u; sudo -n true'` | `bootstrap:privilege` | `SUDO_AUTHORIZATION_REQUIRED` |
41
+ | With only a root login, `workers[].network.user` names the account to create. | `fleet.json` | `worker:account` | `WORKER_ACCOUNT_UNDECLARED` |
42
+ | Joined to the fleet's tailnet: by the seed at first boot, or by `fleet-sync` with a single-use key the control plane `fleet.json` declares (`tailnet`) mints for it. | `tailscale status --json` on the manager | `tailscale:configuration` | `TAILSCALE_SETUP_OR_LOGIN_REQUIRED` |
43
+
44
+ ## First-boot data
45
+
46
+ A rented machine is set up from cloud-init user data and nothing else.
47
+ `devfleet fleet-node-seed --id ID --output PATH` writes it: the account with
48
+ the manager's key, the sudo rule, a host key generated for the machine, the
49
+ pinned Tailscale and a short-lived single-use key. Any image that runs
50
+ cloud-init accepts it (`--autoinstall` writes an Ubuntu autoinstall file for a
51
+ machine installed from the ISO). The seed holds a secret: pass it to the
52
+ provider as user data and delete it afterwards.
53
+
54
+ | Requirement | How to check it | Check | Failure code |
55
+ | --------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------------------- | ----------------------------------------- |
56
+ | The image runs cloud-init, or nothing at first boot. `fleet-sync` waits for a first boot that is still running. | `cloud-init status --wait` | `bootstrap:first-boot` | `FIRST_BOOT_FAILED`, `FIRST_BOOT_PENDING` |
57
+
58
+ ## Network
59
+
60
+ No inbound connection from the internet is needed: the manager reaches a
61
+ worker over the tailnet. A fixed or public address is not needed either.
62
+ Outbound, the worker opens HTTPS (TCP 443) to:
63
+
64
+ - the Ubuntu archive its image is configured with, and `snapshot.ubuntu.com`
65
+ (every pinned package);
66
+ - `pkgs.tailscale.com` (Tailscale) and the fleet's tailnet control plane:
67
+ `tailnet.url` in `fleet.json`, or the hosted control plane when it names
68
+ none; Tailscale also uses UDP 41641 and its relays (UDP 3478, TCP 443);
69
+ - `github.com`, `api.github.com` and GitHub's release download hosts, and the
70
+ other download hosts `mise.lock` names
71
+ (`grep -o 'https://[^/"]*' mise.lock | sort -u` lists them);
72
+ - the CLI proxy at `services.proxy.endpoint`, and the secret store
73
+ `fleet.json` names (Vault's address with `vault`).
74
+
75
+ | Requirement | How to check it | Check | Failure code |
76
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------------------- | ---------------------------------------------------------- |
77
+ | Reaches the Ubuntu archive and the snapshot. | `ssh WORKER 'sudo apt-get update'` | `bootstrap:prerequisites` | `APT_REPOSITORY_UNAVAILABLE`, `APT_DNS_UNAVAILABLE` |
78
+ | Reaches GitHub. | `ssh WORKER 'curl -sI https://api.github.com'` | `worker:github-sync` | `GITHUB_UNREACHABLE` |
79
+ | Reaches the CLI proxy. | `devfleet fleet-status --workers` | `proxy:reachable-from` | - |
80
+ | Accepts no inbound connection beyond loopback and the tailnet, but what `exposure` in `fleet.json` declares. The fleet firewall closes the rest by itself. | `sudo nft list table inet personal_fleet` | `worker:exposure` | `WORKER_LISTENER_UNRESTRICTED`, `WORKER_FIREWALL_INACTIVE` |
81
+ | Its public address, when it has one, answers on no fleet port. | `devfleet fleet-status --workers` | `worker:public-reachability` | `WORKER_PUBLIC_PORT_UNRESTRICTED` |
82
+
83
+ A provider's own firewall (a security group) may stay closed to all inbound
84
+ traffic: Tailscale needs none, and SSH from the internet is never needed once
85
+ the seed has joined the machine to the tailnet.
86
+
87
+ ## What is not needed
88
+
89
+ - A display, a desktop or a GPU: `"session": "headless"` (the `editing`
90
+ preset) runs T3 as a service with no graphical session.
91
+ - A public inbound port, a fixed address or a domain name.
92
+ - Git, curl, Python, Node, Bun or mise beforehand: `fleet-sync` installs them.
93
+ - Docker beforehand: the `containers` feature (on unless a worker turns it
94
+ off) installs it.
95
+ - A password for SSH or `sudo`, or anyone at the console.
96
+ - An account with any particular provider.
97
+
98
+ ## From infrastructure as code
99
+
100
+ [examples/workers-from-infrastructure](../examples/workers-from-infrastructure/main.tf)
101
+ shows the handoff with a local virtual machine: the seed from
102
+ `devfleet fleet-node-seed` goes in as user data, and a worker record
103
+ (`schema/worker-record.schema.json`) comes out for `fleet.json` to read. For a
104
+ hosted provider only these change: the provider block and its credentials, the
105
+ image (the provider's stock Ubuntu image of an accepted release), the instance
106
+ type (at least the size above) and the network (a private network, or an
107
+ inbound rule set with nothing open). The seed and the record stay the same.
@@ -110,6 +110,7 @@
110
110
  "vaultUser": "example-user",
111
111
  "toolchain": "mise.lock",
112
112
  "toolProfiles": ["go"],
113
+ "features": { "containers": true, "session": "desktop" },
113
114
  "agentProfile": "proxy",
114
115
  "unattendedSudo": true,
115
116
  "persistentT3": true,
@@ -0,0 +1,132 @@
1
+ # Workers from infrastructure as code: an example, with a local virtual
2
+ # machine (libvirt) standing in for the provider. Any tool or provider that
3
+ # can boot a stock Ubuntu image with cloud-init user data and write a file
4
+ # works the same way; dev-fleet never calls a provider and never creates a
5
+ # machine. It reads the worker records this writes (worker-records.json, see
6
+ # schema/worker-record.schema.json) because fleet.json names the file:
7
+ #
8
+ # "workerRecords": [{ "file": "infra/worker-records.json" }]
9
+ #
10
+ # The handoff, from the fleet project (OpenTofu installed, libvirt on this
11
+ # machine):
12
+ #
13
+ # 1. Declare the worker. With only `tailnet_name` and `user` set, this
14
+ # writes its record and creates nothing else:
15
+ # tofu -chdir=infra apply -var-file=workers.tfvars
16
+ # 2. Seed it. dev-fleet generates its host key, pins it in the fleet state
17
+ # and writes the cloud-init user data:
18
+ # npx devfleet fleet-node-seed --id rented-one \
19
+ # --output infra/seed/rented-one.yaml --apply --yes --json
20
+ # Put the path in `seed` (and, if you like, the printed hostKey in
21
+ # `host_key`, so that the record carries it too).
22
+ # 3. Create the machine, then converge it:
23
+ # tofu -chdir=infra apply -var-file=workers.tfvars
24
+ # npx devfleet fleet-sync --non-interactive
25
+ # 4. Remove it: drop it from `workers` and apply, or `tofu destroy`. Its
26
+ # record is gone, so the next `npx devfleet fleet-sync` retires it: its
27
+ # token, pinned host key, monitoring system and T3 link are removed.
28
+ #
29
+ # What the machine must offer (image, architecture, size, access, network) is
30
+ # docs/worker-requirements.md. For a hosted provider only these change: the
31
+ # provider block and its credentials, the image (its stock Ubuntu image of an
32
+ # accepted release, in place of `ubuntu_image`), the instance type (at least
33
+ # the minimum size, in place of `memory_mib` and `vcpus`) and the network (a
34
+ # private network, or inbound rules with nothing open, in place of
35
+ # `network_interface`). The seed goes in as the instance's user data and the
36
+ # records come out the same way.
37
+ #
38
+ # This example cannot be applied in dev-fleet's CI (it needs a libvirt host
39
+ # and a tailnet), so it is not proven there; the record format and what
40
+ # fleet-sync does with records are (features/cli/fleet-validate.feature,
41
+ # features/fleet-sync/node-lifecycle.feature). Pin the provider in your own
42
+ # project: `tofu init` writes .terraform.lock.hcl.
43
+
44
+ terraform {
45
+ required_providers {
46
+ libvirt = { source = "dmacvicar/libvirt" }
47
+ local = { source = "hashicorp/local" }
48
+ }
49
+ }
50
+
51
+ provider "libvirt" {
52
+ uri = "qemu:///system"
53
+ }
54
+
55
+ variable "ubuntu_image" {
56
+ description = "A stock Ubuntu cloud image (a URL or a local path) of the release fleet.json names."
57
+ type = string
58
+ }
59
+
60
+ variable "workers" {
61
+ description = "The workers, by fleet ID. A worker with a seed is created; one without is only declared."
62
+ type = map(object({
63
+ tailnet_name = string
64
+ user = string
65
+ seed = optional(string)
66
+ host_key = optional(string)
67
+ memory_mib = optional(number, 8192)
68
+ vcpus = optional(number, 4)
69
+ hourly_rate = optional(number)
70
+ }))
71
+ }
72
+
73
+ locals {
74
+ seeded = { for id, worker in var.workers : id => worker if worker.seed != null }
75
+ }
76
+
77
+ resource "libvirt_volume" "image" {
78
+ name = "devfleet-ubuntu.qcow2"
79
+ source = var.ubuntu_image
80
+ }
81
+
82
+ resource "libvirt_volume" "disk" {
83
+ for_each = local.seeded
84
+ name = "${each.key}.qcow2"
85
+ base_volume_id = libvirt_volume.image.id
86
+ size = 64 * 1024 * 1024 * 1024
87
+ }
88
+
89
+ resource "libvirt_cloudinit_disk" "seed" {
90
+ for_each = local.seeded
91
+ name = "${each.key}-seed.iso"
92
+ user_data = file(each.value.seed)
93
+ }
94
+
95
+ resource "libvirt_domain" "worker" {
96
+ for_each = local.seeded
97
+ name = each.key
98
+ memory = each.value.memory_mib
99
+ vcpu = each.value.vcpus
100
+ cloudinit = libvirt_cloudinit_disk.seed[each.key].id
101
+
102
+ disk {
103
+ volume_id = libvirt_volume.disk[each.key].id
104
+ }
105
+
106
+ network_interface {
107
+ network_name = "default"
108
+ }
109
+ }
110
+
111
+ # The handoff: one record per declared worker. Only what dev-fleet needs to
112
+ # reach the machine, and nothing that names this tool or the provider.
113
+ resource "local_file" "worker_records" {
114
+ filename = "${path.module}/worker-records.json"
115
+ file_permission = "0644"
116
+ content = jsonencode({
117
+ version = 1
118
+ workers = [
119
+ for id, worker in var.workers : merge(
120
+ { id = id, tailnetName = worker.tailnet_name, user = worker.user },
121
+ worker.host_key == null ? {} : { hostKey = worker.host_key },
122
+ worker.hourly_rate == null ? {} : { billing = { hourlyRate = worker.hourly_rate } },
123
+ { labels = { source = "local-vm" } },
124
+ )
125
+ ]
126
+ })
127
+ }
128
+
129
+ output "fleet_workers" {
130
+ description = "The same records, for a fleet.json that runs a command instead: { \"command\": [\"tofu\", \"-chdir=infra\", \"output\", \"-json\", \"fleet_workers\"] }."
131
+ value = jsondecode(local_file.worker_records.content)
132
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "../../schema/worker-record.schema.json",
3
+ "version": 1,
4
+ "workers": [
5
+ {
6
+ "id": "rented-one",
7
+ "tailnetName": "rented-one.tail0a1b2c.ts.net",
8
+ "user": "dev",
9
+ "hostKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleHostKeyOfARentedWorker",
10
+ "billing": { "hourlyRate": 0.5 },
11
+ "labels": { "source": "local-vm" }
12
+ }
13
+ ]
14
+ }
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # dev-fleet
2
2
 
3
- > dev-fleet keeps a set of development machines in sync: one macOS **manager** and any number of Ubuntu 26.04 **workers** reached over SSH and Tailscale. It is used as the npm package `@crvouga/dev-fleet` (CLI `devfleet`) from a project of your own that holds `fleet.json` and pins the package version; one command, `devfleet fleet-sync`, converges every machine to the commit that version was published from. The fleet state lives in an S3-compatible bucket you own.
3
+ > dev-fleet keeps a set of development machines in sync: one macOS **manager** and any number of Ubuntu (26.04 or 24.04) **workers** reached over SSH and Tailscale. It is used as the npm package `@crvouga/dev-fleet` (CLI `devfleet`) from a project of your own that holds `fleet.json` and pins the package version; one command, `devfleet fleet-sync`, converges every machine to the commit that version was published from. The fleet state lives in an S3-compatible bucket you own.
4
4
 
5
5
  Rules for an agent working in a project that uses dev-fleet:
6
6
 
@@ -16,10 +16,12 @@ Rules for an agent working in a project that uses dev-fleet:
16
16
  - [README](https://github.com/crvouga/dev-fleet/blob/main/README.md): what dev-fleet assumes, quickstart, versions, adding a machine, known limits
17
17
  - [Example fleet.json](https://github.com/crvouga/dev-fleet/blob/main/examples/fleet.json): a complete configuration with placeholders (`npx devfleet fleet-state --example` prints it)
18
18
  - [fleet.json schema](https://github.com/crvouga/dev-fleet/blob/main/schema/fleet.schema.json): JSON Schema for the fleet configuration
19
+ - [Check-ID registry](https://github.com/crvouga/dev-fleet/blob/main/schema/check-ids.json): every check ID a report can carry, with what it means, whether it can block readiness (severity) and what repairs it (a fleet-repair recipe, or why only a person can); generated from the code
19
20
 
20
21
  ## Operate it
21
22
 
22
23
  - [Fleet state](https://github.com/crvouga/dev-fleet/blob/main/docs/state.md): the bucket, replication, replacing the manager, what the bucket does not hold yet
24
+ - [Worker requirements](https://github.com/crvouga/dev-fleet/blob/main/docs/worker-requirements.md): what a machine must offer to be a worker (image, architecture, size, access, first-boot data, network), each with its check and failure code, and what changes for a hosted provider
23
25
  - [Topology](https://github.com/crvouga/dev-fleet/blob/main/docs/topology.md): the manager and worker roles and the canary
24
26
  - [What is declared, and where](https://github.com/crvouga/dev-fleet/blob/main/docs/declared.md): what is pinned on a machine and what is not
25
27
  - [Provider accounts](https://github.com/crvouga/dev-fleet/blob/main/docs/accounts.md): the account roster, its states and checks, adding or removing an account, propagation to every node, account selection
package/mise.lock CHANGED
@@ -1355,6 +1355,35 @@ url = "https://github.com/getsops/sops/releases/download/v3.13.3/sops-v3.13.3.am
1355
1355
  url_api = "https://api.github.com/repos/getsops/sops/releases/assets/486808200"
1356
1356
  provenance = "cosign"
1357
1357
 
1358
+ [[tools.tmux]]
1359
+ version = "3.8"
1360
+ backend = "aqua:tmux/tmux-builds"
1361
+ specifiers = ["3.8"]
1362
+
1363
+ [tools.tmux."platforms.linux-arm64"]
1364
+ checksum = "sha256:f399311b1159705e69839eaf4f6f68c8b8540dc3a8538b87c30dedb9c3c7fc23"
1365
+ checksum_source = "mise-versions"
1366
+ url = "https://github.com/tmux/tmux-builds/releases/download/v3.8/tmux-3.8-linux-arm64.tar.gz"
1367
+ url_api = "https://api.github.com/repos/tmux/tmux-builds/releases/assets/721688993"
1368
+
1369
+ [tools.tmux."platforms.linux-x64"]
1370
+ checksum = "sha256:147849ff18b62a8aa3d5d8f75dd07c3f9fdfad529f932236afdec838fc1d15e1"
1371
+ checksum_source = "mise-versions"
1372
+ url = "https://github.com/tmux/tmux-builds/releases/download/v3.8/tmux-3.8-linux-x86_64.tar.gz"
1373
+ url_api = "https://api.github.com/repos/tmux/tmux-builds/releases/assets/721688991"
1374
+
1375
+ [tools.tmux."platforms.macos-arm64"]
1376
+ checksum = "sha256:ec926bbf28ddf55d0dc7c9791d43e03e251ddab392d763f7fbb38d4b8408d051"
1377
+ checksum_source = "mise-versions"
1378
+ url = "https://github.com/tmux/tmux-builds/releases/download/v3.8/tmux-3.8-macos-arm64.tar.gz"
1379
+ url_api = "https://api.github.com/repos/tmux/tmux-builds/releases/assets/721688988"
1380
+
1381
+ [tools.tmux."platforms.macos-x64"]
1382
+ checksum = "sha256:e7b92e51d9e9610ce80d1c477da1e6ff6a6398ea6b6b4d4ff7b19cf60f594555"
1383
+ checksum_source = "mise-versions"
1384
+ url = "https://github.com/tmux/tmux-builds/releases/download/v3.8/tmux-3.8-macos-x86_64.tar.gz"
1385
+ url_api = "https://api.github.com/repos/tmux/tmux-builds/releases/assets/721688994"
1386
+
1358
1387
  [[tools.usql]]
1359
1388
  version = "0.21.6"
1360
1389
  backend = "aqua:xo/usql"
package/mise.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  # Shared workstation toolchain. Bootstrap installs this globally; projects may override it.
2
2
  # Commit mise.lock whenever changing these pins.
3
- # min_version is also the exact mise bootstrap/bootstrap.sh installs; bump the
4
- # mise_*_sha256 digests under [vars] with it.
3
+ # min_version is also the exact mise bootstrap/bootstrap.sh installs; the
4
+ # mise_*_sha256 digests under [vars] move with it (bun bootstrap/pins.ts mise).
5
5
  min_version = "2026.10.24"
6
6
 
7
7
  [settings]
@@ -30,6 +30,8 @@ go = "1.27.3"
30
30
  jq = "1.8.2"
31
31
  yq = "4.54.1"
32
32
  ripgrep = "15.2.0"
33
+ # Terminal multiplexer for long agent and build sessions on a worker.
34
+ tmux = { version = "3.8", os = ["linux", "macos"] }
33
35
  fd = "10.5.0"
34
36
  fzf = "0.74.5"
35
37
  bat = "0.26.1"
@@ -109,7 +111,7 @@ docker_desktop_arm64_sha256 = "9c6f13ce4edda2abf20d801552b6acb96acd3d64b3e91b50c
109
111
  docker_desktop_amd64_sha256 = "96b53f9a4a807b6be6207d18f360b9778ff481e1d94b5027bdf1ae2a22d837cb"
110
112
 
111
113
  # Minimal remote development baseline; versions are resolved exclusively by tools + mise.lock.
112
- worker_tools = "bun,node,gh,python,uv,docker-cli,vault,jq,codex,claude,aqua:anomalyco/opencode,gemini,copilot,http:grok,http:antigravity"
114
+ worker_tools = "bun,node,gh,python,uv,docker-cli,vault,jq,tmux,codex,claude,aqua:anomalyco/opencode,gemini,copilot,http:grok,http:antigravity"
113
115
  # Tool profiles a fleet adds to its workers (`worker.toolProfiles` in
114
116
  # fleet.json, fleet-wide or per worker): one `worker_profile_<name>` each,
115
117
  # naming tools pinned above. bootstrap/worker-tools.ts lists the names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/dev-fleet",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Keeps a set of development machines in sync: one manager and many workers",
5
5
  "license": "MIT",
6
6
  "repository": "github:crvouga/dev-fleet",
@@ -72,5 +72,5 @@
72
72
  "engines": {
73
73
  "node": ">=22"
74
74
  },
75
- "gitHead": "77efce7ad99a3d521169a5e773ad6b97ca21bc75"
75
+ "gitHead": "92f24582645e8a05f47124be1076a28f637ec347"
76
76
  }