@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
@@ -8,14 +8,14 @@ rewrites them).
8
8
 
9
9
  ## Where credentials live
10
10
 
11
- | Credential | Lives in | Read by |
12
- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
13
- | The fleet's secrets (table below) | The provider `secrets` names in `fleet.json` | The node that needs it, through `src/secrets.ts` |
14
- | The bucket's two keys | The environment of the `devfleet` command; `~/.config/dev-fleet/bucket.json` (0600) on the manager after `fleet-state --init` | The manager only |
15
- | A node's Vault token | That node's own Vault session (`~/.vault-token`) | That node only; never copied |
16
- | A node's GitHub login | `gh`'s own store on that node | `gh`, and git through its credential helper |
17
- | Provider accounts (Claude, Codex …) | The CLI proxy's store on the manager | The proxy; nodes hold only the proxy client key |
18
- | Tailscale and T3 pairing | Each machine, signed in by a person | That machine |
11
+ | Credential | Lives in | Read by |
12
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
13
+ | The fleet's secrets (table below) | The provider `secrets` names in `fleet.json` | The node that needs it, through `src/secrets.ts` |
14
+ | The bucket's two keys | The environment of the `devfleet` command; `~/.config/dev-fleet/bucket.json` (0600) on the manager after `fleet-state --init` | The manager only |
15
+ | A node's Vault token | That node's own Vault session (`~/.vault-token`) | That node only; never copied |
16
+ | A node's GitHub login | `gh`'s own store on that node | `gh`, and git through its credential helper |
17
+ | Provider accounts (Claude, Codex …) | The CLI proxy's store on the manager | The proxy; nodes hold only the proxy client key |
18
+ | Tailscale and T3 pairing | Each machine, signed in by a person; a replaced worker's T3 is paired by the manager with a one-time code it never writes down | That machine |
19
19
 
20
20
  ## How they reach each node
21
21
 
@@ -30,14 +30,19 @@ rewrites them).
30
30
  makes `gh` git's credential helper and writes the declared identity
31
31
  (`git` in `fleet.json`, `ansible/github/git.yml`) on every node.
32
32
  - **The proxy client key** is read by each node from its own secrets provider
33
- whenever a launcher starts an agent.
33
+ whenever a launcher starts an agent. The node keeps the last key it read in
34
+ `~/.cache/dev-fleet/proxy-client-key` (0600) and uses it only while the
35
+ provider cannot answer, so an unreachable Vault or an expired session leaves
36
+ that node's agents working until the Vault checks below are fixed.
34
37
  - **The monitoring hub's backup storage keys** are read on the manager by
35
38
  `fleet-sync` and handed to the hub's playbook in a private variables file.
36
39
 
37
40
  ## Rotation and failure
38
41
 
39
- Rotate a secret where the provider keeps it; the next read uses it, nothing
40
- caches it. A Vault token that is not renewed expires: `worker:vault-renewal`
42
+ Rotate a secret where the provider keeps it; the next read uses it. Only the
43
+ proxy client key is remembered, and only as a fallback; a rotated key reaches
44
+ every node at its next launch, and `retire-key` should wait until every node
45
+ has launched an agent since the rotation. A Vault token that is not renewed expires: `worker:vault-renewal`
41
46
  fails and the fix is one login of that machine. A GitHub token that stops
42
47
  working fails `auth:github` and `worker:git-push-auth`; store the new one in
43
48
  Vault and the worker restores itself within ten minutes. During a proxy
@@ -60,6 +65,7 @@ Every failure code is in [the runbooks](runbooks.md).
60
65
  | `notify-webhook-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_WEBHOOK_URL` | `~/.config/dev-fleet/secrets/notify-webhook-url` |
61
66
  | `notify-ntfy-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_NTFY_URL` | `~/.config/dev-fleet/secrets/notify-ntfy-url` |
62
67
  | `tailnet-auth-key` | when its feature is declared | `DEVFLEET_SECRET_TAILNET_AUTH_KEY` | `~/.config/dev-fleet/secrets/tailnet-auth-key` |
68
+ | `tailnet-admin` | when its feature is declared | `DEVFLEET_SECRET_TAILNET_ADMIN` | `~/.config/dev-fleet/secrets/tailnet-admin` |
63
69
 
64
70
  <!-- end: secrets -->
65
71
 
@@ -72,8 +78,10 @@ Every failure code is in [the runbooks](runbooks.md).
72
78
  | `auth:github` | `src/github/checks.ts` |
73
79
  | `auth:vault` | `src/cli/fleet-sync-vault.ts` |
74
80
  | `auth:vault-isolation` | `src/cli/fleet-sync-vault.ts` |
81
+ | `worker:credential-renewal` | `src/worker-host-checks.ts` |
75
82
  | `worker:git-identity` | `src/git-identity.ts` |
76
83
  | `worker:git-push-auth` | `src/definition.ts`, `src/git-identity.ts` |
84
+ | `worker:git-signing` | `src/definition.ts`, `src/git-identity.ts` |
77
85
  | `worker:github-sync` | `src/worker-access-checks.ts` |
78
86
  | `worker:proxy-authentication` | `src/worker-access-checks.ts` |
