@crvouga/dev-fleet 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +88 -7
  2. package/agents/audit.md +18 -18
  3. package/ansible/agents/agents.yml +134 -0
  4. package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
  5. package/ansible/agents/templates/claude.settings.json.j2 +3 -0
  6. package/ansible/agents/templates/codex.config.toml.j2 +11 -0
  7. package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
  8. package/ansible/agents/templates/grok.config.toml.j2 +19 -0
  9. package/ansible/agents/templates/opencode.json.j2 +24 -0
  10. package/ansible/agents/templates/receipt.json.j2 +10 -0
  11. package/ansible/ansible.cfg +14 -0
  12. package/ansible/github/git.yml +82 -0
  13. package/ansible/macos/manager-privilege.yml +83 -0
  14. package/ansible/macos/manager.yml +164 -9
  15. package/ansible/macos/policy.json +13 -0
  16. package/ansible/macos/t3-policy.yml +5 -3
  17. package/ansible/macos/vault-renew.yml +85 -0
  18. package/ansible/macos/worker-host-key.yml +33 -0
  19. package/ansible/monitoring/agent-darwin.yml +3 -0
  20. package/ansible/monitoring/agent.yml +2 -0
  21. package/ansible/monitoring/hub-darwin.yml +3 -0
  22. package/ansible/monitoring/hub-route-darwin.yml +1 -0
  23. package/ansible/net/remote-probe.sh +4 -1
  24. package/ansible/platforms.json +2 -2
  25. package/ansible/pyproject.toml +5 -0
  26. package/ansible/ubuntu/baseline.yml +39 -13
  27. package/ansible/ubuntu/disk-gc.py +6 -2
  28. package/ansible/ubuntu/firewall.nft.j2 +50 -0
  29. package/ansible/ubuntu/policy.json +125 -6
  30. package/ansible/ubuntu/reboot.yml +89 -0
  31. package/ansible/ubuntu/recovery.yml +13 -3
  32. package/ansible/ubuntu/t3/policy.json +3 -0
  33. package/ansible/ubuntu/t3-desktop.yml +23 -2
  34. package/ansible/ubuntu/t3-headless.yml +128 -0
  35. package/ansible/ubuntu/t3-health.sh +24 -10
  36. package/ansible/ubuntu/t3-idle.py +37 -2
  37. package/ansible/ubuntu/t3-policy.yml +4 -1
  38. package/ansible/ubuntu/t3-route.yml +1 -0
  39. package/ansible/ubuntu/tailscale.yml +54 -8
  40. package/ansible/ubuntu/tasks/browser.yml +3 -0
  41. package/ansible/ubuntu/tasks/containers-off.yml +43 -0
  42. package/ansible/ubuntu/tasks/docker.yml +53 -6
  43. package/ansible/ubuntu/tasks/dpkg.yml +4 -0
  44. package/ansible/ubuntu/tasks/features.yml +22 -0
  45. package/ansible/ubuntu/tasks/firewall.yml +172 -0
  46. package/ansible/ubuntu/tasks/install.yml +2 -1
  47. package/ansible/ubuntu/tasks/packages.yml +4 -2
  48. package/ansible/ubuntu/tasks/release.yml +6 -0
  49. package/ansible/ubuntu/tasks/resources.yml +6 -4
  50. package/ansible/ubuntu/tasks/services.yml +2 -2
  51. package/ansible/ubuntu/tasks/sizing.yml +49 -0
  52. package/ansible/ubuntu/tasks/snapshot.yml +14 -3
  53. package/ansible/ubuntu/tasks/ssh.yml +61 -4
  54. package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
  55. package/ansible/ubuntu/tasks/user.yml +1 -0
  56. package/ansible/ubuntu/worker-account.yml +117 -0
  57. package/ansible/uv.lock +265 -0
  58. package/bin/devfleet.mjs +42 -4
  59. package/bootstrap/fleet-remote-bootstrap.sh +91 -25
  60. package/bootstrap/global-config.ts +29 -3
  61. package/bootstrap/package-smoke.ts +127 -0
  62. package/bootstrap/pins.ts +189 -0
  63. package/bootstrap/release-notes.ts +88 -0
  64. package/bootstrap/release-version.ts +41 -7
  65. package/bootstrap/worker-features.ts +114 -0
  66. package/bootstrap/worker-seed.yaml +65 -0
  67. package/bootstrap/worker-tools.ts +39 -9
  68. package/cliproxy/README.md +1 -1
  69. package/cliproxy/docs/health-workflow.example.yml +20 -143
  70. package/docs/authentication.md +21 -11
  71. package/docs/consumer-setup.md +74 -58
  72. package/docs/convergence.md +67 -0
  73. package/docs/declared.md +61 -34
  74. package/docs/machine-setup.md +73 -7
  75. package/docs/manager-setup.md +34 -0
  76. package/docs/operations.md +64 -9
  77. package/docs/releasing.md +49 -1
  78. package/docs/runbooks.md +112 -7
  79. package/docs/state.md +23 -11
  80. package/docs/support-matrix.md +40 -4
  81. package/docs/topology.md +82 -12
  82. package/docs/ubuntu-sync.md +98 -42
  83. package/docs/worker-contract.md +122 -19
  84. package/docs/worker-requirements.md +107 -0
  85. package/examples/fleet.json +1 -0
  86. package/examples/workers-from-infrastructure/main.tf +132 -0
  87. package/examples/workers-from-infrastructure/worker-records.json +14 -0
  88. package/llms.txt +3 -1
  89. package/mise.lock +29 -0
  90. package/mise.toml +5 -3
  91. package/package.json +2 -2
  92. package/schema/check-ids.json +1991 -162
  93. package/schema/check-ids.schema.json +34 -0
  94. package/schema/fleet.schema.json +349 -10
  95. package/schema/output/check.schema.json +1 -1
  96. package/schema/output/fleet-state-definition.schema.json +14 -0
  97. package/schema/output/fleet-sync-schedule.schema.json +26 -0
  98. package/schema/output/fleet-sync.schema.json +18 -0
  99. package/schema/output/report.schema.json +3 -2
  100. package/schema/worker-record.schema.json +76 -0
  101. package/src/agents/converge.ts +140 -24
  102. package/src/agents/files.ts +2 -32
  103. package/src/agents/native-verify.ts +34 -3
  104. package/src/agents/secrets.ts +39 -4
  105. package/src/check-owners.ts +62 -2
  106. package/src/check-registry.ts +630 -0
  107. package/src/cli/agent-guide.ts +3 -3
  108. package/src/cli/cli.ts +4 -0
  109. package/src/cli/fleet-manifest.ts +108 -0
  110. package/src/cli/fleet-node-lifecycle.ts +704 -0
  111. package/src/cli/fleet-node-park.ts +238 -0
  112. package/src/cli/fleet-node-seed.ts +167 -0
  113. package/src/cli/fleet-state.ts +2 -1
  114. package/src/cli/fleet-sync-checkout.ts +7 -0
  115. package/src/cli/fleet-sync-enrollment.ts +193 -32
  116. package/src/cli/fleet-sync-forward.ts +6 -0
  117. package/src/cli/fleet-sync-manager.ts +123 -8
  118. package/src/cli/fleet-sync-output.ts +31 -11
  119. package/src/cli/fleet-sync-plan.ts +65 -9
  120. package/src/cli/fleet-sync-records.ts +113 -0
  121. package/src/cli/fleet-sync-remote.ts +172 -15
  122. package/src/cli/fleet-sync-schedule.ts +14 -4
  123. package/src/cli/fleet-sync-state.ts +31 -10
  124. package/src/cli/fleet-sync-vault.ts +152 -26
  125. package/src/cli/fleet-sync-worker.ts +214 -0
  126. package/src/cli/fleet-sync.ts +172 -21
  127. package/src/cli/fleet-validate.ts +277 -0
  128. package/src/cli/fleet-watch.ts +7 -1
  129. package/src/cli/fleet.ts +87 -5
  130. package/src/cli/managed-health.ts +25 -1
  131. package/src/cli/register-fleet.ts +251 -4
  132. package/src/cli/registry.ts +47 -0
  133. package/src/cli/tailnet.ts +43 -10
  134. package/src/cli/ui-data.ts +3 -1
  135. package/src/cli/worker-usage-report.ts +63 -0
  136. package/src/clock.ts +42 -0
  137. package/src/definition.ts +762 -232
  138. package/src/diagnostics.ts +5 -2
  139. package/src/exposure.ts +102 -0
  140. package/src/fleet-config.ts +3 -1
  141. package/src/git-identity.ts +76 -3
  142. package/src/ide/ide.ts +5 -0
  143. package/src/ide/t3.ts +12 -2
  144. package/src/input-hashes.ts +6 -0
  145. package/src/machine/machine-class.ts +59 -0
  146. package/src/machine/platform.ts +90 -11
  147. package/src/machine/state.ts +70 -16
  148. package/src/machine/support-matrix.ts +11 -9
  149. package/src/machine-health.ts +28 -5
  150. package/src/manager-health.ts +148 -2
  151. package/src/manager-sync.ts +89 -8
  152. package/src/monitoring/beszel.ts +8 -8
  153. package/src/monitoring/enroll.ts +8 -1
  154. package/src/monitoring/hub-access.ts +10 -4
  155. package/src/monitoring/systems.ts +138 -0
  156. package/src/network-definition.ts +13 -4
  157. package/src/node-converge-linux.ts +136 -27
  158. package/src/node-converge.ts +13 -3
  159. package/src/platform/ansible-receipt.ts +17 -2
  160. package/src/platform/ansible.ts +40 -4
  161. package/src/platform/manager-host.ts +25 -8
  162. package/src/platform/resource-verify.ts +18 -7
  163. package/src/platform/t3-policy.ts +24 -10
  164. package/src/platform/t3-service.ts +109 -34
  165. package/src/platform/ubuntu-release.ts +24 -0
  166. package/src/platform/ubuntu-verify.ts +31 -3
  167. package/src/platform/ubuntu-worker.ts +58 -0
  168. package/src/platform/worker-platform.ts +72 -0
  169. package/src/protocol.ts +1 -1
  170. package/src/public-reachability.ts +186 -0
  171. package/src/remote-bootstrap.ts +50 -12
  172. package/src/remote-operation.ts +18 -4
  173. package/src/repair-plan.ts +23 -12
  174. package/src/report.ts +2 -1
  175. package/src/schedule-check.ts +21 -2
  176. package/src/secrets.ts +7 -0
  177. package/src/service-auth.ts +55 -48
  178. package/src/ssh-connection.ts +141 -0
  179. package/src/ssh.ts +63 -24
  180. package/src/state/store.ts +4 -2
  181. package/src/state/sync.ts +35 -6
  182. package/src/state/workers.ts +140 -0
  183. package/src/t3/pairing.ts +59 -0
  184. package/src/t3/peer-link.ts +333 -0
  185. package/src/t3/version.ts +1 -1
  186. package/src/tailnet-cleanup.ts +185 -0
  187. package/src/tailnet-control.ts +346 -0
  188. package/src/tailscale.ts +1 -1
  189. package/src/toolchain-checks.ts +11 -1
  190. package/src/types.ts +4 -2
  191. package/src/worker-account.ts +63 -0
  192. package/src/worker-activity.ts +303 -0
  193. package/src/worker-capacity.ts +31 -0
  194. package/src/worker-credentials.ts +201 -0
  195. package/src/worker-exposure.ts +388 -0
  196. package/src/worker-features.ts +201 -0
  197. package/src/worker-host-checks.ts +71 -20
  198. package/src/worker-manifest.ts +224 -0
  199. package/src/worker-parking.ts +70 -0
  200. package/src/worker-policy.ts +38 -36
  201. package/src/worker-readiness.ts +15 -3
  202. package/src/worker-reboot.ts +129 -0
  203. package/src/worker-records.ts +305 -0
  204. package/src/worker-seed.ts +143 -0
  205. package/src/worker-sizing.ts +76 -0
  206. package/src/cli/schedule-logs.ts +0 -36
