@crvouga/dev-fleet 0.8.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 (204) hide show
  1. package/README.md +65 -9
  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 +55 -9
  14. package/ansible/macos/manager.yml +154 -7
  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/monitoring/agent-darwin.yml +3 -0
  19. package/ansible/monitoring/agent.yml +2 -0
  20. package/ansible/monitoring/hub-darwin.yml +3 -0
  21. package/ansible/monitoring/hub-route-darwin.yml +1 -0
  22. package/ansible/net/remote-probe.sh +4 -1
  23. package/ansible/platforms.json +2 -2
  24. package/ansible/pyproject.toml +5 -0
  25. package/ansible/ubuntu/baseline.yml +39 -13
  26. package/ansible/ubuntu/disk-gc.py +6 -2
  27. package/ansible/ubuntu/firewall.nft.j2 +50 -0
  28. package/ansible/ubuntu/policy.json +125 -6
  29. package/ansible/ubuntu/reboot.yml +89 -0
  30. package/ansible/ubuntu/recovery.yml +13 -3
  31. package/ansible/ubuntu/t3/policy.json +3 -0
  32. package/ansible/ubuntu/t3-desktop.yml +23 -2
  33. package/ansible/ubuntu/t3-headless.yml +128 -0
  34. package/ansible/ubuntu/t3-health.sh +24 -10
  35. package/ansible/ubuntu/t3-idle.py +37 -2
  36. package/ansible/ubuntu/t3-policy.yml +4 -1
  37. package/ansible/ubuntu/t3-route.yml +1 -0
  38. package/ansible/ubuntu/tailscale.yml +54 -8
  39. package/ansible/ubuntu/tasks/browser.yml +3 -0
  40. package/ansible/ubuntu/tasks/containers-off.yml +43 -0
  41. package/ansible/ubuntu/tasks/docker.yml +53 -6
  42. package/ansible/ubuntu/tasks/dpkg.yml +4 -0
  43. package/ansible/ubuntu/tasks/features.yml +22 -0
  44. package/ansible/ubuntu/tasks/firewall.yml +172 -0
  45. package/ansible/ubuntu/tasks/install.yml +2 -1
  46. package/ansible/ubuntu/tasks/packages.yml +4 -2
  47. package/ansible/ubuntu/tasks/release.yml +6 -0
  48. package/ansible/ubuntu/tasks/resources.yml +6 -4
  49. package/ansible/ubuntu/tasks/services.yml +2 -2
  50. package/ansible/ubuntu/tasks/sizing.yml +49 -0
  51. package/ansible/ubuntu/tasks/snapshot.yml +14 -3
  52. package/ansible/ubuntu/tasks/ssh.yml +61 -4
  53. package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
  54. package/ansible/ubuntu/tasks/user.yml +1 -0
  55. package/ansible/ubuntu/worker-account.yml +117 -0
  56. package/ansible/uv.lock +265 -0
  57. package/bin/devfleet.mjs +42 -4
  58. package/bootstrap/fleet-remote-bootstrap.sh +91 -25
  59. package/bootstrap/global-config.ts +29 -3
  60. package/bootstrap/package-smoke.ts +127 -0
  61. package/bootstrap/pins.ts +189 -0
  62. package/bootstrap/release-notes.ts +88 -0
  63. package/bootstrap/release-version.ts +41 -7
  64. package/bootstrap/worker-features.ts +114 -0
  65. package/bootstrap/worker-seed.yaml +7 -3
  66. package/bootstrap/worker-tools.ts +39 -9
  67. package/cliproxy/README.md +1 -1
  68. package/cliproxy/docs/health-workflow.example.yml +20 -143
  69. package/docs/authentication.md +19 -11
  70. package/docs/consumer-setup.md +42 -27
  71. package/docs/convergence.md +67 -0
  72. package/docs/declared.md +61 -34
  73. package/docs/machine-setup.md +73 -7
  74. package/docs/manager-setup.md +10 -3
  75. package/docs/operations.md +64 -9
  76. package/docs/releasing.md +49 -1
  77. package/docs/runbooks.md +98 -7
  78. package/docs/state.md +23 -11
  79. package/docs/support-matrix.md +40 -4
  80. package/docs/topology.md +67 -6
  81. package/docs/ubuntu-sync.md +78 -28
  82. package/docs/worker-contract.md +122 -19
  83. package/docs/worker-requirements.md +107 -0
  84. package/examples/fleet.json +1 -0
  85. package/examples/workers-from-infrastructure/main.tf +132 -0
  86. package/examples/workers-from-infrastructure/worker-records.json +14 -0
  87. package/llms.txt +3 -1
  88. package/mise.lock +29 -0
  89. package/mise.toml +5 -3
  90. package/package.json +2 -2
  91. package/schema/check-ids.json +1967 -170
  92. package/schema/check-ids.schema.json +34 -0
  93. package/schema/fleet.schema.json +327 -8
  94. package/schema/output/check.schema.json +1 -1
  95. package/schema/output/fleet-state-definition.schema.json +14 -0
  96. package/schema/output/fleet-sync-schedule.schema.json +26 -0
  97. package/schema/output/fleet-sync.schema.json +18 -0
  98. package/schema/output/report.schema.json +3 -2
  99. package/schema/worker-record.schema.json +76 -0
  100. package/src/agents/converge.ts +140 -24
  101. package/src/agents/files.ts +2 -32
  102. package/src/agents/native-verify.ts +34 -3
  103. package/src/agents/secrets.ts +39 -4
  104. package/src/check-owners.ts +61 -2
  105. package/src/check-registry.ts +630 -0
  106. package/src/cli/agent-guide.ts +3 -3
  107. package/src/cli/cli.ts +4 -0
  108. package/src/cli/fleet-manifest.ts +9 -3
  109. package/src/cli/fleet-node-lifecycle.ts +313 -64
  110. package/src/cli/fleet-node-park.ts +238 -0
  111. package/src/cli/fleet-node-seed.ts +51 -35
  112. package/src/cli/fleet-state.ts +2 -1
  113. package/src/cli/fleet-sync-checkout.ts +7 -0
  114. package/src/cli/fleet-sync-enrollment.ts +193 -32
  115. package/src/cli/fleet-sync-forward.ts +4 -0
  116. package/src/cli/fleet-sync-manager.ts +103 -33
  117. package/src/cli/fleet-sync-output.ts +31 -11
  118. package/src/cli/fleet-sync-plan.ts +65 -9
  119. package/src/cli/fleet-sync-records.ts +113 -0
  120. package/src/cli/fleet-sync-remote.ts +151 -9
  121. package/src/cli/fleet-sync-schedule.ts +14 -4
  122. package/src/cli/fleet-sync-state.ts +28 -16
  123. package/src/cli/fleet-sync-vault.ts +77 -0
  124. package/src/cli/fleet-sync-worker.ts +214 -0
  125. package/src/cli/fleet-sync.ts +150 -19
  126. package/src/cli/fleet-validate.ts +59 -21
  127. package/src/cli/fleet-watch.ts +7 -1
  128. package/src/cli/fleet.ts +87 -5
  129. package/src/cli/managed-health.ts +25 -1
  130. package/src/cli/register-fleet.ts +115 -4
  131. package/src/cli/registry.ts +14 -0
  132. package/src/cli/tailnet.ts +43 -10
  133. package/src/cli/ui-data.ts +3 -1
  134. package/src/cli/worker-usage-report.ts +63 -0
  135. package/src/clock.ts +42 -0
  136. package/src/definition.ts +332 -9
  137. package/src/diagnostics.ts +5 -2
  138. package/src/exposure.ts +102 -0
  139. package/src/fleet-config.ts +3 -1
  140. package/src/git-identity.ts +76 -3
  141. package/src/ide/ide.ts +5 -0
  142. package/src/ide/t3.ts +12 -2
  143. package/src/input-hashes.ts +6 -0
  144. package/src/machine/machine-class.ts +59 -0
  145. package/src/machine/platform.ts +90 -11
  146. package/src/machine/state.ts +37 -15
  147. package/src/machine/support-matrix.ts +11 -9
  148. package/src/machine-health.ts +28 -5
  149. package/src/manager-health.ts +148 -2
  150. package/src/manager-sync.ts +65 -4
  151. package/src/monitoring/beszel.ts +8 -8
  152. package/src/monitoring/enroll.ts +8 -1
  153. package/src/monitoring/hub-access.ts +10 -4
  154. package/src/monitoring/systems.ts +138 -0
  155. package/src/network-definition.ts +13 -4
  156. package/src/node-converge-linux.ts +136 -27
  157. package/src/node-converge.ts +13 -3
  158. package/src/platform/ansible-receipt.ts +17 -2
  159. package/src/platform/ansible.ts +40 -4
  160. package/src/platform/manager-host.ts +25 -8
  161. package/src/platform/resource-verify.ts +18 -7
  162. package/src/platform/t3-policy.ts +24 -10
  163. package/src/platform/t3-service.ts +109 -34
  164. package/src/platform/ubuntu-release.ts +24 -0
  165. package/src/platform/ubuntu-verify.ts +31 -3
  166. package/src/platform/ubuntu-worker.ts +58 -0
  167. package/src/platform/worker-platform.ts +72 -0
  168. package/src/protocol.ts +1 -1
  169. package/src/public-reachability.ts +186 -0
  170. package/src/remote-bootstrap.ts +50 -12
  171. package/src/remote-operation.ts +18 -4
  172. package/src/repair-plan.ts +23 -12
  173. package/src/report.ts +2 -1
  174. package/src/schedule-check.ts +21 -2
  175. package/src/secrets.ts +4 -0
  176. package/src/service-auth.ts +55 -48
  177. package/src/ssh-connection.ts +141 -0
  178. package/src/ssh.ts +63 -24
  179. package/src/state/store.ts +4 -2
  180. package/src/state/sync.ts +35 -6
  181. package/src/state/workers.ts +140 -0
  182. package/src/t3/pairing.ts +59 -0
  183. package/src/t3/peer-link.ts +333 -0
  184. package/src/t3/version.ts +1 -1
  185. package/src/tailnet-cleanup.ts +185 -0
  186. package/src/tailnet-control.ts +346 -0
  187. package/src/tailscale.ts +1 -1
  188. package/src/toolchain-checks.ts +11 -1
  189. package/src/types.ts +4 -2
  190. package/src/worker-account.ts +63 -0
  191. package/src/worker-activity.ts +303 -0
  192. package/src/worker-capacity.ts +31 -0
  193. package/src/worker-exposure.ts +388 -0
  194. package/src/worker-features.ts +201 -0
  195. package/src/worker-host-checks.ts +71 -20
  196. package/src/worker-manifest.ts +27 -4
  197. package/src/worker-parking.ts +70 -0
  198. package/src/worker-policy.ts +17 -1
  199. package/src/worker-readiness.ts +15 -3
  200. package/src/worker-reboot.ts +129 -0
  201. package/src/worker-records.ts +305 -0
  202. package/src/worker-seed.ts +16 -11
  203. package/src/worker-sizing.ts +76 -0
  204. 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
  # Developer fleet
