@crvouga/dev-fleet 0.8.0 → 0.9.1

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 (205) 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-capability-checks.ts +55 -16
  193. package/src/worker-capacity.ts +31 -0
  194. package/src/worker-exposure.ts +388 -0
  195. package/src/worker-features.ts +201 -0
  196. package/src/worker-host-checks.ts +71 -20
  197. package/src/worker-manifest.ts +27 -4
  198. package/src/worker-parking.ts +70 -0
  199. package/src/worker-policy.ts +17 -1
  200. package/src/worker-readiness.ts +15 -3
  201. package/src/worker-reboot.ts +129 -0
  202. package/src/worker-records.ts +305 -0
  203. package/src/worker-seed.ts +16 -11
  204. package/src/worker-sizing.ts +76 -0
  205. 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: one manager (macOS) and stateless workers (Ubuntu)
6
7
 
@@ -10,7 +11,7 @@ What is proven, and where:
10
11
 
11
12
  | Evidence | Where |
12
13
  | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
13
- | A bare Ubuntu 26.04 machine bootstrapped over real SSH, then repeated | `features/fresh-box/fresh-box.feature`, on every pull request (amd64 and arm64) |
14
+ | A bare Ubuntu machine bootstrapped over real SSH, then repeated | `features/fresh-box/fresh-box.feature`, on every pull request (each accepted release, amd64 and arm64) |
14
15
  | One run over several machines, one versioned report, no partial receipt | `features/fresh-box/fleet.feature` |
15
16
  | Interrupted checkout, package installation and toolchain setup resumed | `features/fresh-box/fleet.feature` |
16
17
  | Overlapping runs, a dropped connection, a held APT lock, no DNS, no disk | `features/fresh-box/fleet.feature` |
@@ -86,9 +87,11 @@ existing SSH aliases, tests strict SSH access, and offers one enrollment
86
87
  confirmation for the verified connections. The apply approval binds a fingerprint
87
88
  of the selected revision, policy and connection records; changed bindings fail
88
89
  with `FLEET_PLAN_STALE` before source preparation or remote mutation. It stores private overrides mode 0600. Unknown host keys are never accepted automatically. Compare fingerprints
89
- out of band before enrolling trust. Unreachable machines remain listed; first
90
- reachability requires an existing trusted LAN SSH route or physical setup of
91
- SSH/Tailscale. No software on the manager can bootstrap a disconnected box.
90
+ out of band before enrolling trust. Unreachable machines remain listed; what a machine
91
+ needs to be reachable the first time (SSH with a key, a known host key, the
92
+ tailnet, or first-boot data that provides them) is listed in
93
+ [worker requirements](worker-requirements.md). No software on the manager can
94
+ bootstrap a disconnected box.
92
95
 
93
96
  A previously configured manager uses saved connection bindings and its approved
94
97
  revision. A changed Tailscale device ID is a replacement gate, even if the old
@@ -177,21 +180,21 @@ independently. Bespoke TypeScript remains only where a native CLI or API has no
177
180
  declarative owner: T3 service lifecycle, Beszel enrollment tokens, provider
178
181
  verification and the CLIProxy singleton.
179
182
 
