@crvouga/dev-fleet 0.1.0 → 0.3.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 (159) hide show
  1. package/README.md +265 -37
  2. package/agents/setup.md +42 -12
  3. package/ansible/macos/manager.yml +10 -7
  4. package/ansible/macos/policy.json +2 -10
  5. package/ansible/macos/t3-policy.yml +49 -0
  6. package/ansible/monitoring/README.md +86 -25
  7. package/ansible/monitoring/agent.yml +40 -2
  8. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  9. package/ansible/monitoring/lock.json +5 -0
  10. package/ansible/pyproject.toml +12 -0
  11. package/ansible/ubuntu/baseline.yml +1 -1
  12. package/ansible/ubuntu/disk-gc.py +336 -0
  13. package/ansible/ubuntu/policy.json +13 -3
  14. package/ansible/ubuntu/t3-desktop.yml +19 -0
  15. package/ansible/ubuntu/t3-health.sh +93 -22
  16. package/ansible/ubuntu/t3-idle.py +42 -0
  17. package/ansible/ubuntu/t3-policy.yml +4 -11
  18. package/ansible/ubuntu/tailscale.yml +8 -5
  19. package/ansible/ubuntu/tasks/recovery.yml +60 -1
  20. package/ansible/ubuntu/tasks/resources.yml +61 -1
  21. package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
  22. package/ansible/ubuntu/wip-guard.py +121 -0
  23. package/ansible/uv.lock +190 -0
  24. package/bootstrap/bootstrap.sh +25 -18
  25. package/bootstrap/doctor.ts +46 -1
  26. package/bootstrap/fleet-manager-run.sh +42 -15
  27. package/bootstrap/fleet-remote-bootstrap.sh +85 -12
  28. package/bootstrap/global-config.ts +64 -6
  29. package/bootstrap/install-launcher.sh +3 -0
  30. package/bootstrap/native-t3.ts +22 -11
  31. package/bootstrap/npm.ts +10 -2
  32. package/bootstrap/release-version.ts +157 -0
  33. package/cliproxy/README.md +3 -3
  34. package/cliproxy/scripts/accounts.py +216 -0
  35. package/cliproxy/scripts/checkpoint.py +29 -1
  36. package/cliproxy/scripts/cli.py +1 -1
  37. package/cliproxy/scripts/clientkeys.py +99 -0
  38. package/cliproxy/scripts/lease.py +80 -0
  39. package/cliproxy/scripts/ops +137 -25
  40. package/cliproxy/scripts/restore-verify +15 -0
  41. package/docs/accounts.md +154 -0
  42. package/docs/consumer-setup.md +408 -0
  43. package/docs/declared.md +91 -0
  44. package/docs/durability.md +26 -0
  45. package/docs/machine-setup.md +400 -0
  46. package/docs/operations.md +276 -0
  47. package/docs/releasing.md +103 -0
  48. package/docs/state.md +254 -0
  49. package/docs/topology.md +69 -0
  50. package/docs/ubuntu-sync.md +357 -0
  51. package/docs/worker-contract.md +67 -0
  52. package/examples/fleet.json +5 -0
  53. package/llms.txt +36 -0
  54. package/mise.lock +29 -0
  55. package/mise.toml +15 -2
  56. package/package.json +11 -2
  57. package/schema/fleet.schema.json +137 -15
  58. package/src/accounts/checks.ts +220 -0
  59. package/src/accounts/context.ts +76 -0
  60. package/src/accounts/propagate.ts +62 -0
  61. package/src/accounts/roster.ts +85 -0
  62. package/src/accounts/selection.ts +67 -0
  63. package/src/accounts/sessions.ts +120 -0
  64. package/src/agents/account-relay.ts +176 -26
  65. package/src/agents/fleet-verify.ts +9 -0
  66. package/src/agents/launch.ts +84 -5
  67. package/src/agents/native-verify.ts +293 -22
  68. package/src/agents/provider-errors.ts +9 -2
  69. package/src/agents/registry.ts +7 -1
  70. package/src/cli/agent-guide.ts +39 -5
  71. package/src/cli/app.ts +11 -4
  72. package/src/cli/cli.ts +7 -0
  73. package/src/cli/command-index.ts +7 -0
  74. package/src/cli/fleet-accounts.ts +253 -0
  75. package/src/cli/fleet-state.ts +499 -0
  76. package/src/cli/fleet-sync-accounts.ts +58 -0
  77. package/src/cli/fleet-sync-checkout.ts +56 -17
  78. package/src/cli/fleet-sync-enrollment.ts +19 -23
  79. package/src/cli/fleet-sync-forward.ts +1 -1
  80. package/src/cli/fleet-sync-manager.ts +84 -12
  81. package/src/cli/fleet-sync-monitoring.ts +5 -5
  82. package/src/cli/fleet-sync-output.ts +7 -3
  83. package/src/cli/fleet-sync-remote.ts +31 -18
  84. package/src/cli/fleet-sync-state.ts +86 -0
  85. package/src/cli/fleet-sync.ts +159 -57
  86. package/src/cli/fleet.ts +4 -2
  87. package/src/cli/managed-health.ts +4 -4
  88. package/src/cli/node-sync.ts +7 -0
  89. package/src/cli/program.ts +20 -4
  90. package/src/cli/project.ts +51 -0
  91. package/src/cli/register-fleet.ts +86 -1
  92. package/src/cli/registry.ts +41 -1
  93. package/src/cli/roster.ts +46 -30
  94. package/src/cli/settings.ts +15 -12
  95. package/src/cli/source-operations.ts +27 -3
  96. package/src/cli/tailnet.ts +2 -2
  97. package/src/definition.ts +116 -4
  98. package/src/diagnostics.ts +1 -1
  99. package/src/fleet-config.ts +105 -19
  100. package/src/ide/definition.ts +86 -0
  101. package/src/ide/ide.ts +43 -0
  102. package/src/ide/index.ts +5 -0
  103. package/src/ide/orchestrate.ts +50 -0
  104. package/src/ide/t3.ts +111 -0
  105. package/src/machine/source.ts +3 -3
  106. package/src/machine/state.ts +15 -27
  107. package/src/manager-sync.ts +9 -18
  108. package/src/monitoring/agent-evidence.ts +146 -0
  109. package/src/monitoring/apply.ts +15 -3
  110. package/src/monitoring/beszel.ts +11 -4
  111. package/src/monitoring/enroll.ts +2 -1
  112. package/src/monitoring/hub-access.ts +4 -3
  113. package/src/monitoring/hub-backup.ts +121 -0
  114. package/src/monitoring/hub-viewer.ts +8 -8
  115. package/src/monitoring/hub.ts +3 -3
  116. package/src/monitoring/manager-hub.ts +30 -14
  117. package/src/node-converge-linux.ts +30 -7
  118. package/src/package-info.ts +45 -0
  119. package/src/platform/ansible.ts +19 -15
  120. package/src/platform/darwin.ts +0 -13
  121. package/src/platform/fallback.ts +0 -8
  122. package/src/platform/linux.ts +0 -10
  123. package/src/platform/resource-verify.ts +8 -4
  124. package/src/platform/t3-policy.ts +33 -6
  125. package/src/platform/t3-service.ts +104 -0
  126. package/src/platform/types.ts +0 -4
  127. package/src/platform/ubuntu-audit.ts +5 -2
  128. package/src/platform/windows.ts +0 -14
  129. package/src/remote-bootstrap.ts +17 -19
  130. package/src/remote-entry.ts +15 -1
  131. package/src/repair-catalog.ts +1 -1
  132. package/src/runner.ts +1 -1
  133. package/src/service-auth.ts +15 -5
  134. package/src/state/bucket.ts +208 -0
  135. package/src/state/check.ts +74 -0
  136. package/src/state/connection.ts +92 -0
  137. package/src/state/enrollment.ts +45 -0
  138. package/src/state/legacy.ts +89 -0
  139. package/src/state/litestream.ts +143 -0
  140. package/src/state/operator.ts +20 -0
  141. package/src/state/paths.ts +40 -0
  142. package/src/state/roster.ts +62 -0
  143. package/src/state/store.ts +169 -0
  144. package/src/state/sync.ts +352 -0
  145. package/src/t3/accounts.ts +7 -1
  146. package/src/t3/models.ts +133 -0
  147. package/src/t3/pairing.ts +31 -0
  148. package/src/t3/profile.ts +203 -0
  149. package/src/t3/version.ts +119 -0
  150. package/src/t3-endpoint.ts +64 -8
  151. package/src/t3-linked-check.ts +24 -2
  152. package/src/toolchain-checks.ts +25 -0
  153. package/src/transport.ts +3 -2
  154. package/src/types.ts +4 -0
  155. package/src/worker-access-checks.ts +56 -3
  156. package/src/worker-host-checks.ts +41 -13
  157. package/src/workstation-checks.ts +1 -1
  158. package/src/index.ts +0 -6
  159. package/src/test-fixtures.ts +0 -36