6
7
 
@@ -22,7 +23,7 @@ Use `devfleet fleet-sync` to reconcile all declared workers and
22
23
  `devfleet fleet-status --workers` to inspect remaining gaps. See
23
24
  [worker-contract.md](worker-contract.md) for bootstrap, ownership and the exact
24
25
  scope of readiness. `fleet-status` without flags diagnoses the local roster.
25
- Workers are Ubuntu 26.04; the manager is macOS and never sleeps on AC power
26
+ Workers are Ubuntu 26.04 or 24.04; the manager is macOS and never sleeps on AC power
26
27
  (`ansible/macos/manager.yml`). Machine policy (never sleep, no automatic
27
28
  logout, always-on Tailscale/SSH/Docker/Beszel, memory and disk safety, per-node
28
29
  credential renewal) is
@@ -134,6 +135,26 @@ expiring: tag them, or disable key expiry for each in the Tailscale admin
134
135
  console (tagged nodes do not expire). `tailscale:key-expiry` warns two weeks
135
136
  ahead and fails three days ahead.
136
137
 
138
+ With `tailnet` in `fleet.json` and the secret `tailnet-admin`, nothing needs
139
+ renewing: every worker joins with a key that carries the fleet's tag, and a
140
+ tagged device has key expiry disabled on the hosted Tailscale. For an enrolled
141
+ worker whose key still expires (one that joined before it was tagged),
142
+ `fleet-sync` disables expiry through the control plane
143
+ (`tailscale:key-expiry-policy`); `tailscale:key-expiry` measures the result on
144
+ the next run. Headscale gives a node registered with a pre-authorised key no
145
+ expiry.
146
+
147
+ ### Maintenance reboots
148
+
149
+ Kernel and library updates apply only after a reboot (`worker:reboot-pending`).
150
+ `devfleet fleet-sync --maintenance-reboot` reboots each worker that has one
151
+ pending: one at a time, the canary first, only while it runs no agent turn
152
+ (`--force` ends them), through `ansible/ubuntu/reboot.yml`. Each worker is
153
+ converged again once it answers; a worker that does not come back ready stops
154
+ the ones after it. A new worker that was never ready is rebooted once after its
155
+ first converge in any `fleet-sync`, so it reaches ready in one run; a worker
156
+ that was ready before is rebooted only by `--maintenance-reboot`.
157
+
137
158
  To re-join a worker whose key expired, a person signs it in again, because
138
159
  only a person's identity can authorise a device:
139
160
 
@@ -205,7 +226,8 @@ serially; diagnostics run at most four remote probes concurrently.
205
226
  shell PATH and installs frozen Bun dependencies. Review OS-specific effects
206
227
  and existing config conflict guards in the shared bootstrap runbook.
207
228
  - Record input hashes only after success and stable checkout inputs. Failed