180
- | Policy | Workers (Ubuntu 26.04, stateless) | Manager (macOS, singleton) |
181
- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
- | Never sleep | systemd sleep targets masked; logind lid/idle policy; GNOME power plugin set to `nothing` | never sleeps on AC power (`pmset -c sleep 0`, `macos/manager.yml`, verified as `manager:power`); battery sleep is left to the human |
183
- | No automatic logout or lock | GNOME `lock-enabled false`, `idle-delay 0`; designated-user lingering | not managed (the human uses it) |
184
- | Always-on services | drop-ins `Restart=always`, `StartLimitIntervalSec=0` for ssh, tailscaled, docker, beszel-agent | singleton services under launchd and Docker Desktop (see the last row) |
185
- | T3 health and connection | `fleet-sync` and `fleet-status --workers` require `worker:t3-health` (local API answers), `t3:manager-endpoint` (manager HTTPS over Tailscale) and `t3:paired-access` (manager T3 link reachable) | the human's T3 desktop holds the links to every worker (`t3:paired-access`) |
186
- | Memory safety | `systemd-oomd` (swap 90%, pressure 60%); `user.slice` `MemoryMax=90%`; each `t3code-*.scope` agent `MemoryMax=30%`, `OOMPolicy=continue`; swap file when none | not managed |
187
- | Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation | not managed |
188
- | Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
189
- | Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
190
- | Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md)) | the human's own Vault and GitHub sessions |
191
- | Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | only `pmset -c`, granted once by `manager-privilege.yml` (`-K`) |
192
- | T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
193
- | T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy | reconciled into the human's T3 settings by `fleet-sync` (`t3:providers`) with the ownership-safe `t3-apply` engine, so every T3 offers the identical set; unrelated and unmanaged instances are preserved. `fleet-sync` also applies the same proxy agent profiles (`configuration:apply`) and verifies the shared Vault gateway credential end to end with `provider:*:inference`, exactly as on workers |
194
- | Singleton services | never; clients of the manager's services only | every singleton runs here: CLI proxy (`cliproxy`), Beszel hub (`workstation/monitoring`) |
183
+ | Policy | Workers (Ubuntu 26.04 or 24.04, stateless) | Manager (macOS, singleton) |
184
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | Never sleep | systemd sleep targets masked; logind lid/idle policy; GNOME power plugin set to `nothing` | never sleeps on AC power (`pmset -c sleep 0`, `macos/manager.yml`, verified as `manager:power`); battery sleep is left to the human |
186
+ | No automatic logout or lock | GNOME `lock-enabled false`, `idle-delay 0`; designated-user lingering | not managed (the human uses it) |
187
+ | Always-on services | drop-ins `Restart=always`, `StartLimitIntervalSec=0` for ssh, tailscaled, docker, beszel-agent | singleton services under launchd and Docker Desktop (see the last row) |
188
+ | T3 health and connection | `fleet-sync` and `fleet-status --workers` require `worker:t3-health` (local API answers), `t3:manager-endpoint` (manager HTTPS over Tailscale) and `t3:paired-access` (manager T3 link reachable) | the human's T3 desktop holds the links to every worker (`t3:paired-access`) |
189
+ | Memory safety | `systemd-oomd` (swap 90%, pressure 60%); `user.slice` `MemoryMax=90%`; each `t3code-*.scope` agent `MemoryMax=30%`, `OOMPolicy=continue`; swap file when none | not managed |
190
+ | Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation | not managed |
191
+ | Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
192
+ | Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
193
+ | Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md)) | the human's own Vault and GitHub sessions |
194
+ | Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | only `pmset -c`, granted once by `manager-privilege.yml` (`-K`) |
195
+ | T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server) on a desktop worker, and the pinned server as the lingering user unit `t3code-headless.service` on a headless one (`features.session`; no session is waited for); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
196
+ | T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy | reconciled into the human's T3 settings by `fleet-sync` (`t3:providers`) with the ownership-safe `t3-apply` engine, so every T3 offers the identical set; unrelated and unmanaged instances are preserved. `fleet-sync` also applies the same proxy agent profiles (`configuration:apply`) and verifies the shared Vault gateway credential end to end with `provider:*:inference`, exactly as on workers |
197
+ | Singleton services | never; clients of the manager's services only | every singleton runs here: CLI proxy (`cliproxy`), Beszel hub (`workstation/monitoring`) |
195
198
 
196
199
  Credential sync is deliberately indirect: no secret is copied between machines.
197
200
  Each worker has a Vault token of its own, kept in a document of its own,
@@ -236,14 +239,58 @@ GNOME lock/idle changes apply to the designated user only. Effective running
236
239
  session/logind behavior still needs a maintenance/reboot acceptance check; writing
237
240
  a file does not prove logout/reboot survival. The sudoers fragment is validated
238
241
  with visudo before installation; this user and its agents can administer the