@@ -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
 
@@ -35,9 +36,12 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
35
36
  A new singleton service is declared under `services` and placed on the
36
37
  manager; it never names a host.
37
38
  - Workers depend on it, so it never sleeps on AC power
38
- (`pmset` in `ansible/macos/policy.json`, checked as `manager:power`, applied
39
- once by an administrator with `ansible/macos/manager.yml`; the failed check
40
- prints the command). While the
39
+ (`pmset` in `ansible/macos/policy.json`, checked as `manager:power` and
40
+ applied by `ansible/macos/manager.yml` on every `fleet-sync` once an
41
+ administrator has given the one grant `ansible/macos/manager-privilege.yml`;
42
+ the failed check prints the command, and
43
+ [docs/manager-setup.md](manager-setup.md) lists every step only a person
44
+ takes). While the
41
45
  manager is asleep, closed or offline, workers have no CLI proxy and no
42
46
  dashboard; nothing fails over, because a singleton never runs twice.
43
47
  - Its state (proxy OAuth/runtime state, the hub database) is backed up off the
@@ -48,7 +52,9 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
48
52
 
49
53
  - Stateless cattle: turn one off, re-image it or add another at will. A worker
50
54
  is declared in `workers`, enrolled once (SSH trust, sudo, its own Vault
51
- 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).
52
58
  - Workers never host a singleton service. The proxy reports any local install