208
- setup can leave partial installs; no rollback is claimed. Trusted checkout
229
+ setup can leave partial installs, which the next sync completes. Going back
230
+ is `fleet-sync --rollback`, as the [README](../README.md#the-version) says. Trusted checkout
209
231
  package lifecycle scripts can execute during installation.
210
232
  - No other application repos, credentials, provider settings or cloud resources
211
233
  are synchronized. `devfleet fleet-sync` remains the broader convergence.
@@ -258,12 +280,45 @@ A notification carries node and check IDs only. `devfleet fleet-watch --test`
258
280
  sends one test notification to every target.
259
281
 
260
282
  Every run stores what it saw, and when, as `watch.json` beside the fleet state
261
- in the bucket. That time is the manager's heartbeat. A watcher on the manager
262
- notices nothing when the manager itself is off, so call the reusable
263
- `fleet-heartbeat.yml` workflow from the repository that holds your
264
- `fleet.json` (its header shows the one `uses:` line): it fails, and GitHub
265
- notifies you, when the heartbeat is older than `max-age-minutes`. `bun run health` remains the separate
266
- single read-only **cloud platform** report.
283
+ in the bucket. That time is the manager's heartbeat. Nobody has to run it:
284
+ `fleet-sync` installs it on the manager as the launchd job
285
+ `dev.devfleet.fleet-watch` (`ansible/macos/manager.yml`), which runs every
286
+ `manager.schedule.watchMinutes` minutes of `fleet.json` (15 by default) from
287
+ the checkout `fleet-sync` last applied, and `manager:watch` fails when it is
288
+ not loaded. It reads the bucket credentials `fleet-state --init` saved.
289
+
290
+ A watcher on the manager notices nothing when the manager itself is off, so
291
+ call the reusable `fleet-heartbeat.yml` workflow from the repository that
292
+ holds your `fleet.json` (its header shows the one `uses:` line): it fails, and
293
+ GitHub notifies you, when the heartbeat is older than `max-age-minutes`
294
+ (60 by default). With the watch job every 15 minutes and the workflow every
295
+ 30, a manager that is turned off fails the workflow within about 90 minutes
296
+ (the age limit plus one interval of the workflow's schedule; GitHub may delay
297
+ a scheduled run). Keep `max-age-minutes` well above `watchMinutes`.
298
+
299
+ The CLI proxy is checked off the manager the same way:
300
+ `.github/workflows/cliproxy-health.yml` is a reusable workflow, called with one
301
+ `uses:` line. It needs no stored secret: it reads the Vault address and the
302
+ proxy endpoint from your `fleet.json`, logs in to Vault with GitHub's OIDC
303
+ token (the read-only role `vault-role` names, `github-actions` by default) and
304
+ runs the checks of the dev-fleet release your `package.json` pins. Copy
305
+ [`cliproxy/docs/health-workflow.example.yml`](../cliproxy/docs/health-workflow.example.yml)
306
+ into `.github/workflows/` and name the release tag in its `uses:` line; it
307
+ runs the free readiness check every 15 minutes, a light inference check hourly
308
+ and the full suite nightly, and keeps one tracking issue in your repository.
309
+
310
+ ### The logs of the manager's scheduled jobs
311
+
312
+ `fleet-sync --schedule` and the watch job write to
313
+ `~/.local/share/personal/fleet/manager/logs/<label>.{out,err}.log`. newsyslog
314
+ rotates each once it passes `jobLogs.maxKiB` of `ansible/macos/policy.json`
315
+ (1024 KiB) and keeps `jobLogs.keep` compressed archives, from the drop-in
316
+ `/etc/newsyslog.d/dev-fleet-USER.conf` that the manager's one administrator
317
+ grant (`ansible/macos/manager-privilege.yml`) installs. `manager:job-logs`
318
+ warns when the drop-in is missing, or a log has grown past twice that size.
319
+
320
+ `bun run health` remains the separate single read-only **cloud platform**
321
+ report.
267
322
 
268
323
  ## Development
269
324
 
package/docs/releasing.md CHANGED
@@ -51,7 +51,33 @@ cannot release because publishing was never set up (no tag and nothing on
51
51
  npm) fails, and the run summary says why; a merge with nothing new to release
52
52
  passes and says so. Re-running a
53
53
  release whose version is already on npm skips the publish and finishes the tag
54
- and GitHub Release.
54
+ and GitHub Release, whether all its jobs or only the failed ones are re-run: a
55
+ `HEAD` that carries its tag but has no GitHub Release is released again.
56
+
57
+ Nothing is published until the release has been tried. The workflow packs
58
+ the package once, then, for that commit and that tarball:
59
+
60
+ - installs the tarball in an empty directory, the way a fleet project does,
61
+ and runs `devfleet --version` and `devfleet fleet-state --example` on Node
62
+ 22 and 24 ([`bootstrap/package-smoke.ts`](../bootstrap/package-smoke.ts)):
63
+ a broken bin path or a file missing from `files` fails here;
64
+ - runs the [Ubuntu VM test](../.github/workflows/vm.yml) (a real Ubuntu
65
+ machine: baseline, a second run, drift, reboot, and the production SSH
66
+ bootstrap of the commit);
67
+ - runs the [tailnet test](../.github/workflows/tailnet.yml) (a worker joins
68
+ a self-hosted tailnet unattended and leaves no device behind).
69
+
70
+ Only when all three pass is that same tarball published, and the GitHub Release
71
+ gets notes written from the Conventional Commits since the last release
72
+ ([`bootstrap/release-notes.ts`](../bootstrap/release-notes.ts)). A commit
73
+ whose change restarts T3 on the workers, and so ends the agent turns running
74
+ there, says so in a `Worker-Restart: <reason>` footer, and the notes list it
75
+ first.
76
+
77
+ There is no `next` channel yet: publishing uses npm Trusted Publishing, which
78
+ can publish but cannot move a dist-tag, and moving one would need an npm
79
+ token stored in CI. The VM test before publishing stands in for the soak a
80
+ `next` channel would give.
55
81
 
56
82
  A hand-pushed tag still works and publishes exactly that version:
57
83
 
@@ -62,6 +88,28 @@ git tag vX.Y.Z && git push origin vX.Y.Z
62
88
  CI never receives an npm token: it publishes through npm Trusted Publishing
63
89
  (OIDC).
64
90
 
91
+ ## When a release fails
92
+
93
+ A release that fails publishes nothing, and neither does any merge after it
94
+ until one passes: npm stays at the last version. The
95
+ [Failure issues workflow](../.github/workflows/failures.yml) keeps one open
96
+ issue, `Release is failing on main` (label `ci-failure`), with the run, the
97
+ commit and the jobs and steps that failed; a failed VM test keeps its
98
+ redacted results and the guest's logs as an artifact of the run
99
+ (`gh run download <run>`). The next passing release closes the issue and
100
+ publishes everything since the last tag. The same holds for CI on `main`,
101
+ which the release waits for.
102
+
103
+ A pull request is merged by a workflow, and a merge made with a workflow's
104
+ own token starts no CI on `main`. [`auto-merge.yml`](../.github/workflows/auto-merge.yml)
105
+ starts it when the newest commit on `main` has no run: when a CI run ends,
106
+ and every hour. A run started that way starts nothing when it ends either,
107
+ so the same hourly run starts the release once CI has passed on the newest
108
+ commit of `main` and no release has run for it, and the Failure issues
109
+ workflow reads the newest finished run of every workflow it follows each
110
+ hour as well. A merge is therefore released within about an hour of its CI
111
+ passing.
112
+
65
113
  ## One-time setup
66
114
 
67
115
  npm can publish new versions through OIDC but cannot create a package that does
package/docs/runbooks.md CHANGED
@@ -8,7 +8,7 @@ person (a login, an approval, a choice) is marked **needs a person**.
8
8
  `features/rules/runbooks.feature` fails when a code in the source has no
9
9
  entry here, or a `devfleet` command quoted in the documents does not exist.
10
10
 
11
- There are 250 codes; 31 need a person.
11
+ There are 301 codes; 46 need a person.
12
12
 
13
13
  ## Bootstrap of a new or returning worker
14
14
 
@@ -34,7 +34,7 @@ There are 250 codes; 31 need a person.
34
34
  - **Meaning:** A package transaction could not run or the package database is inconsistent.
35
35
  - **Confirm:** `ssh WORKER sudo dpkg --audit` and `sudo apt-get -o Debug::NoLocking=1 check`.
36
36
  - **Fix:** Let any running package process finish (it is never killed), free disk under `/var`, then run `devfleet fleet-sync`.
37
- - **Escalate:** APT_STATE_BROKEN and DPKG_INTERRUPTED need `sudo dpkg --configure -a` by a person once no package process runs.
37
+ - **Escalate:** An interrupted package run is finished by the next `devfleet fleet-sync`: the bootstrap and the baseline each run `dpkg --configure -a` once, bounded. APT_STATE_BROKEN and DPKG_INTERRUPTED mean that repair did not clear `dpkg --audit`: a person repairs the packages it names.
38
38
 
39
39
  - `APT_DISK_FULL`: apt disk full.
40
40
  - `APT_DNS_UNAVAILABLE`: apt dns unavailable.
@@ -104,6 +104,18 @@ There are 250 codes; 31 need a person.
104
104
  - `WORKER_RECOVERY_SETUP_FAILED`: worker recovery setup failed.
105
105
  - `WORKER_RECOVERY_TIMER_INACTIVE`: worker recovery timer inactive.
106
106
 
107
+ ## Worker size and features
108
+
109
+ - **Meaning:** The machine is smaller than the minimum worker (`sizing.minimumWorker` in `ansible/ubuntu/policy.json`), more agent sessions run than its memory policy supports, its declared `features` in `fleet.json` are not ones dev-fleet knows, or something dev-fleet installed for a feature that is now off is still there.
110
+ - **Confirm:** `devfleet fleet-sync --dry-run` shows the `platform:worker-size` check of each worker and, under the plan, what turning a feature off removes; `devfleet fleet-status --workers` shows `worker:features` and `worker:agent-sessions`.
111
+ - **Fix:** Resize a machine below the minimum, then run `devfleet fleet-sync`; nothing was installed on it. For a feature removal, run `devfleet fleet-sync` again. For too many sessions, finish some or move work to a larger worker.
112
+ - **Escalate:** A package another package depends on stays installed until a person removes that one.
113
+
114
+ - `AGENT_SESSION_CAPACITY_EXHAUSTED`: agent session capacity exhausted: more agent sessions run than the memory policy supports (a warning; the size of a session is an estimate).
115
+ - `WORKER_FEATURES_INVALID`: worker features invalid: the session is neither desktop nor headless.
116
+ - `WORKER_FEATURE_REMOVAL_INCOMPLETE`: worker feature removal incomplete: a Docker package or the docker group membership is still there with containers off.
117
+ - `WORKER_SIZE_BELOW_MINIMUM`: worker size below minimum: less memory, disk or CPUs than the minimum worker. **Needs a person.**
118
+
107
119
  ## Containers on a worker
108
120
 
109
121
  - **Meaning:** `worker:docker-run` could not start the pinned container or build a one-line image as the worker user.
@@ -119,6 +131,23 @@ There are 250 codes; 31 need a person.
119
131
  - `DOCKER_SMOKE_IMAGE_MISSING`: docker smoke image missing.
120
132
  - `DOCKER_SOCKET_ACCESS_DENIED`: docker socket access denied.
121
133
 
134
+ ## Exposure of a worker
135
+
136
+ - **Meaning:** Something on the worker is reachable from outside the tailnet, or could be: the fleet firewall (`ansible/ubuntu/tasks/firewall.yml`) is not loaded or kept SSH open, a service listens beyond loopback and the tailnet (`worker:exposure`), or the manager reached a fleet port on the worker's public address (`worker:public-reachability`). The exposure policy is `exposure` in `fleet.json` (fleet-wide under `worker`, per worker beside `network`).
137
+ - **Confirm:** On the worker: `sudo nft list table inet personal_fleet`, `sudo ss -H -l -n -t -u -p` and `sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin'`. From the manager: `devfleet fleet-status --workers`.
138
+ - **Fix:** Run `devfleet fleet-sync`. It closes SSH beyond the tailnet only when the manager's session arrives over the tailnet: point the worker's SSH alias at its tailnet name first. Bind a service to `127.0.0.1` or the tailnet address, or declare its port in `publicPorts`.
139
+ - **Escalate:** A firewall the tailnet path could not verify (the previous rules are back) needs `tailscale status` on the worker; a public SSH port is opened on purpose only with `publicSsh`, by a person.
140
+
141
+ - `FIREWALL_RULES_REJECTED`: firewall rules rejected by `nft --check`; the previous rules stay loaded.
142
+ - `FIREWALL_TAILNET_UNVERIFIED`: the tailnet path did not answer a new probe after the rules loaded; the previous rules were restored.
143
+ - `FIREWALL_UNMANAGED_CONFLICT`: `/etc/personal-fleet/firewall.nft` exists without the fleet's marker. **Needs a person.**
144
+ - `SSH_HARDENING_OVERRIDDEN`: `sshd -T` still allows passwords or root login: an earlier fragment or a Match block wins. **Needs a person.**
145
+ - `WORKER_EXPOSURE_UNOBSERVED`: the addresses, sockets or firewall could not be read with `sudo -n`.
146
+ - `WORKER_FIREWALL_INACTIVE`: the fleet firewall is not loaded.
147
+ - `WORKER_LISTENER_UNRESTRICTED`: a socket listens beyond loopback and the tailnet outside the allow-list; the summary names the port and the owning unit. A fleet service fails; another warns or fails by `otherListeners`.
148
+ - `WORKER_PUBLIC_PORT_UNRESTRICTED`: the manager connected to SSH, T3 or the monitoring port on the worker's public address.
149
+ - `WORKER_PUBLIC_SSH_UNDECLARED`: SSH is still open beyond the tailnet without `publicSsh`, because no session over the tailnet proved the manager's path. **Needs a person.**
150
+
122
151
  ## Headless browser on a worker
123
152
 
124
153
  - **Meaning:** `worker:browser` could not launch the pinned headless Chromium of the web tool profile.
@@ -133,10 +162,10 @@ There are 250 codes; 31 need a person.
133
162
 
134
163
  ## Git identity and pushing
135
164
 
136
- - **Meaning:** Commits on the node would carry the wrong author, or git cannot authenticate to GitHub on its own.
165
+ - **Meaning:** Commits on the node would carry the wrong author, would not be signed although `git.signing` is declared, or git cannot authenticate to GitHub on its own.
137
166
  - **Confirm:** On the node: `git config --global --get user.name`, and `printf "protocol=https\nhost=github.com\n\n" | GIT_TERMINAL_PROMPT=0 git credential fill >/dev/null`.
138
167
  - **Fix:** Declare the `git` block in `fleet.json` and run `devfleet fleet-sync` (`ansible/github/git.yml`). Remove lines in `~/.gitconfig` that override it.
139
- - **Escalate:** GIT_REMOTE_REFUSED means the fleet account lost access to the declared repository: a person grants it.
168
+ - **Escalate:** GIT_REMOTE_REFUSED means the fleet account lost access to the declared repository: a person grants it. GitHub shows signed commits as verified only once a person adds each node's `~/.ssh/dev-fleet-git-signing.pub` to the account as a signing key.
140
169
 
141
170
  - `GIT_CONFIGURATION_INCOMPLETE`: git configuration incomplete.
142
171
  - `GIT_CREDENTIAL_HELPER_MISSING`: git credential helper missing.
@@ -146,6 +175,9 @@ There are 250 codes; 31 need a person.
146
175
  - `GIT_IDENTITY_UNDECLARED`: git identity undeclared.
147
176
  - `GIT_REMOTE_REFUSED`: git remote refused. **Needs a person.**
148
177
  - `GIT_REMOTE_UNREACHABLE`: git remote unreachable.
178
+ - `GIT_SIGNING_FAILED`: git signing failed: the node's own key is missing or cannot sign without a prompt.
179
+ - `GIT_SIGNING_MISSING`: git signing missing.
180
+ - `GIT_SIGNING_OVERRIDDEN`: git signing overridden.
149
181
 
150
182
  ## GitHub sign-in
151
183
 
@@ -180,12 +212,34 @@ There are 250 codes; 31 need a person.
180
212
  - `TAILSCALE_SOURCE_OWNER_CONFLICT`: tailscale source owner conflict. **Needs a person.**
181
213
  - `TAILSCALE_UPGRADE_PENDING`: tailscale upgrade pending. **Needs a person.**
182
214
 
215
+ ## A new worker with no hands on it
216
+
217
+ - **Meaning:** A machine created without a person (from a pipeline or infrastructure-as-code) could not be enrolled, joined to the tailnet, given its account or brought to ready in one run: its first boot is still running or failed, the tailnet control plane refused a key, the image lets only root in and the account could not be created, or its first-converge or maintenance reboot did not happen.
218
+ - **Confirm:** The `bootstrap:first-boot`, `tailscale:enrolment-key`, `worker:account` and `worker:reboot` checks of the report `devfleet fleet-sync --json` prints, or the `tailscale:device` check of `fleet-node-replace` and `fleet-node-retire` and the `tailscale:leftover` check of `devfleet fleet-status`, which ask the same control plane; on the worker, `cloud-init status --long`.
219
+ - **Fix:** FIRST_BOOT_PENDING and WORKER_REBOOT_BUSY clear on their own: run `devfleet fleet-sync` again later. A TAILNET code names what to fix in `tailnet` of `fleet.json` or in the secret `tailnet-admin`; then run `devfleet fleet-sync`.
220
+ - **Escalate:** A first boot that failed means the image or its user data is wrong: recreate the machine. A root login that cannot create the declared account needs a person on the machine (`useradd --create-home --shell /bin/bash --groups sudo USER`).
221
+
222
+ - `FIRST_BOOT_FAILED`: first boot failed: cloud-init ended in an error. **Needs a person.**
223
+ - `FIRST_BOOT_PENDING`: first boot pending: cloud-init still runs; retryable.
224
+ - `TAILNET_ADMIN_REQUIRED`: tailnet admin required: fleet.json declares tailnet without the secret. **Needs a person.**
225
+ - `TAILNET_CONTROL_DENIED`: tailnet control denied the tailnet-admin credential. **Needs a person.**
226
+ - `TAILNET_CONTROL_REJECTED`: tailnet control rejected the request (tag or user). **Needs a person.**
227
+ - `TAILNET_CONTROL_RESPONSE_INVALID`: tailnet control response invalid.
228
+ - `TAILNET_CONTROL_UNAVAILABLE`: tailnet control unavailable.
229
+ - `TAILNET_KEY_DELIVERY_FAILED`: tailnet key delivery failed.
230
+ - `TAILNET_KEY_INVALID`: tailnet key invalid.
231
+ - `WORKER_ACCOUNT_CREATION_FAILED`: worker account creation failed. **Needs a person.**
232
+ - `WORKER_ACCOUNT_ROOT_REQUIRED`: worker account root required. **Needs a person.**
233
+ - `WORKER_ACCOUNT_UNDECLARED`: worker account undeclared in workers[].network.user. **Needs a person.**
234
+ - `WORKER_REBOOT_BUSY`: worker reboot busy: an agent turn runs.
235
+ - `WORKER_REBOOT_FAILED`: worker reboot failed.
236
+
183
237
  ## T3 Code
184
238
 
185
239
  - **Meaning:** The native T3 server or desktop app, its policy, its private route or its providers did not converge or verify.
186
240
  - **Confirm:** `devfleet t3-status` and the `t3:*` checks of `devfleet fleet-status`.
187
241
  - **Fix:** Run `devfleet fleet-sync`, then `devfleet t3-apply` for providers. A MIGRATION or MAINTENANCE code names a one-time step in its remedy.
188
- - **Escalate:** Pairing and relinking a peer needs a person in T3 Settings.
242
+ - **Escalate:** A worker's first pairing, and relinking a peer that was not replaced, need a person in T3 Settings (Connections). A replaced worker is paired by `devfleet fleet-node-replace` or the next `devfleet fleet-sync` (`t3:link`).
189
243
 
190
244
  - `MANAGER_T3_LINK_UNVERIFIED`: manager t3 link unverified.
191
245
  - `T3_BASELINE_PREREQUISITES_REQUIRED`: t3 baseline prerequisites required.
@@ -224,6 +278,8 @@ There are 250 codes; 31 need a person.
224
278
  - `T3_PROVIDER_NAMES_PENDING`: t3 provider names pending.
225
279
  - `T3_PROVIDER_NAMES_UNVERIFIED`: t3 provider names unverified.
226
280
  - `T3_RESTART_PENDING`: t3 restart pending.
281
+ - `T3_SERVER_INSTALL_FAILED`: t3 server install failed: the pinned server of a headless worker is missing from the toolchain, or `t3-headless.yml` did not apply.
282
+ - `T3_SERVER_STOPPED`: t3 server stopped: the lingering `t3code-headless.service` of a headless worker is not running.
227
283
  - `T3_SETTINGS_BUSY`: t3 settings busy.
228
284
 
229
285
  ## Monitoring (Beszel)
@@ -295,10 +351,12 @@ There are 250 codes; 31 need a person.
295
351
  - **Meaning:** An agent CLI, its profile or a provider behind the CLI proxy did not answer as expected.
296
352
  - **Confirm:** The `agent:*` and `provider:*` checks of `devfleet fleet-status`, and `devfleet cliproxy` on the manager.
297
353
  - **Fix:** Run `devfleet fleet-sync`; a rate limit or upstream outage clears on its own.
298
- - **Escalate:** PROVIDER_AUTH_REJECTED needs a person to sign the account in again on the proxy.
354
+ - **Escalate:** PROVIDER_AUTH_REJECTED needs a person to sign the account in again on the proxy. AGENT_PROFILE_RENDER_MISMATCH is a defect of the release (a template of `ansible/agents/agents.yml` no longer renders what `src/agents/render.ts` specifies): report it to dev-fleet. No receipt vouches for what that play wrote, so later applies refuse the file until a person reviews it.
299
355
 
300
356
  - `AGENT_MEMORY_LIMIT_MISSING`: agent memory limit missing.
301
357
  - `AGENT_MODEL_UNSERVED`: agent model unserved.
358
+ - `AGENT_PROFILE_RENDER_MISMATCH`: agent profile render mismatch.
359
+ - `AGENT_PROFILE_REQUEST_INVALID`: agent profile request invalid.
302
360
  - `AGENT_PROFILE_STALE`: agent profile stale.
303
361
  - `AGENT_SERVICE_ENVIRONMENT_INCOMPLETE`: agent service environment incomplete.
304
362
  - `AGENT_SERVICE_ENVIRONMENT_UNVERIFIED`: agent service environment unverified.
@@ -338,7 +396,7 @@ There are 250 codes; 31 need a person.
338
396
  ## Vault and credentials
339
397
 
340
398
  - **Meaning:** A machine cannot read the secrets it needs with its own Vault session.
341
- - **Confirm:** `vault token lookup` on the machine and the `worker:vault-renewal` check.
399
+ - **Confirm:** `vault token lookup` on the machine and the `worker:vault-renewal` or `manager:vault-renewal` check.
342
400
  - **Fix:** Log the machine in once (`devfleet fleet-sync` asks on the manager); renewal is automatic afterwards.
343
401
  - **Escalate:** A refused policy needs the Vault administrator.
344
402
 
@@ -346,6 +404,8 @@ There are 250 codes; 31 need a person.
346
404
  - `VAULT_LOGIN_REQUIRED`: vault login required. **Needs a person.**
347
405
  - `VAULT_POLICY_PERMISSION_REQUIRED`: vault policy permission required. **Needs a person.**
348
406
  - `VAULT_RENEWAL_FAILING`: vault renewal failing.
407
+ - `VAULT_RENEWAL_MISSING`: vault renewal missing: the manager's renewal job is not loaded. Run `devfleet fleet-sync` on the manager.
408
+ - `VAULT_RENEWAL_NOT_MANAGER`: vault renewal not manager.
349
409
  - `VAULT_WORKER_POLICY_FAILED`: vault worker policy failed.
350
410
 
351
411
  ## The fleet definition and a worker's lifecycle
@@ -361,6 +421,23 @@ There are 250 codes; 31 need a person.
361
421
  - `DEFINITION_UNREADABLE`: definition unreadable.
362
422
  - `SSH_HOST_KEY_NOT_CHANGED`: ssh host key not changed.
363
423
 
424
+ ## Parked, idle, replaced and retired workers
425
+
426
+ - **Meaning:** a worker could not be parked or resumed (agent turns under way, a declared `parking.stop` or `parking.start` command failed, the sync after a resume did not end ready), a worker whose record disappeared could not be retired, or the manager's T3 link to a replaced or retired worker could not be removed, or the manager's T3 could not pair with a replaced worker's new environment.
427
+ - **Confirm:** `devfleet fleet-status --workers` lists each worker as active or parked, with its uptime, agent turns and last agent activity; the report of `devfleet fleet-node-park`, `devfleet fleet-node-resume` or `devfleet fleet-sync` names the step that failed.
428
+ - **Fix:** For WORKER_BUSY wait for the turns to end, or repeat `devfleet fleet-node-park` with `--force`. For a failed hook, run the command `fleet.json` names under `parking` by hand with the worker ID, then repeat. For WORKER_RESUME_FAILED follow the sync report and run `devfleet fleet-sync --node ID`. For T3_LINK_UNLINK_FAILED remove the link in the manager's T3 (Settings, Connections). For T3_LINK_PAIRING_FAILED follow the remedy, which names why the link was refused (Linked environments off in the T3 policy, the manager's T3 not running, a worker T3 that did not mint a code), then run `devfleet fleet-sync --node ID`: it pairs a replaced worker until it is paired.
429
+ - **Escalate:** A record retirement that keeps failing at the same step: follow that step's own runbook.
430
+
431
+ - `T3_LINK_PAIRING_FAILED`: t3 link pairing failed: the manager's T3 could not pair with a replaced worker's new T3 environment. The pairing code never appears in a report.
432
+ - `T3_LINK_UNLINK_FAILED`: t3 link unlink failed.
433
+ - `WORKER_ACTIVITY_UNREADABLE`: worker activity unreadable. **Needs a person.**
434
+ - `WORKER_AUTO_PARK_REFUSED`: worker auto park refused.
435
+ - `WORKER_BUSY`: worker busy. **Needs a person.**
436
+ - `WORKER_RECORD_RETIREMENT_FAILED`: worker record retirement failed.
437
+ - `WORKER_RESUME_FAILED`: worker resume failed.
438
+ - `WORKER_START_HOOK_FAILED`: worker start hook failed.
439
+ - `WORKER_STOP_HOOK_FAILED`: worker stop hook failed.
440
+
364
441
  ## Fleet operations and the fleet state