79
87
  | `worker:vault-renewal` | `src/worker-access-checks.ts` |
@@ -24,13 +24,13 @@ Nothing else is needed. You never clone dev-fleet, copy its files or write a
24
24
  wrapper script around a checkout of it: the npm package is the tool, and the
25
25
  version in `package.json` is the commit every machine runs.
26
26
 
27
- | Concept | Meaning |
28
- | ----------- | ------------------------------------------------------------------------------------------------------ |
29
- | manager | The one macOS machine you run `devfleet fleet-sync` on. Hosts the CLI proxy and the monitoring hub. |
30
- | worker | An Ubuntu 26.04 machine the manager converges over SSH. Stateless: rebuilt from Git and your secrets. |
31
- | fleet state | One SQLite database (roster, approved revision, configuration last applied) replicated to your bucket. |
32
- | bucket | Any S3-compatible bucket you own, or a directory. The source of truth for the fleet state. |
33
- | revision | The dev-fleet commit the installed package was published from (`npx devfleet --version`). |
27
+ | Concept | Meaning |
28
+ | ----------- | -------------------------------------------------------------------------------------------------------------- |
29
+ | manager | The one macOS machine you run `devfleet fleet-sync` on. Hosts the CLI proxy and the monitoring hub. |
30
+ | worker | An Ubuntu 26.04 or 24.04 machine the manager converges over SSH. Stateless: rebuilt from Git and your secrets. |
31
+ | fleet state | One SQLite database (roster, approved revision, configuration last applied) replicated to your bucket. |
32
+ | bucket | Any S3-compatible bucket you own, or a directory. The source of truth for the fleet state. |
33
+ | revision | The dev-fleet commit the installed package was published from (`npx devfleet --version`). |
34
34
 
35
35
  ## Rules for agents
36
36
 
@@ -75,25 +75,30 @@ On the manager (the Mac this project is checked out on):
75
75
  | Tailscale signed in | `tailscale status --json` (look for `"BackendState":"Running"`) |
76
76
  | An SSH alias per worker that works without a prompt | `ssh -o BatchMode=yes WORKER_ALIAS true` for each worker |
77
77
 
78
- On every worker:
79
-
80
- | Requirement | Verify, from the manager |
81
- | ------------------------------------ | ------------------------------------------------------------------ |
82
- | Ubuntu 26.04, amd64 or arm64 | `ssh WORKER_ALIAS '. /etc/os-release; echo $VERSION_ID; uname -m'` |
83
- | A non-root account that can use sudo | `ssh WORKER_ALIAS 'id -u; sudo -n true && echo sudo-ok'` |
84
- | Joined to the same tailnet | the worker appears in `tailscale status` on the manager |
85
-
86
- A worker needs nothing else beforehand: no Git, curl, Python, Node, Bun or
87
- mise. `fleet-sync` installs them. If `sudo -n true` fails, that is fine for
88
- the first, interactive run, which asks for the password once and installs a
89
- sudo rule for the fleet; it is not fine for unattended runs.
90
-
91
- The `ssh` value of a worker in `fleet.json` is an alias from the manager's
92
- `~/.ssh/config`. dev-fleet always connects with `BatchMode=yes` and
93
- `StrictHostKeyChecking=yes`, so the host key must already be in
94
- `known_hosts`. **Ask** the human to verify each fingerprint out of band and
95
- connect once by hand; never add `StrictHostKeyChecking=no` or
96
- `accept-new`. A typical alias:
78
+ On every worker, check each row of
79
+ [what a machine must offer to be a worker](worker-requirements.md): the
80
+ image, architecture and size, the access, the first-boot data and the
81
+ network. Each row says how to verify it. If `sudo -n true` fails, that is fine
82
+ for the first, interactive run, which asks for the password once and installs
83
+ a sudo rule for the fleet; it is not fine for unattended runs.
84
+
85
+ A worker is reached in one of two ways, always with `BatchMode=yes` and
86
+ `StrictHostKeyChecking=yes`; never add `StrictHostKeyChecking=no` or
87
+ `accept-new`.
88
+
89
+ - **From declared data (no alias).** Leave `ssh` out: dev-fleet connects as
90
+ `network.user` on `network.sshPort` to `network.tailscaleName`, and checks
91
+ the host key against a known_hosts file of its own, rendered from the key
92
+ the fleet state pins. The human's `~/.ssh/config` and `~/.ssh/known_hosts`
93
+ are neither needed nor changed. The key is pinned without trusting a first
94
+ connection: `npx devfleet fleet-node-seed --id ID --output PATH --apply
95
+ --yes` generates it for a new machine, `npx devfleet fleet-node-replace
96
+ --id ID --host-key "TYPE BASE64" --apply --yes` pins one the human verified
97
+ out of band, and a worker record (below) may carry it as `hostKey`.
98
+ - **Through the human's own alias.** `ssh` names a Host alias in the
99
+ manager's `~/.ssh/config`, and its host key must already be in their
100
+ `known_hosts`. **Ask** the human to verify each fingerprint out of band and
101
+ connect once by hand. A typical alias:
97
102
 
98
103
  ```
99
104
  Host worker-one
@@ -302,6 +307,14 @@ An agent cannot answer those prompts, and `--json`, `--non-interactive` or a
302
307
  npx devfleet fleet-sync
303
308
  ```