239
- machine. SSH authentication remains enabled.
242
+ machine.
243
+
244
+ SSH changes validate the complete configuration before reload and restore the
245
+ previous fragments on validation failure. sshd takes keys only and refuses
246
+ root (`10-personal-fleet-exposure.conf`, which sorts before an image's own
247
+ fragments; `SSH_HARDENING_OVERRIDDEN` when `sshd -T` still shows otherwise).
248
+ Existing listeners and socket activation are preserved; unmanaged fragments
249
+ require review. The manager makes a **new** strict SSH connection after apply.
250
+
251
+ ### Exposure
252
+
253
+ Every worker, with a public address or without one, admits inbound traffic
254
+ only over loopback and the tailnet (`ansible/ubuntu/tasks/firewall.yml`, the
255
+ `inet personal_fleet` nftables table, loaded at boot by
256
+ `personal-fleet-firewall.service`). Open beyond it: tailscaled's UDP port,
257
+ DHCP replies, ICMP, and what the exposure policy opens. Where Docker is
258
+ installed, a published container port is dropped from outside the tailnet
259
+ before Docker's own chains see it. Docker's and Tailscale's tables are left to
260
+ their owners.
261
+
262
+ The policy is `exposure` in `fleet.json`, fleet-wide under `worker` and per
263
+ worker beside `network`; a field a worker names replaces the fleet-wide one:
264
+
265
+ ```json
266
+ "exposure": {
267
+ "publicSsh": false,
268
+ "publicPorts": [{ "protocol": "udp", "port": 51820 }],
269
+ "otherListeners": "warn"
270
+ }
271
+ ```
240
272
 
241
- SSH forwarding changes validate the complete configuration before reload and
242
- restore the previous fragment on validation failure. Existing listeners,
243
- authentication and firewall rules are preserved. Existing socket activation is preserved for forwarding-only changes; listener
244
- migrations and unmanaged fragments require review. The manager makes a **new**
245
- strict SSH connection after apply. Private-listener/firewall acceptance and
246
- failure-after-reload recovery still require disposable integration testing.
273
+ `publicSsh` is break-glass access: SSH stays open beyond the tailnet.
274
+ Without it the public SSH port closes only when the session running the sync
275
+ arrived over the tailnet, after the tailnet path answers a new probe and sshd
276
+ a new connection on the tailnet address; a failed verification restores the
277
+ previous rules (`FIREWALL_TAILNET_UNVERIFIED`). A session from a public
278
+ address, a tailnet that is not up, or a run without a session that finds the
279
+ port open keeps it open, and `worker:exposure` reports
280
+ `WORKER_PUBLIC_SSH_UNDECLARED` until the manager's SSH alias names the
281
+ worker's tailnet name.
282
+
283
+ `worker:exposure` (on the worker) lists listening sockets and fails on one
284
+ bound beyond loopback and the tailnet outside the allow-list (sshd, tailscaled,
285
+ the system's DHCP and name-resolution listeners, `publicPorts`), naming the
286
+ port and its unit. A fleet service there always fails; anything else, such as
287
+ an agent's development server, warns or fails by `otherListeners`.
288
+ `worker:public-reachability` (from the manager, in `fleet-status --workers`)
289
+ tries the SSH, T3 and monitoring ports on the public address Tailscale knows
290
+ for the worker; it never blocks readiness, because the manager may share the
291
+ worker's network. Both are not applicable without an address outside the
292
+ tailnet. Closing the public path on a machine with two interfaces still needs
293
+ the disposable VM or a real machine to prove.
247
294
 
248
295
  The manager is the only configuration coordinator. No per-node pull timer is
249
296
  installed. Manager schedule controls use the existing platform adapter:
@@ -327,7 +374,10 @@ A worker whose T3 has not loaded the staged provider policy, or runs an older
327
374
  release than the one installed, is restarted only while it runs no agent turn;
328
375
  otherwise sync reports the pending restart and the recovery timer applies it
329
376
  at the first idle check, failing the check after
330
- `t3RestartMaxDeferralHours` (`ansible/ubuntu/policy.json`). A pending proxy
377
+ `t3RestartMaxDeferralHours` (`ansible/ubuntu/policy.json`).
378
+ `fleet-sync --restart-busy` passes `t3_restart_busy` to the plays, which then
379
+ restart a busy T3 at once and end its turns; the `--dry-run` plan says, per
380
+ T3 restart, whether running turns end. A pending proxy
331
381
  update is installed even when it recreates the gateway. The manager finishes before the workers
332
382
  start, so they are verified against the updated proxy. `--dry-run` previews.
333
383
 
@@ -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,