365
442
 
366
443
  - **Meaning:** fleet-sync could not plan, lock, reach a machine, promote a revision or keep the fleet state.
@@ -380,6 +457,7 @@ There are 250 codes; 31 need a person.
380
457
  - `FLEET_REMOTE_LOCKED`: fleet remote locked.
381
458
  - `FLEET_REMOTE_OPERATION_FAILED`: fleet remote operation failed.
382
459
  - `FLEET_REVISION_REQUIRED`: fleet revision required.
460
+ - `FLEET_ROLLBACK_UNAVAILABLE`: fleet rollback unavailable.
383
461
  - `FLEET_SCHEDULE_PREVIEW`: fleet schedule preview.
384
462
  - `FLEET_SOURCE_UNAVAILABLE`: fleet source unavailable.
385
463
  - `FLEET_STATE_DIVERGED`: fleet state diverged. **Needs a person.**
@@ -394,6 +472,19 @@ There are 250 codes; 31 need a person.
394
472
  - `REMOTE_CONFIGURATION_INCOMPLETE`: remote configuration incomplete.
395
473
  - `REMOTE_PHASE_FAILED`: remote phase failed.
396
474
 
475
+ ## The manager's own policy and scheduled jobs
476
+
477
+ - **Meaning:** `ansible/macos/manager.yml` did not pass its check-mode run or its apply, or something it declares (network time, the fleet-watch job, the rotation of the scheduled jobs' logs) is not in place.
478
+ - **Confirm:** The `manager:playbook`, `manager:clock`, `manager:watch` and `manager:job-logs` checks of `devfleet fleet-sync --dry-run`; on the manager, the check-mode run the remedy quotes.
479
+ - **Fix:** Run `devfleet fleet-sync`, which applies the playbook in check mode first, then for real. What needs root (power settings, reading network time, newsyslog) comes from the one grant: `ansible-playbook -e user=$(id -un) -K ansible/macos/manager-privilege.yml`, run again after an upgrade that changed it.
480
+ - **Escalate:** The grant needs an administrator's password, once.
481
+
482
+ - `MANAGER_CLOCK_UNOBSERVED`: manager clock unobserved: the grant that lets fleet-sync read network time is missing. **Needs a person.**
483
+ - `MANAGER_JOB_LOG_ROTATION_MISSING`: manager job log rotation missing: the newsyslog drop-in the grant installs is missing or outdated. **Needs a person.**
484
+ - `MANAGER_PLAYBOOK_CHECK_FAILED`: manager playbook check failed: nothing was applied.
485
+ - `MANAGER_PLAYBOOK_FAILED`: manager playbook failed.
486
+ - `MANAGER_WATCH_INTERVAL_INVALID`: manager watch interval invalid: `manager.schedule.watchMinutes` is outside 5 to 1440.
487
+
397
488
  ## Notifications (fleet-watch)
398
489
 
399
490
  - **Meaning:** fleet-watch could not reach a notification target `notify` in `fleet.json` names, or its address is not stored as a secret.
package/docs/state.md CHANGED
@@ -4,12 +4,13 @@ A fleet is described in two places you own. Your project in Git holds
4
4
  `fleet.json` and pins the dev-fleet version. Everything else the manager
5
5
  cannot rebuild from Git is the **fleet state**:
6
6
 
7
- | Document | What it is |
8
- | ------------ | --------------------------------------------------------------------- |
9
- | `roster` | The enrolled connection to each worker (SSH alias, Tailscale device) |
10
- | `enrollment` | The revision `fleet-sync` approved and the one it last applied |
11
- | `definition` | The fleet configuration `fleet-sync` last applied: your `fleet.json` |
12
- | `history` | The last 90 days of `fleet-sync` runs (at most 500), and the schedule |
7
+ | Document | What it is |
8
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `roster` | The enrolled connection to each worker (SSH alias, Tailscale device) |
10
+ | `enrollment` | The revision `fleet-sync` approved and the one it last applied |
11
+ | `definition` | The fleet configuration `fleet-sync` last applied: your `fleet.json` |
12
+ | `history` | The last 90 days of `fleet-sync` runs (at most 500), and the schedule |
13
+ | `workers` | Per worker: its pinned SSH host key, whether it is parked and since when, whether it came from a worker record, and whether the manager is to pair its T3 after a replacement |
13
14
 
14
15
  `devfleet fleet-watch` also keeps `watch.json` in the bucket, beside the
15
16
  database: the health of each worker when it last looked, and when that was
@@ -195,6 +196,12 @@ state that is replicated differently or not at all:
195
196
  | Monitoring hub (Beszel) database | `~/Library/Application Support/personal-beszel` | `fleet-sync` recreates accounts and systems; metric history, alerts and settings made by hand are lost |
196
197
  | T3 ownership record | `~/.t3/userdata/personal-managed.json` | `t3-apply --import ID` adopts instances again |
197
198
 
199
+ The manager reaches a worker that has no SSH alias against
200
+ `~/.local/share/dev-fleet/ssh/known_hosts` (beside the state directory): a
201
+ cache rendered from the host keys the `workers` document pins and the worker
202
+ records declare, rewritten before every session. Delete it and the next
203
+ command writes it again; nothing is ever learned into it.
204
+
198
205
  Everything else under `~/.local/share/personal/fleet` and
199
206
  `~/.local/state/personal/fleet` (the copy of the source repository, run
200
207
  checkouts, reports, receipts, logs) is a cache that the next `fleet-sync`
@@ -257,9 +264,14 @@ mise exec litestream -- litestream restore -no-expand-env \
257
264
  ```
258
265
 
259
266
  `devfleet fleet-state --restore --timestamp 2026-11-01T00:00:00Z --yes` does
260
- the same and then uploads that version as the bucket's current one. It is the
261
- remedy for a `corrupt` replica: the bucket answers, but its latest version
262
- cannot be read.
267
+ the same and then uploads that version as the bucket's current one, which
268
+ takes the manager lease. It is the remedy for a `corrupt` replica: the bucket
269
+ answers, but its latest version cannot be read. `--yes` consents to replacing
270
+ the bucket, not to replacing another manager: while one holds the lease in the
271
+ bucket's current version, the restore stops with `FLEET_MANAGER_LEASE_HELD`
272
+ before it changes anything, and `--take-over` overrides it. When the current
273
+ version cannot be read, its lease cannot either, and the lease in this
274
+ machine's copy (the last one it saw) decides instead.
263
275
 
264
276
  Litestream is pinned in `mise.toml` and installed the first time it is used.
265
277
  The manager is the only machine that runs it.
@@ -274,8 +286,8 @@ writes anything and stops with `FLEET_MANAGER_LEASE_HELD`. `fleet-state
274
286
  --init` still restores the state on such a machine; it only writes nothing.
275
287
 
276
288
  The lease runs for seven days after the last change. After that, any machine
277
- may take it. Before that, `--take-over` (on `fleet-sync`, `fleet-state --init`
278
- or `fleet-state --import`) makes this machine the manager; use it only once
289
+ may take it. Before that, `--take-over` (on `fleet-sync`, `fleet-state --init`,
290
+ `fleet-state --import` or `fleet-state --restore --timestamp`) makes this machine the manager; use it only once
279
291
  the other one is gone ([replacing the manager](manager-recovery.md)).
280
292
 
281
293
  A machine is named in the lease by a digest of its machine ID
@@ -13,11 +13,12 @@ rest. The default is one macOS manager and Ubuntu workers treated as cattle.
13
13
  | -------------- | --------- | ------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------- |
14
14
  | manager | macOS | 14 or newer | arm64, x86_64 | supported | ansible/macos/policy.json |
15
15
  | worker | Ubuntu | 26.04 (resolute) | amd64, arm64 | supported | ansible/ubuntu/policy.json; a machine of its own, physical or virtual |
16
+ | worker | Ubuntu | 24.04 (noble) | amd64, arm64 | supported | ansible/ubuntu/policy.json; a machine of its own, physical or virtual |
16
17
  | manager | any other | any | any | refused | The fleet configuration accepts only a macOS manager. |
17
18
  | worker | Ubuntu | any other release | any | refused | UBUNTU_BASELINE_MISMATCH |
18
19
  | worker | Ubuntu | an accepted release | any other | refused | UBUNTU_BASELINE_MISMATCH |
19
- | worker | Debian | any | any | refused | UBUNTU_BASELINE_MISMATCH |
20
- | worker | macOS | any | any | refused | The fleet configuration accepts only Linux workers. |
20
+ | worker | Debian | any | any | refused | UBUNTU_BASELINE_MISMATCH; not yet supported (see below). |
21
+ | worker | macOS | any | any | refused | The fleet configuration accepts only Linux workers; not yet supported (see below). |
21
22
  | worker | Windows | any | any | refused | The fleet configuration accepts only Linux workers. |
22
23
  | worker | WSL2 | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
23
24
  | worker | container | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
@@ -32,5 +33,40 @@ refuses a worker that is not Linux, the platform probe refuses another
32
33
  release, CPU or environment (`ubuntu:release`, `platform:environment`), and
33
34
  the bootstrap and the Ubuntu baseline refuse them again on the machine.
34
35
 
35
- Supporting another platform means adding it to the policy that names it, with
36
- its package pins, a fresh-box job and its row here.
36
+ ## Adding a platform or a release
37
+
38
+ What differs from one worker platform to another is named by the
39
+ `WorkerPlatform` interface (`src/platform/worker-platform.ts`): its policy and
40
+ playbooks, its package manager and pins, its Tailscale key, its service
41
+ manager, its session model and its host checks. Ubuntu is the one
42
+ implementation (`src/platform/ubuntu-worker.ts`).
43
+
44
+ Another Ubuntu release is one entry in `releases` of
45
+ `ansible/ubuntu/policy.json`: its version, codename and architectures, its
46
+ package and browser library pins read from the snapshot's Packages indexes
47
+ for that release (`packages`, `browserPackages`), and `tailscaleKeySha256`
48
+ only when Tailscale signs its suite with another key than the common one.
49
+ `ansible/ubuntu/tasks/release.yml` selects the entry from the facts, so the
50
+ playbooks need no change of their own. The release is supported once the
51
+ fresh-box features pass on it: CI runs them for every accepted release
52
+ (`DEVFLEET_TEST_FRESH_BOX_RELEASE`), with its image pinned in
53
+ `features/fresh-box/Dockerfile`.
54
+
55
+ ## Not yet supported
56
+
57
+ These are refused today. What each would need, beyond a row here:
58
+
59
+ - **Debian workers.** A `WorkerPlatform` of its own with a Debian policy:
60
+ pins read from snapshot.debian.org for each release, Tailscale's key digest
61
+ for the Debian suite, the platform probe accepting `ID=debian`, and the
62
+ bootstrap and `tasks/release.yml` accepting it in place of
63
+ `UBUNTU_BASELINE_MISMATCH`. The playbooks assume Ubuntu's archive keyring,
64
+ snapshot service and unattended-upgrades origins, and the GNOME desktop
65
+ T3 runs in; each needs a Debian equivalent. Then a fresh-box image and job.
66
+ - **macOS workers.** Everything a worker platform names differs: launchd in
67
+ place of systemd as the service manager, a logged-in user session in place
68
+ of a lingering systemd user manager, Homebrew or pinned downloads in place
69
+ of APT, power and sleep policy through `pmset`, and host checks of their
70
+ own. The fleet configuration would accept `macOS` for a worker, and the
71
+ convergence would run playbooks under `ansible/macos` for it. No container
72
+ can stand in for a fresh macOS machine, so the test would be a macOS runner.
package/docs/topology.md CHANGED
@@ -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
  # Fleet topology: one manager, many workers
6
7
 
@@ -8,10 +9,10 @@ The fleet has exactly two roles, declared in
8
9
  your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.schema.json)) and validated by
9
10
  [`src/definition.ts`](../src/definition.ts):
10
11
 
11
- | Role | How many | Machine | Holds state | Runs |
12
- | ----------- | ----------- | ------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- |
13
- | **manager** | exactly one | the human's macOS machine (`manager`) | yes | `fleet-sync`, the T3 desktop that links to every worker, and **every singleton service** |
14
- | **workers** | any | Linux boxes (`workers`, Ubuntu 26.04) | no | the T3 desktop app (UI plus server), agent tools, the Beszel agent; clients only |
12
+ | Role | How many | Machine | Holds state | Runs |
13
+ | ----------- | ----------- | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- |
14
+ | **manager** | exactly one | the human's macOS machine (`manager`) | yes | `fleet-sync`, the T3 desktop that links to every worker, and **every singleton service** |
15
+ | **workers** | any | Linux boxes (`workers`, Ubuntu 26.04 or 24.04) | no | the T3 desktop app (UI plus server), agent tools, the Beszel agent; clients only |
15
16
 
16
17
  ## Manager
17
18
 
@@ -51,7 +52,9 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
51
52
 
52
53
  - Stateless cattle: turn one off, re-image it or add another at will. A worker
53
54
  is declared in `workers`, enrolled once (SSH trust, sudo, its own Vault
54
- token), and then fully rebuilt by `fleet-sync` from Git and Vault.
55
+ token), and then fully rebuilt by `fleet-sync` from Git and Vault. What a
56
+ machine must offer before that is listed in
57
+ [worker requirements](worker-requirements.md).
55
58
  - Workers never host a singleton service. The proxy reports any local install
56
59
  on a worker as a duplicate owner, and the hub refuses to install anywhere but
57
60
  the manager.
@@ -73,5 +76,63 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
73
76
 
74
77
  - **Add or replace a worker:** add it to `workers`, enroll it with
75
78
  `fleet-sync`, done. Remove it by deleting the entry.
79
+ - **Workers an infrastructure tool creates:** `workerRecords` in `fleet.json`
80
+ names a file (relative to `fleet.json`) or a command that returns worker
81
+ records, the provider-neutral description of a machine in
82
+ [`schema/worker-record.schema.json`](../schema/worker-record.schema.json):
83
+ `id`, `tailnetName` or `address`, `user`, `port`, `hostKey` and optional
84
+ `labels` and `billing`. Their workers join the inline list; dev-fleet only
85
+ reads them and never calls a provider or creates a machine. A record that
86
+ appears is a declared worker, enrolled by the next `fleet-sync`; one that
87
+ disappears (a file that is gone holds none) is retired by the next
88
+ `fleet-sync` exactly as `fleet-node-retire` retires one. A worker listed by
89
+ hand is never retired by itself. An invalid record is refused naming its
90
+ field, such as `$.workerRecords[0].workers[1].hostKey`.
91
+ [`examples/workers-from-infrastructure`](../examples/workers-from-infrastructure/main.tf)
92
+ shows the handoff with a local virtual machine, the seed going in and the
93
+ record coming out. The canary stays the one `canary` names, so the order
94
+ of generated records means nothing.
95
+ - **Replace or retire a worker:** `fleet-node-replace` and `fleet-node-retire`
96
+ take everything of the old machine with it: its pinned host key, its
97
+ monitoring system (pointed at the new machine, or deleted), the manager's
98
+ T3 link to its environment, its Vault token and its tailnet device. The
99
+ manager's T3 pairs with the replacement's environment by itself
100
+ (`t3:link`): at once when its T3 runs, otherwise in the next `fleet-sync`
101
+ once it does, and the roster records the new environment. The pairing code
102
+ is minted on the worker over SSH and handed to the manager's own T3 server
103
+ in a request body, never on a command line, in a report or in a log. The device is deleted through the
104
+ control plane fleet.json declares under `tailnet`, asked with the secret
105
+ `tailnet-admin` (workers are not registered as ephemeral devices); without
106
+ either, `tailscale:device` is skipped and names the device to remove in the
107
+ admin console. `fleet-status --workers` warns about a monitoring system that
108
+ matches no declared worker (`beszel:leftover`), and `fleet-status` about a
109
+ device carrying the worker tag that is no declared worker, parked or not,
110
+ and no device the roster binds (`tailscale:leftover`).
111
+
112
+ ## Parked and idle workers
113
+
114
+ A rented worker costs money while it runs. `devfleet fleet-node-park --id ID`
115
+ records in the fleet state that it is stopped or destroyed on purpose: from
116
+ then on `fleet-sync` and `fleet-status` list it as parked with the time since
117
+ (`worker:parked`, not applicable), never contact it, never make it the
118
+ canary, and it never changes an exit code. Parking refuses while agent turns
119
+ run on it (`WORKER_BUSY`) unless `--force`. `devfleet fleet-node-resume --id
120
+ ID` marks it active and runs `fleet-sync --node ID`, which converges and
121
+ verifies it. `parking.stop` and `parking.start` in `fleet.json` name commands
122
+ (an infrastructure tool, never a provider dev-fleet calls) run with the worker
123
+ ID as their last argument. A parked worker found online on the tailnet is a
124
+ warning (`worker:parked-online`); one that comes back as a new machine goes
125
+ through `fleet-node-replace`.
126
+
127
+ `fleet-status` reports, for every worker it reaches, its uptime, the agent
128
+ turns under way and the time of the last agent activity, read from what T3
129
+ records (status and times, never a session's content), in its text and as
130
+ `workers` in its JSON. With `parking.idleAfterHours`, a worker with no agent
131
+ activity for longer is `worker:idle` (a warning whose remedy is to park it);
132
+ one with a turn under way never is. `billing.hourlyRate` on a worker (or its
133
+ record) is only displayed and summed: `usage` says how many workers run, how
134
+ many are idle and what the idle ones cost per day. `parking.autoPark: true`
135
+ lets `fleet-watch` park idle workers by itself; it is off unless set.
136
+
76
137
  - **Replace the manager:** follow
77
138
  [replacing the manager](manager-recovery.md).