304
309
 
310
+ A worker that needs none of those prompts is enrolled without the human: a
311
+ run with `--json` or `--non-interactive` enrolls a declared worker when its
312
+ pinned host key matched, `sudo -n` works on it, and exactly one tailnet device
313
+ matches it (or none, when `fleet.json` declares `tailnet` and its secret
314
+ `tailnet-admin`, so the run joins it with a single-use key). The report lists
315
+ what it enrolled (`enrolled` in the JSON). A machine made with
316
+ `devfleet fleet-node-seed` meets all of these.
317
+
305
318
  It converges the manager first, then the canary worker, then the rest. It
306
319
  prints one table and the path of the full JSON report
307
320
  (`~/.local/share/personal/fleet/manager/last-sync.json`, readable by the human
@@ -382,7 +395,9 @@ and codes; leave out everything private.
382
395
  - **Remove a worker.** Delete its entry, commit, run `npx devfleet fleet-sync`.
383
396
  - **Upgrade the fleet.** `npm install --save-dev --save-exact @crvouga/dev-fleet@latest`,
384
397
  commit, `npx devfleet fleet-sync`. The canary proves the new revision first.
385
- Install an older version and sync to roll back.
398
+ `npx devfleet fleet-sync --rollback` goes back to the revision applied
399
+ before (the revision, tools and T3; system packages roll forward only, as
400
+ the [README](../README.md#the-version) says).
386
401
  - **Replace the manager.** Follow
387
402
  [replacing the manager](manager-recovery.md).
388
403
 
@@ -0,0 +1,67 @@
1
+ # What converges a machine, and what is not Ansible yet
2
+
3
+ The goal is one convergence engine: Ansible, on every platform, with the CLI
4
+ as an orchestrator that validates the fleet configuration, runs playbooks,
5
+ reads their results and reports checks. Today three mechanisms change a
6
+ machine: the playbooks under `ansible/`, the bootstrap scripts, and some
7
+ TypeScript. This page lists every step that is not an Ansible task yet, each
8
+ with the reason it is not, so the list can only shrink.
9
+ `features/rules/convergence.feature` fails when a file that changes a machine
10
+ is missing from it.
11
+
12
+ ## Ansible
13
+
14
+ | Playbook | Converges |
15
+ | ----------------------------------------- | ----------------------------------------------------------------------------- |
16
+ | `ansible/ubuntu/baseline.yml` | A worker's packages, Docker, services, power, user, SSH, disk |
17
+ | `ansible/ubuntu/tailscale.yml` | Tailscale on a worker |
18
+ | `ansible/ubuntu/t3-desktop.yml` | The T3 desktop app on a desktop worker |
19
+ | `ansible/ubuntu/t3-headless.yml` | The T3 server as a lingering user service on a headless worker |
20
+ | `ansible/ubuntu/t3-policy.yml` | T3's administrator policy |
21
+ | `ansible/ubuntu/t3-route.yml` | T3's private tailnet route |
22
+ | `ansible/ubuntu/recovery.yml` | Recovery of a stopped T3 desktop app or server |
23
+ | `ansible/ubuntu/worker-account.yml` | The declared account on an image that lets only root in, from the manager |
24
+ | `ansible/ubuntu/reboot.yml` | One worker's first-converge or maintenance reboot, from the manager |
25
+ | `ansible/monitoring/agent.yml` | The Beszel agent on a worker |
26
+ | `ansible/monitoring/agent-darwin.yml` | The Beszel agent on the manager |
27
+ | `ansible/monitoring/hub-darwin.yml` | The Beszel hub and its tunnel on the manager |
28
+ | `ansible/monitoring/hub-route-darwin.yml` | The hub's private tailnet route |
29
+ | `ansible/macos/manager.yml` | The manager's power settings, network time and fleet-watch job, unattended |
30
+ | `ansible/macos/t3-policy.yml` | T3's administrator policy on the manager |
31
+ | `ansible/macos/manager-privilege.yml` | The one administrator grant: sudoers, job log rotation and the T3 policy |
32
+ | `ansible/macos/worker-host-key.yml` | A worker's pinned SSH host key in the manager's known_hosts |
33
+ | `ansible/macos/vault-renew.yml` | The launch agent that renews the manager's own Vault session |
34
+ | `ansible/github/git.yml` | The Git identity and GitHub credentials of every node |
35
+ | `ansible/agents/agents.yml` | Each agent CLI's dedicated profile, rendered from `ansible/agents/templates/` |
36
+
37
+ Every playbook passes `ansible-lint` in the production profile, every command
38
+ in it says when it changes something and runs, or is skipped, in a dry run
39
+ (`features/rules/playbooks.feature`). The Beszel agent playbook is proven on a
40
+ systemd machine to report no change on a second run and in `--check`, and to
41
+ report drift, and only that drift (`features/monitoring/agent-lifecycle.feature`).
42
+
43
+ ## Not Ansible yet
44
+
45
+ | Step | Where | Why it is not a task yet |
46
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
47
+ | Install Git, curl, CA certificates, Python and Ansible | `bootstrap/fleet-remote-bootstrap.sh` | It must run before Ansible exists on the machine; it is the minimal bootstrap |
48
+ | Create the worker account, its host key, the privilege rule and Tailscale on first boot | `bootstrap/worker-seed.yaml` (written by `fleet-node-seed`) | cloud-init runs it before the manager can reach the machine; fleet-sync converges everything else with Ansible afterwards |
49
+ | Install mise and the pinned toolchain | `bootstrap/bootstrap.sh`, `bootstrap/setup.ts` | mise must exist before any tool does, and `mise install --locked` already converges from a lock |
50
+ | Install a worker's subset of tools | `bootstrap/worker-tools.ts`, `bootstrap/worker-features.ts` | It chooses which locked tools `mise install` gets; moving it means a toolchain role |
51
+ | Publish the global mise configuration | `bootstrap/global-config.ts` | Derived from `mise.toml` and the agent worktree roots on every run; a `toolchain` role with a template would replace it |
52
+ | `PATH` lines in shell profiles | `bootstrap/shell-path.ts` | Edits files a person owns, guarded by markers; a `blockinfile` task would replace it |
53
+ | The `devfleet` launcher link | `bootstrap/install-launcher.sh` | Points at the fleet-owned checkout of the revision, known only during the run |
54
+ | The T3 server archive | `bootstrap/native-t3.ts` | Installed with the toolchain, checked against the digest in `mise.toml`; a `t3` role would replace it |
55
+ | The AWS CLI on Windows | `bootstrap/native-aws.ts` | Windows is not a fleet role |
56
+ | Docker Desktop on the manager | `bootstrap/docker-desktop.sh` | A person installs and approves it on macOS; dev-fleet only measures it |
57
+ | A worker's service credentials from Vault | `bootstrap/worker-github.ts`, `bootstrap/worker-auth.ts` | A login belongs to a machine and a person; it is read from a session, never declared |
58
+ | The manager's own convergence: proxy, profiles, hub, agent | `src/cli/fleet-sync-manager.ts` | Orchestrates the manager playbooks and the proxy's own installer; the proxy is not a playbook yet (`docs/declared.md`) |
59
+ | Scheduled runs | `src/cli/fleet-sync-schedule.ts`, `src/platform/launchd.ts`, `src/platform/systemd-user.ts` | Writes a launchd job or systemd timer from TypeScript; listed in `docs/declared.md` as a schedule that should be a playbook |
60
+
61
+ ## Still to do
62
+
63
+ Roles with a common interface (`baseline`, `toolchain`, `agents`, `t3`,
64
+ `network`, `monitoring`, `recovery`, `manager`), an inventory generated from
65
+ the fleet configuration, and `site.yml`, `manager.yml` and `workers.yml` that
66
+ bring a fresh manager and worker to ready with no other mutating step. Each
67
+ row of the table above moves into one of those roles.
package/docs/declared.md CHANGED
@@ -12,29 +12,33 @@ may only shrink.
12
12
 
13
13
  ## Declared
14
14
 
15
- | What | Declared in | Pinned by | Applied by | Measured by |
16
- | --------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------- |
17
- | The fleet: machines, services, accounts | Your `fleet.json` | The dev-fleet version in your `package.json`, which names one commit | `fleet-sync` | `fleet-status` |
18
- | Tools: languages, CLIs, coding agents | `mise.toml` | `mise.lock`: version and checksum for each platform | `mise install --locked` | Tool and `agent:verification` checks |
19
- | mise itself | `min_version` and `mise_*_sha256` in `mise.toml` | The digest of its release archive, held here | `bootstrap/bootstrap.sh` | Its version, at every toolchain setup |
20
- | JavaScript dependencies | `package.json` | `bun.lock` | `bun install --frozen-lockfile` | The install fails on a difference |
21
- | T3 Code, server and desktop app | `t3_*` under `[vars]` in `mise.toml` | SHA-256 of each artifact | `bootstrap/native-t3.ts`; `ansible/ubuntu/t3-desktop.yml` | T3 checks; `t3:version` on every machine |
22
- | T3 policy on the manager (updates off) | `ansible/ubuntu/t3/policy.json`, shared with the workers | The commit | `ansible/macos/t3-policy.yml`, once, as an administrator | `t3:updates` |
23
- | Worker work-in-progress guard | `ansible/ubuntu/wip-guard.py`, `ansible/ubuntu/tasks/recovery.yml` | The commit | `fleet-sync` | `features/ide/work-in-progress.feature` |
24
- | Ubuntu packages | `packages` and `snapshot` in `ansible/ubuntu/policy.json` | Exact versions from a dated archive snapshot | `ansible/ubuntu/tasks/packages.yml` | `package:*` |
25
- | Tailscale on workers | `tailscale` in `ansible/ubuntu/policy.json` | Exact version; digest of the signing key | `ansible/ubuntu/tailscale.yml` | `package:tailscale` |
26
- | Worker policy: power, SSH, sudo, memory, disk | `ansible/ubuntu/*.yml`, values shared with checks in `policy.json` | The commit | `fleet-sync` | Host and resource checks |
27
- | Monitoring: hub, tunnel, agents | `ansible/monitoring/*.yml`, `compose.yml`, `lock.json` | Image digests; SHA-256 of each agent archive | `fleet-sync` | `beszel:*` |
28
- | The manager's private route to the hub | `ansible/monitoring/hub-route-darwin.yml` | The commit | `fleet-sync` | The route, read back |
29
- | The manager's power settings | `pmset` in `ansible/macos/policy.json` | The commit | `ansible/macos/manager.yml`, on every `fleet-sync`, after one grant (`manager-privilege.yml`) | `manager:power` |
30
- | Ansible on the manager | `ansible/pyproject.toml` | `ansible/uv.lock`: one release, one Python, every dependency hashed | `uv run --locked --isolated` | A hash that differs stops the run |
31
- | Ansible on a worker | `ansible-core` in `ansible/ubuntu/policy.json` | Exact version | `ansible/ubuntu/tasks/packages.yml` | `package:ansible-core` |
32
- | The CLI proxy | `cliproxy/compose.yaml`, `image.lock.json`, `requirements.lock`, `launchd/`, `config.template.yaml` | Image digests; hashed Python dependencies | `devfleet cliproxy install`, and `fleet-sync` after it | The proxy's audit |
33
- | Agent profiles and T3 providers | `agents/registry.json`, `agents/t3.json` | The commit | `fleet-sync` | Profile receipts; the plan after every apply |
34
- | Worker time service | `ansible/ubuntu/tasks/services.yml`, offsets in `clock` in `ansible/ubuntu/policy.json` | The commit | `fleet-sync` | `worker:clock` |
35
- | The manager's container watchdog | `ansible/monitoring/container-watchdog.py`, `containerWatchdog` in `ansible/macos/policy.json` | The commit | `fleet-sync` (`hub-darwin.yml`) | `manager:container-watchdog` |
36
- | Docker Desktop on the manager | `docker_desktop_*` under `[vars]` in `mise.toml` | Version and build | You | `manager:docker` (warns) |
37
- | CI | `.github/workflows/` | Each action at a commit; tools from `mise.lock` | GitHub | `features/rules/` |
15
+ | What | Declared in | Pinned by | Applied by | Measured by |
16
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
17
+ | The fleet: machines, services, accounts | Your `fleet.json` | The dev-fleet version in your `package.json`, which names one commit | `fleet-sync` | `fleet-status` |
18
+ | Tools: languages, CLIs, coding agents | `mise.toml` | `mise.lock`: version and checksum for each platform | `mise install --locked` | Tool and `agent:verification` checks |
19
+ | mise itself | `min_version` and `mise_*_sha256` in `mise.toml` | The digest of its release archive, held here | `bootstrap/bootstrap.sh` | Its version, at every toolchain setup |
20
+ | JavaScript dependencies | `package.json` | `bun.lock` | `bun install --frozen-lockfile` | The install fails on a difference |
21
+ | T3 Code, server and desktop app | `t3_*` under `[vars]` in `mise.toml` | SHA-256 of each artifact | `bootstrap/native-t3.ts`; `ansible/ubuntu/t3-desktop.yml` | T3 checks; `t3:version` on every machine |
22
+ | T3 policy on the manager (updates off, linked environments on) | `ansible/ubuntu/t3/policy.json`, shared with the workers | The commit | `ansible/macos/t3-policy.yml`, once, as an administrator (`manager-privilege.yml`) | `t3:updates` |
23
+ | Worker work-in-progress guard | `ansible/ubuntu/wip-guard.py`, `ansible/ubuntu/tasks/recovery.yml` | The commit | `fleet-sync` | `features/ide/work-in-progress.feature` |
24
+ | Ubuntu packages | `packages` and `snapshot` in `ansible/ubuntu/policy.json` | Exact versions from a dated archive snapshot | `ansible/ubuntu/tasks/packages.yml` | `package:*` |
25
+ | Tailscale on workers | `tailscale` in `ansible/ubuntu/policy.json` | Exact version; digest of the signing key | `ansible/ubuntu/tailscale.yml` | `package:tailscale` |
26
+ | Worker policy: power, SSH, sudo, memory, disk | `ansible/ubuntu/*.yml`, values shared with checks in `policy.json` | The commit | `fleet-sync` | Host and resource checks |
27
+ | Monitoring: hub, tunnel, agents | `ansible/monitoring/*.yml`, `compose.yml`, `lock.json` | Image digests; SHA-256 of each agent archive | `fleet-sync` | `beszel:*` |
28
+ | The manager's private route to the hub | `ansible/monitoring/hub-route-darwin.yml` | The commit | `fleet-sync` | The route, read back |
29
+ | The manager's power settings | `pmset` in `ansible/macos/policy.json` | The commit | `ansible/macos/manager.yml`, on every `fleet-sync`, after one grant (`manager-privilege.yml`) | `manager:power` |
30
+ | Ansible on the manager | `ansible/pyproject.toml` | `ansible/uv.lock`: one release, one Python, every dependency hashed | `uv run --locked --isolated` | A hash that differs stops the run |
31
+ | Ansible on a worker | `ansible-core` in `ansible/ubuntu/policy.json` | Exact version | `ansible/ubuntu/tasks/packages.yml` | `package:ansible-core` |
32
+ | The CLI proxy | `cliproxy/compose.yaml`, `image.lock.json`, `requirements.lock`, `launchd/`, `config.template.yaml` | Image digests; hashed Python dependencies | `devfleet cliproxy install`, and `fleet-sync` after it | The proxy's audit |
33
+ | Agent profiles and T3 providers | `agents/registry.json`, `agents/t3.json`; each CLI's configuration from `ansible/agents/templates/` | The commit | `fleet-sync` (agent profiles through `ansible/agents/agents.yml`) | Profile receipts; the plan after every apply; `agent:*:session` |
34
+ | Worker time service | `ansible/ubuntu/tasks/services.yml`, offsets in `clock` in `ansible/ubuntu/policy.json` | The commit | `fleet-sync` | `worker:clock` |
35
+ | The manager's playbook and its receipt | `ansible/macos/manager.yml` | The commit | `fleet-sync`, check mode first, then applied; re-applied at least daily | `manager:playbook` |
36
+ | The manager's network time | `ansible/macos/manager.yml` (an assert) | The commit | macOS | `manager:clock`, `worker:clock-offset` |
37
+ | fleet-watch on a schedule | `fleetWatch` in `ansible/macos/policy.json`, `manager.schedule.watchMinutes` in `fleet.json` | The commit | `fleet-sync` (`manager.yml`) | `manager:watch` |
38
+ | Rotation of the scheduled jobs' logs | `jobLogs` in `ansible/macos/policy.json` | The commit | `ansible/macos/manager-privilege.yml`, once, as an administrator | `manager:job-logs` |
39
+ | The manager's container watchdog | `ansible/monitoring/container-watchdog.py`, `containerWatchdog` in `ansible/macos/policy.json` | The commit | `fleet-sync` (`hub-darwin.yml`) | `manager:container-watchdog` |
40
+ | Docker Desktop on the manager | `docker_desktop_*` under `[vars]` in `mise.toml` | Version and build | You | `manager:docker` (warns) |
41
+ | CI | `.github/workflows/` | Each action at a commit; tools from `mise.lock` | GitHub | `features/rules/` |
38
42
 
39
43
  A value lives in a policy file when something besides the playbook reads it,
40
44
  so the check and the playbook cannot disagree. A value only a playbook uses
@@ -62,11 +66,11 @@ stays in the playbook: the playbook is its declaration.
62
66
  scripts from the declarative files above, not by a playbook.
63
67
  - **Files TypeScript and shell still write.** The installed `fleet.json`, the
64
68
  `current` and `devfleet` links, the `PATH` lines in shell profiles, the
65
- global mise configuration, the T3 server archive, agent profiles and T3
66
- settings. Each is derived
69
+ global mise configuration, the T3 server archive and T3 settings. Each is derived
67
70
  from a tracked file and rewritten on every run, but by code.
68
71
  - **Logins.** Tailscale, Vault, GitHub, each provider account and T3 pairing
69
- belong to a machine and a person. They are checked, never copied.
72
+ (except a replaced worker's, which the manager pairs itself) belong to a
73
+ machine and a person. They are checked, never copied.
70
74
  - **Windows.** `bootstrap/bootstrap.ps1` exists; Windows is not a fleet role
71
75
  and nothing tests it.
72
76
 
@@ -80,13 +84,36 @@ moving to it entirely means NixOS workers and nix-darwin on the manager, which
80
84
  replaces every unit and path that exists today. The one thing above that Nix
81
85
  would settle and the current tools do not is the rest of a worker's packages.
82
86
 
87
+ ## Keeping pins current
88
+
89
+ A pin that is only moved by hand goes stale and is then moved in a large
90
+ batch, the riskiest way to change a fleet. Each pin is moved by a machine,
91
+ one pull request at a time, which CI, the fresh workers and the VM test then
92
+ judge like any change, unless it says below why a person moves it.
93
+
94
+ | Pin | Moved by |
95
+ | ---------------------------------------- | --------------------------------------------------------------------------------------------- |
96
+ | Tools in `mise.toml` and `mise.lock` | Renovate (`mise`), one pull request per tool |
97
+ | JavaScript and Python dependencies | Renovate, with lock file maintenance |
98
+ | Actions and CI service images | Renovate, each action at a commit |
99
+ | mise itself | The [Pins workflow](../.github/workflows/pins.yml): `bun bootstrap/pins.ts mise`, weekly |
100
+ | T3 Code (nightly) | The Pins workflow: `bun bootstrap/pins.ts t3`, weekly, every artifact hashed |
101
+ | The tunnel image of the monitoring hub | Renovate, by tag and digest (a custom manager in `renovate.json`) |
102
+ | Beszel (hub image and agents) | A person, because the agents' binary digests need the archives unpacked; `pins.ts` cannot yet |
103
+ | Docker Desktop on the manager | A person, because a person installs it and dev-fleet only measures the version |
104
+ | The Ubuntu snapshot and package versions | A person, because a new snapshot moves every package at once and each needs its exact version |
105
+
106
+ The workflows never name the mise release: they install it through
107
+ [`.github/actions/mise`](../.github/actions/mise/action.yml), which reads
108
+ `min_version` from `mise.toml`.
109
+
83
110
  ## Changing a pin
84
111
 
85
- | To change | Edit | Then |
86
- | ----------------------- | -------------------------------------------------------------------- | ------------------------------------------------ |
87
- | A tool | Its version in `mise.toml` | Update `mise.lock` with `mise lock` |
88
- | mise | `min_version` and the four `mise_*_sha256` in `mise.toml` | Copy the digests from the release's `SHASUMS256` |
89
- | An Ubuntu package | `packages` in `ansible/ubuntu/policy.json`, and `snapshot` if needed | The VM test in `ansible/ubuntu/tests` |
90
- | Ansible on the manager | `ansible/pyproject.toml` | `uv lock` in `ansible/` |
91
- | Beszel | `ansible/monitoring/lock.json`, `compose.yml`, the agent unit | `bun test features/rules` |
92
- | A manager power setting | `pmset` in `ansible/macos/policy.json` | The next `fleet-sync` applies it |
112
+ | To change | Edit | Then |
113
+ | ----------------------- | -------------------------------------------------------------------- | ------------------------------------- |
114
+ | A tool | Its version in `mise.toml` | Update `mise.lock` with `mise lock` |
115
+ | mise | `bun bootstrap/pins.ts mise` moves `min_version` and the digests | The fresh-box run |
116
+ | An Ubuntu package | `packages` in `ansible/ubuntu/policy.json`, and `snapshot` if needed | The VM test in `ansible/ubuntu/tests` |
117
+ | Ansible on the manager | `ansible/pyproject.toml` | `uv lock` in `ansible/` |
118
+ | Beszel | `ansible/monitoring/lock.json`, `compose.yml`, the agent unit | `bun test features/rules` |
119
+ | A manager power setting | `pmset` in `ansible/macos/policy.json` | The next `fleet-sync` applies it |
@@ -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
  ## Machine setup: macOS, Windows and Linux
6
7
 
@@ -184,7 +185,13 @@ railway login
184
185
  Only sign into providers the task actually needs. On fleet machines the Git
185
186
  identity and the GitHub credential helper come from the `git` block of
186
187
  `fleet.json`: `fleet-sync` writes them in `~/.config/git/config` and checks
187
- them as `worker:git-identity` and `worker:git-push-auth`. Elsewhere set `git
188
+ them as `worker:git-identity` and `worker:git-push-auth`. With
189
+ `"signing": "ssh"` in that block every node also makes an SSH key of its own
190
+ (`~/.ssh/dev-fleet-git-signing`, never copied off the node) and signs every
191
+ commit and tag with it, checked as `worker:git-signing`; GitHub shows those
192
+ commits as verified once you add each node's `.pub` to the account as a
193
+ signing key (`gh ssh-key add ~/.ssh/dev-fleet-git-signing.pub --type signing`
194
+ on the node). Elsewhere set `git
188
195
  config --global user.name` and `user.email` after confirming them; do not guess
189
196
  an email or overwrite an existing identity. Enable Git LFS hooks when needed with `git lfs install`.
190
197
 
@@ -357,9 +364,44 @@ headless service, so environment identity and pairing are kept. A conflicting
357
364
  launcher version requires reviewed maintenance; the recovery timer stops a
358
365
  still-running headless service. Routine fleet-sync never ends a running turn
359
366
  to load a new pin or policy: the app restarts into them at the first idle
360
- check (`t3:running-version`, `t3:policy-loaded`). The disposable Ubuntu VM test still exercises the former headless service
361
- and does not cover the desktop session. Private routing and paired execution
362
- require separate evidence.
367
+ check (`t3:running-version`, `t3:policy-loaded`). Private routing and paired
368
+ execution require separate evidence.
369
+
370
+ #### Headless workers
371
+
372
+ A worker whose session is `headless` (`features.session` in `fleet.json`, or
373
+ the `editing` preset) has no display manager, no graphical session and no
374
+ GPU. It runs the pinned T3 **server**, not the desktop app under a virtual
375
+ display: the pinned release ships the server as its own archive
376
+ (`t3-<version>-linux-<arch>.tar.gz`, the `t3_version` and
377
+ `t3_linux_*_sha256` pins, from the release's `SHA256SUMS`), which the
378
+ toolchain already installs on every worker for policy commands. Its
379
+ `t3 serve` runs the server the desktop app embeds, with no browser; the
380
+ subcommand and its flags were read from the pinned release itself
381
+ (`t3 serve --help` of `0.1.8-nightly.20261106.4262`, whose `--mode web`
382
+ server answered `/.well-known/t3/environment` with that `serverVersion`).
383
+ A virtual display would install and pin a display server and a desktop
384
+ toolkit for one process, which costs the memory a small worker needs.
385
+
386
+ `ansible/ubuntu/t3-headless.yml` declares `t3code-headless.service`, a user
387
+ unit started by `default.target`: the designated user lingers, so it starts at
388
+ boot with nobody logged in. It listens on `127.0.0.1:3773` like the desktop
389
+ app, with `T3CODE_HOME=%h/.t3`, so the private route, the policy, the provider
390
+ instances, `personal-worker-recovery` (which reads the release from the
391
+ unit's versioned server path) and the checks are the same for both session
392
+ types. Switching a worker between types stops and removes the other type's
393
+ unit and keeps `~/.t3`; GDM auto-login, if the machine had it, is left as it
394
+ was. A headless worker installs none of the desktop-only packages
395
+ (`features.desktop` in `ansible/ubuntu/policy.json`) and reports their checks
396
+ as not applicable. The server prints a one-time pairing token to its own
397
+ journal at start, as `t3 serve` does anywhere; dev-fleet never reads it.
398
+ Pairing is done from the manager: by a person the first time, and by
399
+ `fleet-node-replace` or the next `fleet-sync` for a machine that replaces a
400
+ worker.
401
+
402
+ The disposable Ubuntu VM test (`ansible/ubuntu/tests/vm.py`) converges each
403
+ session type, reboots with nobody logged in and kills the T3 process to
404
+ prove the recovery timer brings it back.
363
405
 
364
406
  Normal Ubuntu fleet setup is controller-led:
365
407
  `devfleet fleet-sync`. See the
@@ -396,7 +438,10 @@ Controller-driven remote bootstrap passes `--worker` to the shared setup owner.
396
438
  It installs the `vars.worker_tools` subset from `mise.toml`, with versions and
397
439
  artifacts taken from the same `mise.lock`; native T3 and Ubuntu Docker/service
398
440
  policy remain source-pinned. `bun bootstrap/doctor.ts --worker --docker --json`
399
- checks that profile. It does not require unrelated cloud CLIs on workers.
441
+ checks that profile. It does not require unrelated cloud CLIs on workers. A
442
+ worker whose `containers` feature is off (`features.containers: false`, or the
443
+ `editing` preset) leaves `docker-cli` out of its base tools, and its doctor
444
+ runs with `--no-containers` instead of `--docker`.
400
445
 
401
446
  Tool profiles add locked tool sets on top of it: `worker.toolProfiles` in
402
447
  `fleet.json` (fleet-wide) or `toolProfiles` on one worker selects `web` (pnpm,
@@ -404,7 +449,28 @@ Deno, Yarn and headless Chromium for browser tests), `go`, `rust` (with CMake
404
449
  and Ninja) or `jvm` (Temurin), each a `worker_profile_<name>` under `[vars]` in
405
450
  `mise.toml`. `worker.tools` adds single tools dev-fleet already pins. The setup
406
451
  installs them (`--node` reads the installed configuration) and the doctor
407
- checks each as `tool:<name>` (`--profiles`, `--tools`).
452
+ checks each as `tool:<name>` (`--profiles`, `--tools`). The base tools include
453
+ `tmux`, for sessions that outlive an SSH connection.
454
+
455
+ A tool dev-fleet does not ship belongs to the project that needs it: pin it in
456
+ the project's own `mise.toml` and lock it with `mise lock`. On every fleet
457
+ machine dev-fleet's global configuration sets `locked = true` for the
458
+ `project` scope, so mise installs a project's tools in a checkout only from
459
+ that project's `mise.lock` and refuses a tool the lock does not name;
460
+ `mise install --locked` in the checkout installs and verifies them.
461
+
462
+ There is no `mobile` profile. No mobile toolchain can be pinned with
463
+ checksums for both worker architectures through mise: Flutter publishes no
464
+ Linux arm64 release, the Android SDK's platforms and build tools are fetched
465
+ by `sdkmanager` at run time (outside any lock, behind a licence a person
466
+ accepts), and iOS builds need macOS and Xcode, which workers are not. A
467
+ project that builds for Android on x86_64 workers pins `flutter`, `java` and
468
+ `android-sdk` in its own `mise.toml` as above.
469
+
470
+ `worker.exposure` (fleet-wide) or `exposure` on one worker says what a worker
471
+ opens beyond loopback and the tailnet; without it, nothing. See
472
+ [Exposure](ubuntu-sync.md#exposure).
473
+
408
474
  The source-owned `worker-auth.ts` timer renews the worker's existing Vault token
409
475
  and can restore the declared GitHub account from its authorized Vault service
410
476
  credential, without copying another machine's login directory.
@@ -22,6 +22,13 @@ step added here must be added there too, with the check that verifies it.
22
22
 
23
23
  <!-- /human-steps -->
24
24
 
25
- The grant lets the manager's user run exactly `pmset -c` without a password,
26
- so later syncs never prompt. Run the same playbook with `-e grant=absent` to
27
- remove it.
25
+ The grant lets the manager's user run exactly `pmset -c` and
26
+ `systemsetup -getusingnetworktime` without a password, so later syncs never
27
+ prompt. The same playbook, run as an administrator, also installs what only
28
+ root may write: the newsyslog drop-in that keeps the logs of the scheduled
29
+ jobs under a fixed size (`manager:job-logs`) and T3's administrator policy
30
+ (`t3:updates`). Every `fleet-sync` then applies `ansible/macos/manager.yml`
31
+ unattended: in check mode first, then for real, with a receipt like the
32
+ workers' and a full re-apply at least once a day (`manager:playbook`). Run the
33
+ grant again when `manager:clock` or `manager:job-logs` asks for it after an
34
+ upgrade, or with `-e grant=absent` to remove it.