53
59
  on a worker as a duplicate owner, and the hub refuses to install anywhere but
54
60
  the manager.
@@ -56,13 +62,77 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
56
62
  checkouts. Durable work lives in Git remotes; credentials come from Vault.
57
63
  Work in progress in agent worktrees is pushed to a recovery ref every 15
58
64
  minutes; [durability](durability.md) says what a lost machine takes with it.
59
- - The first declared worker is the rollout canary for a revision new to the
60
- fleet: a failed canary stops promotion to the other workers. Repeating a
61
- revision that already converged everywhere runs all workers at once.
65
+ - One worker is the rollout canary for a revision new to the fleet: the one
66
+ `fleet-sync --canary` or `canary` in `fleet.json` names, else the first
67
+ declared worker. When it cannot be reached (powered off, being rebuilt),
68
+ the next reachable worker in declared order proves the revision instead and
69
+ the unreachable one is reported as offline, never as a blocked rollout. A
70
+ canary that was deployed and failed stops promotion to the other workers.
71
+ Each worker's applied revision is kept in the fleet state, so one that
72
+ converged keeps its progress when another did not. Repeating a revision
73
+ that already converged everywhere runs all workers at once.
62
74
 
63
75
  ## Changing the topology
64
76
 
65
77
  - **Add or replace a worker:** add it to `workers`, enroll it with