package/README.md CHANGED
@@ -10,14 +10,40 @@ Status: extracted from a working three-machine fleet and in daily use there.
10
10
  It has not yet been set up from nothing by a second person; expect rough edges
11
11
  and read [what is assumed](#what-it-assumes) first.
12
12
 
13
+ ## How you use it
14
+
15
+ You do not clone this repository. dev-fleet is a command you install, and your
16
+ fleet is a small project of your own:
17
+
18
+ ```
19
+ my-fleet/ your repository
20
+ fleet.json your machines, your services, the bucket that holds the state
21
+ package.json pins the dev-fleet version every machine runs
22
+ ```
23
+
24
+ You run `devfleet` from inside that project and pass it the credentials of a
25
+ storage bucket you own. Nothing else has to be kept:
26
+
27
+ | What | Where it lives |
28
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
29
+ | The fleet configuration | `fleet.json`, in your repository |
30
+ | The version of dev-fleet | The `@crvouga/dev-fleet` dependency in your `package.json` |
31
+ | The fleet state: the enrolled workers, the approved revision, the configuration applied | Your bucket (any S3-compatible service) |
32
+ | Secrets | The provider `fleet.json` names (`vault`, `sops`, `file` or `env`) |
33
+ | The source, checkouts, tools and reports dev-fleet puts on a machine | A cache: delete it and `fleet-sync` rebuilds it |
34
+
35
+ The CLI proxy and the monitoring hub on the manager still keep data of their
36
+ own; [known limits](#known-limits) says what is lost with the manager.
37
+
13
38
  ## What it assumes
14
39
 
15
40
  dev-fleet is opinionated. These are part of it, not options:
16
41
 
17
- - The **manager** is a macOS machine. It runs `fleet-sync` and hosts the
18
- shared services (the CLI proxy and the monitoring hub) in Docker Desktop.
42
+ - The **manager** is a macOS machine. You run `fleet-sync` on it, and it hosts
43
+ the shared services (the CLI proxy and the monitoring hub) in Docker Desktop.
19
44
  - **Workers** are Ubuntu 26.04 machines reached over SSH. They are stateless:
20
45
  everything on them is rebuilt from Git and your secrets.
46
+ - The fleet state is kept in an **S3-compatible bucket** you own.
21
47
  - Machines reach each other over **Tailscale**.
22
48
  - Coding agents (Claude Code, Codex, Grok, OpenCode, Antigravity) run through
23
49
  **CLIProxyAPI** on the manager, and **T3 Code** lists one provider per
@@ -25,58 +51,157 @@ dev-fleet is opinionated. These are part of it, not options:
25
51
  - Machine policy is **Ansible** (`ansible/`), tool versions are **mise** pins
26
52
  (`mise.toml`, `mise.lock`), monitoring is **Beszel**.
27
53
 
54
+ What dev-fleet puts on a machine is written in tracked files and pinned to a
55
+ version or digest. [What is declared, and where](docs/declared.md) lists it,
56
+ and what is not pinned yet.
57
+
28
58
  What it does not assume is anything about you: no account, hostname, domain,
29
59
  tailnet or repository is written in this code. They live in your `fleet.json`.
30
60
 
31
61
  ## Quickstart
32
62
 
33
- On the manager (macOS, with Git, Docker Desktop and Tailscale signed in):
63
+ Setting it up with a coding agent? Point it at
64
+ [`llms.txt`](llms.txt) or straight at
65
+ [Set up a fleet from your own project](docs/consumer-setup.md): the whole
66
+ procedure in one pass, including what the agent must ask you for and the
67
+ first sync only you can run. Both ship in the package, under
68
+ `node_modules/@crvouga/dev-fleet/`.
69
+
70
+ On the manager you need macOS with Git, Node 22 or newer, Docker Desktop and
71
+ Tailscale signed in. You also need an empty private bucket on any
72
+ S3-compatible service (AWS S3, Cloudflare R2, Backblaze B2, MinIO) and an
73
+ access key that can read and write it.
34
74
 
35
75
  ```sh
36
- git clone https://github.com/crvouga/dev-fleet.git ~/.local/share/dev-fleet/source
37
- cd ~/.local/share/dev-fleet/source
38
- bash bootstrap/bootstrap.sh # installs the pinned toolchain with mise
76
+ mkdir my-fleet && cd my-fleet && git init
77
+ npm init -y
78
+ npm install --save-dev --save-exact @crvouga/dev-fleet
39
79
 
40
- mkdir -p ~/.config/dev-fleet
41
- cp examples/fleet.json ~/.config/dev-fleet/fleet.json
42
- $EDITOR ~/.config/dev-fleet/fleet.json # your machines, endpoints and secrets
80
+ npx devfleet fleet-state --example > fleet.json
81
+ $EDITOR fleet.json # your machines, and your bucket under "state"
43
82
 
44
- export DEVFLEET_REPO=$PWD
45
- sh bootstrap/fleet-manager-run.sh "$(git rev-parse HEAD)" --dry-run # preview
46
- sh bootstrap/fleet-manager-run.sh "$(git rev-parse HEAD)" # apply
83
+ export DEVFLEET_BUCKET_ACCESS_KEY_ID=…
84
+ export DEVFLEET_BUCKET_SECRET_ACCESS_KEY=…
85
+ npx devfleet fleet-sync --dry-run # preview
86
+ npx devfleet fleet-sync # apply
47
87
  ```
48
88
 
49
- `fleet-sync` converges the manager first, then every worker over SSH: it sends
50
- the pinned revision and your configuration, installs the toolchain, applies
51
- the Ansible policy and verifies the result. It prints one table and writes a
52
- full JSON report.
89
+ Commit `fleet.json`, `package.json` and the lockfile. Keep the two credentials
90
+ out of Git: pass them from wherever you keep secrets (a password manager's
91
+ `run` command, a git-ignored `.env`, your shell). They are read from the
92
+ environment and never taken from arguments.
53
93
 
54
- Most people keep `fleet.json` in their own repository and point at it:
94
+ `fleet-sync` converges the manager first, then every worker over SSH. It
95
+ fetches the commit your dev-fleet version was published from, sends it and
96
+ your configuration to each machine, installs the pinned toolchain, applies the
97
+ Ansible policy and verifies the result. It prints one table and writes a full
98
+ JSON report. The first run on a machine installs everything, mise included, so
99
+ Git is the only tool it needs beforehand.
100
+
101
+ `--dry-run` changes no machine. On the manager it checks the release out into
102
+ dev-fleet's cache and installs the Bun that release pins beside any others, so
103
+ that it can run; it never installs the toolchain, publishes the global mise
104
+ configuration, edits shell profiles or touches a worker.
105
+
106
+ `devfleet` finds the closest `fleet.json` in the directory you run it from or
107
+ above it. `DEVFLEET_CONFIG` names a file elsewhere.
108
+
109
+ The CLI runs on [Bun](https://bun.sh), which the package brings as a
110
+ dependency. If your package manager does not run dependency install scripts,
111
+ allow Bun's or install Bun yourself; the CLI uses the one on `PATH`.
112
+
113
+ ## The version
114
+
115
+ Every release of `@crvouga/dev-fleet` is published from one commit and names
116
+ it. The version in your `package.json` is therefore the revision of every
117
+ machine in the fleet:
55
118
 
56
119
  ```sh
57
- DEVFLEET_CONFIG=/path/to/your/fleet.json sh bootstrap/fleet-manager-run.sh REVISION
120
+ npx devfleet --version # 1.4.0 (commit 3f2a…)
121
+ npm install --save-dev --save-exact @crvouga/dev-fleet@latest
122
+ npx devfleet fleet-sync # the fleet moves to that release
58
123
  ```
59
124
 
60
- Pin `REVISION` to a full commit SHA of this repository; that is your version.
125
+ Install an older version and run `fleet-sync` to go back. The first worker
126
+ proves a new revision before the others receive it.
127
+
128
+ ## Your project's own tools
129
+
130
+ On a fleet machine the global mise configuration (`~/.config/mise/config.toml`
131
+ and its `mise.lock`) belongs to dev-fleet: `fleet-sync` publishes it with the
132
+ fleet's pinned tools and the agent worktree roots mise must trust. A project
133
+ pins its own tools in its own `mise.toml` and installs them with
134
+ `mise install --locked` in its checkout. It never publishes a global
135
+ configuration. If anything replaces dev-fleet's, `fleet-status` fails the check
136
+ `tool:global-mise-config` until the next `fleet-sync`.
137
+
138
+ ## Using it from code
139
+
140
+ dev-fleet has no library API. The package is the `devfleet` command: it
141
+ declares no `main` or `exports`, and nothing under `src/`, `bootstrap/` or
142
+ `ansible/` is a supported import. Any release, a patch included, may change
143
+ them, and some modules read the fleet configuration as soon as they are
144
+ imported. If your project needs something dev-fleet does, ask for a command in
145
+ an [issue](#bugs-and-feature-requests).
146
+
147
+ ## State
148
+
149
+ Your bucket is the source of truth for everything Git does not hold: the
150
+ roster of enrolled workers, the approved revision, and the configuration
151
+ `fleet-sync` last applied. It is one SQLite database that
152
+ [Litestream](https://litestream.io) replicates to the bucket. `fleet.json`
153
+ says where the bucket is and the command is passed its credentials:
154
+
155
+ ```json
156
+ "state": {
157
+ "bucket": "my-fleet-state",
158
+ "endpoint": "https://ACCOUNT.r2.cloudflarestorage.com",
159
+ "region": "auto"
160
+ }
161
+ ```
61
162
 
62
- ## Run it from npm
163
+ ```sh
164
+ npx devfleet fleet-state # where the state is, and how it stands with the bucket
165
+ ```
63
166
 
64
- The CLI is also published as `@crvouga/dev-fleet`, for any machine or project
65
- that has Node:
167
+ The copy on the manager is disposable. **To replace the manager**, clone your
168
+ project on the new machine and run the same two commands:
66
169
 
67
170
  ```sh
68
- npx @crvouga/dev-fleet commands --json
69
- npx @crvouga/dev-fleet fleet-status --local
171
+ npm ci
172
+ npx devfleet fleet-sync # with the two bucket credentials in the environment
70
173
  ```
71
174
 
72
- `fleet-sync` still runs from a clone, because it pins a git revision. See
73
- [releasing](docs/releasing.md) for how versions are published.
175
+ The roster and the approved revision come back from the bucket, and the
176
+ machine is rebuilt from Git.
177
+
178
+ Some runs are started by a machine rather than by you: a worker that hands
179
+ `fleet-sync` to the manager, for example. They get no environment, so save the
180
+ credentials on the manager once (in `~/.config/dev-fleet/bucket.json`,
181
+ readable by you alone):
182
+
183
+ ```sh
184
+ npx devfleet fleet-state --init
185
+ ```
186
+
187
+ Without a bucket everything still works from the local copy, and `fleet-sync`
188
+ warns that the manager holds the only one. [docs/state.md](docs/state.md)
189
+ explains how replication stays correct, how to move an existing fleet to a
190
+ project, and which state is not in the bucket yet.
191
+
192
+ ## Provider accounts
193
+
194
+ Provider accounts live only on the CLI proxy. `npx devfleet fleet-accounts`
195
+ lists them with their state; `--add`, `--relogin` and `--remove` change one
196
+ and propagate the result to every node, and `fleet-sync --only accounts`
197
+ propagates them without a full sync.
198
+ [docs/accounts.md](docs/accounts.md) explains how.
74
199
 
75
200
  ## Add or remove a machine
76
201
 
77
202
  1. Set up SSH from the manager to the new Ubuntu box (a non-root user with
78
203
  sudo) and join it to your tailnet.
79
- 2. Add it under `workers` in `fleet.json`:
204
+ 2. Add it under `workers` in your `fleet.json` and commit:
80
205
 
81
206
  ```json
82
207
  {
@@ -92,40 +217,111 @@ npx @crvouga/dev-fleet fleet-status --local
92
217
  }
93
218
  ```
94
219
 
95
- 3. Run `fleet-sync`. A box that has only sudo is enough: the opt-in test
96
- `bun ansible/ubuntu/tests/fresh-box.ts` proves that against a bare
97
- container.
220
+ 3. Run `fleet-sync`. A machine that has only an SSH server and an account
221
+ with sudo is enough: every pull request proves that against a bare
222
+ Ubuntu machine, over SSH
223
+ ([`fresh-box.feature`](features/fresh-box/fresh-box.feature)).
98
224
 
99
- Remove a machine by deleting its entry.
225
+ Remove a machine by deleting its entry and running `fleet-sync`.
100
226
 
101
227
  ## Configuration
102
228
 
103
- `fleet.json` is validated by [`schema/fleet.schema.json`](schema/fleet.schema.json);
104
- [`examples/fleet.json`](examples/fleet.json) is a complete example.
229
+ Your `fleet.json` is validated by
230
+ [`schema/fleet.schema.json`](schema/fleet.schema.json), which the installed
231
+ package carries: `fleet-state --example` prints a complete example that points
232
+ `$schema` at it, so an editor checks the file as you type.
233
+ [`examples/fleet.json`](examples/fleet.json) is the same example.
105
234
 
106
235
  | Block | What it declares |
107
236
  | ---------- | ------------------------------------------------------------------- |
108
237
  | `source` | The Git repository nodes accept code from (this one, or your fork) |
238
+ | `state` | The bucket that holds the fleet state; never its credentials |
109
239
  | `secrets` | Where secrets are kept: `env`, `file`, `sops` or `vault` |
110
240
  | `manager` | The one macOS manager |
111
241
  | `workers` | The Ubuntu workers |
112
242
  | `services` | The proxy's public origin and the monitoring dashboard and accounts |
113
243
  | `worker` | Policy every worker must meet |
244
+ | `ides` | The IDEs every machine runs; `["t3"]` when absent |
245
+ | `t3` | T3 settings every machine holds (`profile`) and `requiredModels` |
246
+
247
+ ### Supported IDEs
248
+
249
+ Every supported IDE is reached through one interface (`src/ide/ide.ts`): its
250
+ settings on each machine, the manager's own copy (pinned version and policy)
251
+ and what the manager measures of each worker. Enabling one is `ides` in
252
+ `fleet.json`.
253
+
254
+ | IDE | `ides` | Manager | Workers |
255
+ | ------- | ------ | ----------------------------------------------------- | ------------------------------------------ |
256
+ | T3 Code | `t3` | Pinned CLI, policy with updates off, version measured | Pinned desktop app, locked policy, pairing |
257
+
258
+ Under `t3.profile`, `fleet.json` names top-level T3 settings every machine
259
+ holds the same, such as tool servers. A setting not named there stays the
260
+ machine's own, and one edited on a machine since `fleet-sync` last wrote it
261
+ is reported as `T3_PROFILE_CONFLICT` by the check `t3:profile`, never
262
+ overwritten. The check `t3:models` reports a model T3 offers that the proxy
263
+ does not serve, and a model in `t3.requiredModels` that is missing.
114
264
 
115
265
  Secrets are asked for by purpose (`proxy-client-key`,
116
266
  `monitoring-account-password`, `monitoring-tunnel-token`,
117
267
  `proxy-tunnel-token`); see [`src/secrets.ts`](src/secrets.ts).
118
268
 
269
+ `fleet-sync` records the configuration it applies in the fleet state, so a
270
+ run started outside your project uses the one last applied.
271
+ `fleet-state --export` prints it.
272
+
273
+ To run your own fork, publish it as your own package and name its repository
274
+ under `source`: the commit a package names must be reachable there.
275
+
276
+ ## Bugs and feature requests
277
+
278
+ A defect in dev-fleet, or something it cannot do yet, is raised as an
279
+ [issue](https://github.com/crvouga/dev-fleet/issues). From your project that
280
+ is the only way to change dev-fleet, for you and for the coding agents working
281
+ there. Do not patch the installed package, a checkout `fleet-sync` put on a
282
+ machine or a file it manages: the next install or `fleet-sync` replaces them.
283
+
284
+ The tool says so itself: `devfleet --help` names the issue tracker, and the
285
+ agent contract (`devfleet commands --json`, under `agent.support`) carries the
286
+ rule, what an issue should include and what it must leave out. Agents only
287
+ read it if your project sends them there, so put this in the `AGENTS.md` of
288
+ the project that holds your `fleet.json`:
289
+
290
+ ```md
291
+ The fleet is managed with dev-fleet (`npx devfleet`, pinned in package.json).
292
+ Read `npx devfleet commands --json` before using it. Never edit dev-fleet
293
+ itself: not node_modules, not a clone, not what it installs on a machine. A
294
+ bug in dev-fleet or a feature it lacks is a GitHub issue at
295
+ https://github.com/crvouga/dev-fleet/issues; tell me what is blocked and link
296
+ it. Issues are public: no credentials, hostnames, accounts, domains or
297
+ fleet.json contents.
298
+ ```
299
+
119
300
  ## Known limits
120
301
 
121
302
  - Each worker's own login and renewal, and the proxy's backup and recovery,
122
303
  are implemented for the **`vault`** secrets provider only. With `env`, `file`
123
304
  or `sops` the fleet's three shared secrets are read, but those two features
124
305
  are not available yet.
125
- - State on machines still uses the historical `personal` prefix
306
+ - The bucket holds the fleet's own state. The CLI proxy keeps its separate
307
+ encrypted backup (`vault` only), and the monitoring hub's database is not
308
+ replicated: `fleet-sync` recreates its accounts and systems, but metric
309
+ history is lost with the manager. See
310
+ [what is not in the bucket yet](docs/state.md#what-is-not-in-the-bucket-yet).
311
+ - Caches and units on machines still use the historical `personal` prefix
126
312
  (`~/.local/share/personal/fleet`, `/etc/personal-fleet`, unit names). It
127
313
  works, and renaming it needs a migration.
314
+ - The CLI proxy's installed code is a Git worktree of the manager's copy of
315
+ the source repository (`~/.local/share/personal/fleet/manager/source`).
316
+ `fleet-sync` fetches that copy again when it is deleted, but the proxy then
317
+ needs `devfleet cliproxy install` once more.
128
318
  - The manager is a Mac host. There is no container image of the manager.
319
+ - `fleet-sync --schedule enable` (unattended runs on a timer) is not available
320
+ from the published package yet, so nothing re-applies the fleet until you
321
+ run `fleet-sync`.
322
+ - Not everything on a machine is pinned. [What is declared, and
323
+ where](docs/declared.md) lists what is and what is not: most of a worker's
324
+ own packages, the manager's applications and every login are outside it.
129
325
  - The reference documents under `docs/` were written for the fleet this came
130
326
  from and are being revised.
131
327
 
@@ -134,24 +330,56 @@ Secrets are asked for by purpose (`proxy-client-key`,
134
330
  | Path | What |
135
331
  | ------------ | ----------------------------------------------------------------- |
136
332
  | `src/` | Inventory, orchestration, verification, the report; `src/cli` CLI |
333
+ | `src/state/` | The fleet state database and its replication to the bucket |
137
334
  | `ansible/` | Machine policy: `ubuntu/`, `macos/`, `monitoring/` |
138
335
  | `agents/` | Agent launchers T3 runs, and their registry |
139
336
  | `cliproxy/` | The CLIProxyAPI service: install, audit, backup, model health |
140
337
  | `bootstrap/` | Toolchain bootstrap and the manager and worker entry scripts |
141
338
  | `schema/` | JSON schemas for `fleet.json` and the private roster |
142
339
  | `examples/` | A complete example configuration |
340
+ | `features/` | What dev-fleet does, as Gherkin features: the tests |
341
+ | `bin/` | The entry point of the published package |
143
342
 
144
343
  ## Develop
145
344
 
345
+ Working on dev-fleet itself is the only reason to clone it:
346
+
146
347
  ```sh
348
+ git clone https://github.com/crvouga/dev-fleet.git && cd dev-fleet
147
349
  bash bootstrap/bootstrap.sh
148
350
  bun install --frozen-lockfile
149
351
  bun run check
352
+ ./devfleet fleet-status --local # the CLI, from this checkout
150
353
  ```
151
354
 
152
- `bun run check` runs formatting, typecheck, lint, the TypeScript tests and the
153
- proxy package's Python tests. Tests run against `examples/fleet.json` and
154
- never contact a real machine.
355
+ Run from a checkout, `fleet-sync` converges the fleet to the checkout's `HEAD`
356
+ commit (never to uncommitted files). The checkout's `origin` must be the
357
+ repository `fleet.json` names under `source`. Every merge to `main` publishes a release;
358
+ see [releasing](docs/releasing.md).
359
+
360
+ What dev-fleet does is specified in Gherkin, under
361
+ [`features/`](features/README.md): a `.feature` file states a rule in plain
362
+ words and the steps beside it check it. There are no other TypeScript tests.
363
+ `bun run check` runs formatting, typecheck, lint, every feature and the proxy
364
+ package's Python tests. Features run against `examples/fleet.json` and never
365
+ contact a real machine.
366
+
367
+ Every pull request runs these checks ([`ci.yml`](.github/workflows/ci.yml)):
368
+
369
+ | Check | What it proves |
370
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
371
+ | Check | `bun run check` |
372
+ | Fleet state | The pinned Litestream replicates the fleet state to a directory and to an S3 API, and a second manager restores it |
373
+ | Fresh worker over SSH | A new Ubuntu machine with only an SSH server and sudo is bootstrapped by the production code over real SSH, on amd64 and on arm64 |
374
+ | Workflows | The workflow files pass `actionlint` |
375
+
376
+ Some scenarios need something a laptop may not have, and are skipped there:
377
+
378
+ ```sh
379
+ DEVFLEET_TEST_FRESH_BOX=1 bun test features/fresh-box # Docker; tests the HEAD commit
380
+ mise install litestream
381
+ DEVFLEET_TEST_S3=http://127.0.0.1:5000 bun test features/state
382
+ ```
155
383
 
156
384
  ## License
157
385
 
package/agents/setup.md CHANGED
@@ -1,3 +1,11 @@
1
+ # Bugs and feature requests
2
+
3
+ This file ships inside the dev-fleet package. If you are an agent working in
4
+ another project and dev-fleet is wrong or lacks something, do not edit it
5
+ here, in a clone or on a machine. Raise an issue in the dev-fleet repository;
6
+ `devfleet --help` prints the address and `devfleet commands --json` carries
7
+ the rule under `agent.support`.
8
+
1
9
  # Manager-led Ubuntu fleet setup
2
10
 
3
11
  For the enrolled Ubuntu development fleet, run on the designated manager:
@@ -16,23 +24,17 @@ The legacy local commands below retain their existing behavior for compatibility
16
24
  Use **personal** for node and T3 configuration. Workstation and fleet are internal
17
25
  implementation packages; neither exports its own executable.
18
26
 
19
- After the shared [machine bootstrap](../../../bootstrap/README.md), run in the
20
- personal checkout:
27
+ `fleet-sync` installs the `devfleet` launcher on every machine it converges,
28
+ linked to the fleet-owned checkout of the applied revision. On such a machine:
21
29
 
22
30
  ```sh
23
- sh bootstrap/install-launcher.sh
24
- devfleet fleet-node-sync
25
31
  devfleet fleet-status
26
32
  ```
27
33
 
28
- The npm fallback is the same interface:
29
-
30
- ```sh
31
- devfleet fleet-node-sync
32
- ```
33
-
34
- Use `PERSONAL_WORKSPACE` or `--workspace PATH` when invoking the npm CLI outside
35
- the checkout. The installed source launcher resolves its checkout automatically.
34
+ `fleet-node-sync` reconciles one machine from a checkout of dev-fleet
35
+ (`--workspace PATH` or `PERSONAL_WORKSPACE` names it). It is for development:
36
+ run from the published package it is refused, because `fleet-sync` does the
37
+ same from a checkout it owns.
36
38
  Each machine still needs its own authorized Vault session, SSH keys and T3 pairing.
37
39
  The sync never copies them or provisions infrastructure.
38
40
 
@@ -111,6 +113,34 @@ T3's installed server source at revision
111
113
  external settings watch/reload, and optimistic CLI writes. Future T3 schema changes
112
114
  require adapter verification. A disk-level success is not a live inference test.
113
115
 
116
+ ## Supported alternatives
117
+
118
+ `PROVIDER_PROTOCOL_INCOMPATIBLE` means the gateway answered that it has no
119
+ endpoint, method or media type for the protocol a route speaks (HTTP 404, 405,
120
+ 415 or 501). The route as declared cannot work, so the check fails and the
121
+ sync exits nonzero; signing in again or retrying changes nothing. Nothing on
122
+ the machine is changed to work around it.
123
+
124
+ The protocol a tool speaks to the proxy is its `proxy` field in
125
+ `agents/registry.json` (`responses`, `messages`, `generateContent` or
126
+ `chat_completions`). The supported alternatives, in this order:
127
+
128
+ 1. A model the proxy lists at `/v1/models` for the same protocol: change the
129
+ tool's model in `agents/registry.json`.
130
+ 2. Another protocol the tool's own CLI speaks and the pinned proxy serves.
131
+ `chat_completions` is served for OpenAI-compatible clients such as
132
+ OpenCode and Grok.
133
+ 3. The tool's direct profile, signed in natively on that machine, instead of
134
+ the shared proxy.
135
+
136
+ Each is a reviewed change to a tracked file, followed by `devfleet
137
+ fleet-sync`. A newer CLIProxyAPI is a reviewed change to
138
+ `cliproxy/image.lock.json`; a sync never upgrades it to get around a failure.
139
+
140
+ A model that is merely missing from the catalog is `PROVIDER_MODEL_UNAVAILABLE`
141
+ instead: a warning, because it is usually the provider account's (billing or
142
+ access), not the route's.
143
+
114
144
  ## Other local operations
115
145
 
116
146
  All former workstation operations are available through `dev-fleet`:
@@ -1,26 +1,26 @@
1
1
  ---
2
2
  # Manager power policy. Workers depend on the manager's singleton services
3
3
  # (CLI proxy, Beszel hub), so the manager never sleeps on AC power. Sleep on
4
- # battery is left to the human. Run once with: ansible-playbook -K manager.yml
4
+ # battery is left to the human. The settings are the `pmset` object in
5
+ # policy.json; fleet-sync measures the same object as manager:power. Changing
6
+ # them needs an administrator: ansible-playbook -K manager.yml
5
7
  - name: Keep the fleet manager awake on AC power
6
8
  hosts: localhost
7
9
  connection: local
8
10
  become: true
9
11
  gather_facts: false
10
- vars:
11
- manager_pmset_ac:
12
- sleep: '0'
13
- womp: '1'
12
+ vars_files:
13
+ - policy.json
14
14
  tasks:
15
15
  - name: Inspect effective power management settings
16
16
  ansible.builtin.command:
17
17
  argv: [/usr/bin/pmset, -g, custom]
18
18
  changed_when: false
19
19
  register: pmset_custom
20
- - name: Never sleep while on AC power
20
+ - name: Apply the declared AC power settings
21
21
  ansible.builtin.command:
22
22
  argv: [/usr/bin/pmset, -c, '{{ item.key }}', '{{ item.value }}']
23
- loop: '{{ manager_pmset_ac | dict2items }}'
23
+ loop: '{{ pmset | dict2items }}'
24
24
  loop_control:
25
25
  label: '{{ item.key }}'
26
26
  vars:
@@ -28,3 +28,6 @@
28
28
  observed: "{{ ac_section | regex_findall('^\\s*' ~ item.key ~ '\\s+(\\S+)', multiline=True) | first | default('') }}"
29
29
  when: observed != item.value
30
30
  changed_when: true
31
+
32
+ - name: Stage the fleet's T3 policy
33
+ ansible.builtin.import_playbook: t3-policy.yml
@@ -1,17 +1,9 @@
1
1
  {
2
2
  "version": 1,
3
3
  "minimumMajorVersion": 14,
4
- "ansibleCore": "2.19.9",
5
4
  "pmset": {
6
5
  "sleep": "0",
7
- "disksleep": "0",
8
- "autorestart": "1",
9
- "womp": "1",
10
- "powernap": "0"
6
+ "womp": "1"
11
7
  },
12
- "autoLogOutDelay": 0,
13
- "screensaverIdleTime": 0,
14
- "tailscaleDaemon": "com.tailscale.tailscaled",
15
- "beszelDaemon": "dev.devfleet.beszel-agent",
16
- "credentialRenewalAgent": "dev.devfleet.worker-auth"
8
+ "beszelDaemon": "dev.devfleet.beszel-agent"
17
9
  }
@@ -0,0 +1,49 @@
1
+ ---
2
+ # T3 Code on the manager reads the same administrator policy as on every
3
+ # worker (ansible/ubuntu/t3/policy.json): updates off, so the manager's T3
4
+ # changes only when the fleet's pin does. Provider instances are reconciled
5
+ # by fleet-sync on every run and stay unlocked here: locking them needs an
6
+ # administrator for each change of the account roster. fleet-sync measures
7
+ # the result as t3:updates. Needs an administrator: ansible-playbook -K manager.yml
8
+ - name: Stage the fleet's T3 policy on the manager
9
+ hosts: localhost
10
+ connection: local
11
+ become: true
12
+ gather_facts: false
13
+ vars:
14
+ t3_policy_directory: /Library/Application Support/T3Code
15
+ tasks:
16
+ - name: Inspect existing administrator policy
17
+ ansible.builtin.stat:
18
+ path: '{{ t3_policy_directory }}/policy.json'
19
+ register: policy_file
20
+ - name: Inspect Fleet ownership marker
21
+ ansible.builtin.stat:
22
+ path: '{{ t3_policy_directory }}/.personal-fleet-policy'
23
+ register: policy_owner
24
+ - name: Refuse to overwrite an unrelated administrator policy
25
+ ansible.builtin.assert:
26
+ that: not policy_file.stat.exists or policy_owner.stat.exists
27
+ fail_msg: T3_POLICY_OWNER_CONFLICT; review and merge the existing administrator policy in Git before adoption.
28
+ - name: Create administrator-owned policy directory
29
+ ansible.builtin.file:
30
+ path: '{{ t3_policy_directory }}'
31
+ state: directory
32
+ owner: root
33
+ group: wheel
34
+ mode: '0755'
35
+ - name: Record policy ownership before installation
36
+ ansible.builtin.copy:
37
+ dest: '{{ t3_policy_directory }}/.personal-fleet-policy'
38
+ content: "ansible/ubuntu/t3/policy.json\n"
39
+ owner: root
40
+ group: wheel
41
+ mode: '0644'
42
+ - name: Install the fleet's policy with backup
43
+ ansible.builtin.copy:
44
+ content: "{{ lookup('ansible.builtin.file', '../ubuntu/t3/policy.json') | from_json | to_nice_json }}"
45
+ dest: '{{ t3_policy_directory }}/policy.json'
46
+ owner: root
47
+ group: wheel
48
+ mode: '0644'
49
+ backup: true