66
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
+
67
137
  - **Replace the manager:** follow
68
138
  [replacing the manager](manager-recovery.md).
@@ -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,37 +180,43 @@ 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` | never; the human runs `manager.yml` once with `-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
- OpenTofu issues each enrolled worker its own renewable periodic token
198
- (`vault.tokens` entries `fleet-worker-<node>`, policy `personal-read`, sunk into
199
- `secret/personal/prd` as `FLEET_WORKER_TOKEN_<NODE>`). `devfleet fleet-sync`
200
- delivers each node's token automatically over SSH stdin (never as an argument)
201
- whenever that node has no working Vault session. Interactive runs first log the
202
- manager in (`vault login -method=userpass`, password typed on your terminal)
203
- and fall back to the same password login on a node whose token is not issued.
204
- Without either, the node reports `VAULT_LOGIN_REQUIRED` and its T3 proxy
205
- providers stay unavailable. The manual equivalent of the automatic delivery:
206
-
207
- ```sh
208
- vault kv get -field=FLEET_WORKER_TOKEN_WORKER_ONE secret/personal/prd \
209
- | ssh worker-one 'vault login -address=https://vault.example.com -no-print -'
210
- ```
200
+ Each worker has a Vault token of its own, kept in a document of its own,
201
+ `<mount>/<project>/workers/<id>` (fields `token` and `accessor`; `<project>`
202
+ is the first segment of the `proxy-client-key` path). The manager issues it
203
+ with `devfleet fleet-node-replace --id ID --apply --yes`: a periodic orphan
204
+ token under the policy `fleet-worker-<id>`, which reads the fleet's shared
205
+ document and the worker's own document and nothing else, so one worker's
206
+ token cannot read another's (`features/fleet-sync/worker-credentials.feature`
207
+ proves it against a Vault development server). The manager's own login needs
208
+ to write policies, create tokens and write the mount.
209
+
210
+ `devfleet fleet-sync` delivers each node's token over SSH stdin (never as an
211
+ argument) whenever that node has no working Vault session. A token still kept
212
+ in the shared document as `FLEET_WORKER_TOKEN_<NODE>` is delivered too, with
213
+ the warning `VAULT_WORKER_TOKEN_SHARED`: every worker can read that document,
214
+ so issue the worker its own token and remove the field. A password login with
215
+ the fleet's shared Vault user on the worker's own terminal happens only with
216
+ `fleet-sync --vault-password-login`. Without a token the node reports
217
+ `VAULT_LOGIN_REQUIRED` and its T3 proxy providers stay unavailable.
218
+ `devfleet fleet-node-retire --id ID --apply --yes` revokes a worker's token by
219
+ its accessor and removes its document and policy.
211
220
 
212
221
  From then on the node's `personal-worker-auth` timer renews that token and its
213
222
  `personal-worker-github` timer keeps its GitHub sign-in current
@@ -230,14 +239,58 @@ GNOME lock/idle changes apply to the designated user only. Effective running
230
239
  session/logind behavior still needs a maintenance/reboot acceptance check; writing
231
240
  a file does not prove logout/reboot survival. The sudoers fragment is validated
232
241
  with visudo before installation; this user and its agents can administer the
233
- 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
+ ```
234
272
 
235
- SSH forwarding changes validate the complete configuration before reload and
236
- restore the previous fragment on validation failure. Existing listeners,
237
- authentication and firewall rules are preserved. Existing socket activation is preserved for forwarding-only changes; listener
238
- migrations and unmanaged fragments require review. The manager makes a **new**
239
- strict SSH connection after apply. Private-listener/firewall acceptance and
240
- 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.
241
294
 
242
295
  The manager is the only configuration coordinator. No per-node pull timer is
243
296
  installed. Manager schedule controls use the existing platform adapter:
@@ -321,7 +374,10 @@ A worker whose T3 has not loaded the staged provider policy, or runs an older
321
374
  release than the one installed, is restarted only while it runs no agent turn;
322
375
  otherwise sync reports the pending restart and the recovery timer applies it
323
376
  at the first idle check, failing the check after
324
- `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
325
381
  update is installed even when it recreates the gateway. The manager finishes before the workers
326
382
  start, so they are verified against the updated proxy. `--dry-run` previews.
327
383