@crvouga/dev-fleet 0.3.0 → 0.4.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 (77) hide show
  1. package/README.md +22 -16
  2. package/ansible/github/git.yml +66 -0
  3. package/ansible/macos/policy.json +11 -1
  4. package/ansible/monitoring/compose.yml +10 -0
  5. package/ansible/monitoring/container-watchdog.py +118 -0
  6. package/ansible/monitoring/hub-darwin.yml +146 -0
  7. package/ansible/net/remote-probe.sh +5 -0
  8. package/ansible/ubuntu/baseline.yml +2 -0
  9. package/ansible/ubuntu/disk-gc.py +25 -3
  10. package/ansible/ubuntu/disk-guard.py +24 -5
  11. package/ansible/ubuntu/policy.json +50 -1
  12. package/ansible/ubuntu/tasks/browser.yml +56 -0
  13. package/ansible/ubuntu/tasks/docker.yml +52 -0
  14. package/ansible/ubuntu/tasks/resources.yml +2 -2
  15. package/ansible/ubuntu/tasks/services.yml +13 -0
  16. package/bootstrap/doctor.ts +32 -6
  17. package/bootstrap/fleet-remote-bootstrap.sh +31 -4
  18. package/bootstrap/setup.ts +23 -8
  19. package/bootstrap/worker-tools.ts +118 -0
  20. package/cliproxy/README.md +1 -1
  21. package/cliproxy/compose.yaml +4 -0
  22. package/cliproxy/scripts/checkpoint.py +1 -1
  23. package/docs/authentication.md +90 -0
  24. package/docs/declared.md +2 -0
  25. package/docs/machine-setup.md +22 -12
  26. package/docs/operations.md +87 -31
  27. package/docs/runbooks.md +380 -0
  28. package/docs/state.md +13 -7
  29. package/docs/ubuntu-sync.md +7 -6
  30. package/examples/fleet.json +19 -1
  31. package/llms.txt +2 -0
  32. package/mise.toml +7 -0
  33. package/package.json +2 -2
  34. package/schema/check-ids.json +522 -0
  35. package/schema/fleet.schema.json +178 -1
  36. package/schema/output/check.schema.json +23 -0
  37. package/schema/output/error.schema.json +14 -0
  38. package/schema/output/fleet-report.schema.json +79 -0
  39. package/schema/output/fleet-sync.schema.json +112 -0
  40. package/schema/output/report.schema.json +38 -0
  41. package/src/auth-checks.ts +3 -0
  42. package/src/cli/agent-guide.ts +5 -3
  43. package/src/cli/fleet-report.ts +126 -0
  44. package/src/cli/fleet-sync-manager.ts +11 -0
  45. package/src/cli/fleet-sync-plan.ts +200 -0
  46. package/src/cli/fleet-sync-remote.ts +3 -1
  47. package/src/cli/fleet-sync-schedule.ts +142 -35
  48. package/src/cli/fleet-sync.ts +90 -8
  49. package/src/cli/fleet-watch.ts +198 -0
  50. package/src/cli/fleet.ts +4 -0
  51. package/src/cli/register-fleet.ts +51 -0
  52. package/src/cli/registry.ts +13 -0
  53. package/src/cli/schedule-logs.ts +36 -0
  54. package/src/clock.ts +63 -0
  55. package/src/definition.ts +142 -5
  56. package/src/git-identity.ts +160 -0
  57. package/src/machine-health.ts +51 -4
  58. package/src/manager-health.ts +205 -0
  59. package/src/manager-sync.ts +12 -2
  60. package/src/monitoring/hub-backup.ts +63 -17
  61. package/src/monitoring/manager-hub.ts +44 -2
  62. package/src/node-converge-linux.ts +16 -5
  63. package/src/notify.ts +239 -0
  64. package/src/remote-bootstrap.ts +87 -25
  65. package/src/remote-operation.ts +213 -0
  66. package/src/schedule-check.ts +40 -0
  67. package/src/secrets.ts +26 -8
  68. package/src/ssh.ts +10 -2
  69. package/src/state/history.ts +172 -0
  70. package/src/state/store.ts +11 -4
  71. package/src/tailscale.ts +51 -0
  72. package/src/toolchain-checks.ts +8 -3
  73. package/src/worker-capability-checks.ts +167 -0
  74. package/src/worker-capacity.ts +19 -7
  75. package/src/worker-host-checks.ts +32 -0
  76. package/src/worker-policy.ts +33 -0
  77. package/src/worker-readiness.ts +10 -1
@@ -181,19 +181,21 @@ wrangler login
181
181
  railway login
182
182
  ```
183
183
 
184
- Only sign into providers the task actually needs. Git identity is personal:
185
- set `git config --global user.name` and `git config --global user.email` after
186
- confirming the desired name/email. Do not guess an email or overwrite an existing
187
- identity. Enable Git LFS hooks when needed with `git lfs install`.
184
+ Only sign into providers the task actually needs. On fleet machines the Git
185
+ identity and the GitHub credential helper come from the `git` block of
186
+ `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
+ config --global user.name` and `user.email` after confirming them; do not guess
189
+ an email or overwrite an existing identity. Enable Git LFS hooks when needed with `git lfs install`.
188
190
 
189
191
  Optional existing workstation/agent settings:
190
192
 
191
193
  ```sh
192
194
  sh bootstrap/install-launcher.sh
193
- personal workstation doctor
195
+ devfleet fleet-status --local
194
196
  # Only for tasks needing the workspace's managed secrets:
195
197
  vault login --method=userpass username=example-user
196
- personal workstation sync
198
+ devfleet fleet-sync
197
199
  ```
198
200
 
199
201
  The machine bootstrap installs tools/dependencies only. `personal:install` additionally
@@ -231,7 +233,7 @@ The manifest installs the **Docker client**. Containers also require a local or
231
233
  remote Docker engine, Compose v2 and Buildx plugins. These cannot be installed as
232
234
  one identical unprivileged binary on all OSes.
233
235
 
234
- - **macOS:** run `personal workstation tools docker-setup` to install the pinned,
236
+ - **macOS:** run `bash bootstrap/docker-desktop.sh` to install the pinned,
235
237
  checksum-verified Docker Desktop bundle from this repository and open it.
236
238
  Complete its first-run agreement/setup in the app. Repeated invocation reuses
237
239
  an existing installation. The shared mise Docker client remains in use.
@@ -280,11 +282,11 @@ mise install --locked bun
280
282
  mise exec --locked bun -- bun bootstrap/setup.ts
281
283
  ```
282
284
 
283
- For an installed workstation CLI, `personal workstation update` pulls with a clean-tree/fast-forward
284
- guard, refreshes the toolchain/global config and frozen dependencies, and runs the
285
- existing workstation convergence. `personal workstation tools sync` reapplies tool pins/global config
286
- without pulling or touching project dependencies; `personal workstation tools check --json` verifies
287
- them. Sync is explicit; Git commits do not silently update other machines.
285
+ On fleet machines `devfleet fleet-sync` refreshes the toolchain, the global
286
+ configuration and frozen dependencies; `mise exec --locked bun -- bun
287
+ bootstrap/setup.ts --tools-only` reapplies tool pins and the global
288
+ configuration without touching project dependencies, and `bun
289
+ bootstrap/doctor.ts --json` verifies them. Sync is explicit; Git commits do not silently update other machines.
288
290
 
289
291
  For multiple development computers, use [the fleet package](operations.md):
290
292
  `devfleet fleet-node-add` registers an existing SSH alias in a local roster,
@@ -395,6 +397,14 @@ It installs the `vars.worker_tools` subset from `mise.toml`, with versions and
395
397
  artifacts taken from the same `mise.lock`; native T3 and Ubuntu Docker/service
396
398
  policy remain source-pinned. `bun bootstrap/doctor.ts --worker --docker --json`
397
399
  checks that profile. It does not require unrelated cloud CLIs on workers.
400
+
401
+ Tool profiles add locked tool sets on top of it: `worker.toolProfiles` in
402
+ `fleet.json` (fleet-wide) or `toolProfiles` on one worker selects `web` (pnpm,
403
+ Deno, Yarn and headless Chromium for browser tests), `go`, `rust` (with CMake
404
+ and Ninja) or `jvm` (Temurin), each a `worker_profile_<name>` under `[vars]` in
405
+ `mise.toml`. `worker.tools` adds single tools dev-fleet already pins. The setup
406
+ installs them (`--node` reads the installed configuration) and the doctor
407
+ checks each as `tool:<name>` (`--profiles`, `--tools`).
398
408
  The source-owned `worker-auth.ts` timer renews the worker's existing Vault token
399
409
  and can restore the declared GitHub account from its authorized Vault service
400
410
  credential, without copying another machine's login directory.
@@ -4,9 +4,9 @@
4
4
 
5
5
  # Developer fleet
6
6
 
7
- `@pkgs/fleet` diagnoses development computers: SSH, VS Code Remote-SSH,
7
+ `devfleet` diagnoses development computers: SSH, VS Code Remote-SSH,
8
8
  per-machine authentication, T3 Connect and shared toolchain/dependency drift.
9
- It is a local CLI/library, not a daemon or cloud manager. It reuses the
9
+ It is a local CLI, not a library, a daemon or a cloud manager. It reuses the
10
10
  existing mise/workstation owners rather than introducing another installer.
11
11
 
12
12
  The fleet is one singleton **manager** (the human's macOS machine, which runs
@@ -46,8 +46,9 @@ devfleet fleet-repair --node dev-linux --repair workspace --json # plan only
46
46
  devfleet fleet-repair --node dev-linux --repair workspace --apply --yes --json
47
47
  ```
48
48
 
49
- This private package exposes a TypeScript API from `@pkgs/fleet`; the public
50
- executable is `personal` (`bun run personal` from the checkout). Schema, roster
49
+ dev-fleet is a command with no library API: run it as `npx devfleet` from the
50
+ project that holds your `fleet.json` (`./devfleet` from a checkout of dev-fleet
51
+ itself). Schema, roster
51
52
  listing, onboarding and repair previews do not contact nodes or auth services.
52
53
  Status never opens a login flow.
53
54
 
@@ -61,7 +62,7 @@ remains the sole cloud resource owner; fleet creates no networks, hosts or secre
61
62
 
62
63
  ```json
63
64
  {
64
- "$schema": "./packages/fleet/schema.json",
65
+ "$schema": "./node_modules/@crvouga/dev-fleet/schema/roster.schema.json",
65
66
  "version": 1,
66
67
  "nodes": [
67
68
  {
@@ -97,21 +98,50 @@ remote nodes' required lists. Run `fleet-nodes --schema` for editor/agent valida
97
98
 
98
99
  ## Evidence and diagnosis
99
100
 
100
- | Check | Evidence / next action |
101
- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | `workspace`, `git:clean` | HEAD, input availability and dirty-tree status; no paths/diffs/file contents logged. |
103
- | `revision`, `sync:*` | Compare exact HEAD and SHA-256 of package.json, bun.lock, mise.toml, mise.lock against manager. Missing inputs never pass. |
104
- | `dependencies` | Last successful fleet frozen-sync receipt matches input hashes. Missing receipt is unverified, not in-sync. |
105
- | `auth:toolchain` | Existing bootstrap/doctor.ts runs in the node checkout with its Bun, bounded to 90s. Use personal workstation tools check --json for individual pins. |
106
- | `ssh:agent` | Manager agent exposes an identity; missing agent warns because IdentityFile keys may still work. |
107
- | `ssh:session` | Fresh batch SSH session actually runs the Node/Bun probe. No reused multiplexed connection or agent forwarding. |
108
- | `ssh:host-key`, `ssh:dns`, `ssh:auth`, `ssh:network`, `ssh:runtime`, `ssh:timeout` | Classified failure and targeted remedy, without raw logs. |
109
- | `auth:github` | gh auth status --active succeeds on this machine; not proof of Git SSH or every repository permission. |
110
- | `auth:vault` | Server-side vault token lookup succeeds in this context; not proof of access to every secret path. |
111
- | `auth:vscode` | code CLI finds the local Remote-SSH extension. Other profiles/Cursor/GUI PATH/agents can differ. |
112
- | `t3:settings` | Connect explicitly enabled and not revoked in t3 status --json; settings are not connectivity. |
113
- | `t3:api` | Read-only t3 environment list --json succeeds against the local API; no pairing/token minting. |
114
- | `t3:end-to-end`, `vscode:forwarding` | SKIP: actual relay/peer authorization and VS Code tunnel/server installation require operator testing. |
101
+ | Check | Evidence / next action |
102
+ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `workspace`, `git:clean` | HEAD, input availability and dirty-tree status; no paths/diffs/file contents logged. |
104
+ | `revision`, `sync:*` | Compare exact HEAD and SHA-256 of package.json, bun.lock, mise.toml, mise.lock against manager. Missing inputs never pass. |
105
+ | `dependencies` | Last successful fleet frozen-sync receipt matches input hashes. Missing receipt is unverified, not in-sync. |
106
+ | `auth:toolchain` | Existing bootstrap/doctor.ts runs in the node checkout with its Bun, bounded to 90s. Run `bun bootstrap/doctor.ts --json` in the node's checkout for individual pins. |
107
+ | `ssh:agent` | Manager agent exposes an identity; missing agent warns because IdentityFile keys may still work. |
108
+ | `ssh:session` | Fresh batch SSH session actually runs the Node/Bun probe. No reused multiplexed connection or agent forwarding. |
109
+ | `ssh:host-key`, `ssh:dns`, `ssh:auth`, `ssh:network`, `ssh:runtime`, `ssh:timeout` | Classified failure and targeted remedy, without raw logs. |
110
+ | `auth:github` | gh auth status --active succeeds on this machine; not proof of Git SSH or every repository permission. |
111
+ | `auth:vault` | Server-side vault token lookup succeeds in this context; not proof of access to every secret path. |
112
+ | `auth:vscode` | code CLI finds the local Remote-SSH extension. Other profiles/Cursor/GUI PATH/agents can differ. |
113
+ | `t3:settings` | Connect explicitly enabled and not revoked in t3 status --json; settings are not connectivity. |
114
+ | `t3:api` | Read-only t3 environment list --json succeeds against the local API; no pairing/token minting. |
115
+ | `t3:end-to-end`, `vscode:forwarding` | SKIP: actual relay/peer authorization and VS Code tunnel/server installation require operator testing. |
116
+
117
+ ### Machine health
118
+
119
+ | Check | Evidence / next action |
120
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
+ | `worker:capacity:inodes` | Free inodes on the home filesystem, against `inodeWarnFreePercent`/`inodeFailFreePercent` in `ansible/ubuntu/policy.json`. The disk guard and disk collector act on the same floors. |
122
+ | `worker:clock` | `timedatectl` says the clock is synchronised and a time service is enabled; with a reference time, the offset is held to `clock` in the policy. |
123
+ | `worker:reboot-pending` | `/var/run/reboot-required` exists: warns with its age, fails after `rebootPendingFailDays`. Reboot when no agent turn is running. |
124
+ | `tailscale:key-expiry` | The node key expiry Tailscale reports: warns 14 days ahead, fails 3 days ahead. See [Tailscale key expiry](#tailscale-key-expiry). |
125
+ | `manager:release` | `sw_vers` against `minimumMajorVersion` in `ansible/macos/policy.json`. |
126
+ | `manager:updates` | `softwareupdate --list --no-scan` (nothing is downloaded): warns on pending updates, and names one that waits for a restart. Names never shown. |
127
+ | `manager:disk` | Free space on the data volume and the space Docker holds, against `disk` in `ansible/macos/policy.json`. |
128
+ | `manager:container-watchdog` | The container watchdog (`ansible/monitoring/container-watchdog.py`, a launch agent `hub-darwin.yml` installs) restarts a labelled container that was unhealthy for `unhealthyChecks` runs, at most `maxRestartsPerHour` times an hour (`containerWatchdog` in `ansible/macos/policy.json`). Warns after a restart; fails once it stops restarting. |
129
+
130
+ ### Tailscale key expiry
131
+
132
+ A node drops off the tailnet on the day its key expires. Keep workers from
133
+ expiring: tag them, or disable key expiry for each in the Tailscale admin
134
+ console (tagged nodes do not expire). `tailscale:key-expiry` warns two weeks
135
+ ahead and fails three days ahead.
136
+
137
+ To re-join a worker whose key expired, a person signs it in again, because
138
+ only a person's identity can authorise a device:
139
+
140
+ 1. On the worker's console, run `sudo tailscale up` and open the link it
141
+ prints, signed in to the fleet's tailnet.
142
+ 2. In the admin console, disable key expiry for the worker, or tag it.
143
+ 3. From the manager, run `devfleet fleet-sync`: `ansible/ubuntu/tailscale.yml`
144
+ verifies the login and `tailscale:key-expiry` passes.
115
145
 
116
146
  An SSH shell is not proof that VS Code or T3 Connect works. Diagnostics check
117
147
  **directed manager → node** connectivity, not all pairs. Run from each intended
@@ -147,7 +177,7 @@ need normal repository validation. A green install is not a healthy fleet.
147
177
  5. Authenticate separately: GitHub, Git SSH, Vault, coding providers and T3 pairing
148
178
  are distinct sessions. Use gh auth login/setup-git, approved vault login and
149
179
  operator-driven t3 up --connect as needed. Never put secrets in reports.
150
- Workstation/provider configuration remains an explicit `personal workstation sync` operation.
180
+ Workstation and provider configuration are applied by `devfleet fleet-sync`.
151
181
  6. In VS Code's local profile install Remote-SSH, select the same SSH binary,
152
182
  alias/config, enable its login terminal temporarily and inspect Output →
153
183
  Remote - SSH. Restart after PATH/agent changes. Check disk space, remote
@@ -178,7 +208,7 @@ serially; diagnostics run at most four remote probes concurrently.
178
208
  setup can leave partial installs; no rollback is claimed. Trusted checkout
179
209
  package lifecycle scripts can execute during installation.
180
210
  - No other application repos, credentials, provider settings or cloud resources
181
- are synchronized. `personal workstation update`/`personal workstation sync` remain explicit broader convergence.
211
+ are synchronized. `devfleet fleet-sync` remains the broader convergence.
182
212
  - Run doctor again afterward; successful sync does not prove connectivity/auth.
183
213
 
184
214
  Subprocesses have deadlines and a combined 1 MiB output cap. SSH probes default
@@ -204,17 +234,43 @@ not a way to certify the whole fleet from one shell. Usage errors use
204
234
  exception strings. Fleet never prompts; --non-interactive is accepted.
205
235
 
206
236
  No telemetry or report file is written automatically. Reports still contain
207
- aliases/hashes: review before sharing publicly. For monitoring, schedule read-only
208
- `devfleet fleet-status --json` in your existing task runner and alert on exit 1/2;
209
- do not schedule automatic sync/repairs. `bun run health` remains the separate
237
+ aliases/hashes: review before sharing publicly. Do not schedule automatic
238
+ sync/repairs.
239
+
240
+ ## Notifications and the heartbeat
241
+
242
+ `devfleet fleet-watch` runs `fleet-status --workers`, compares the result with
243
+ the previous run and notifies only when a worker changes between ready and
244
+ degraded, and again every `repeatHours` (default 24) while it stays degraded.
245
+ Name the targets in `fleet.json`; the address of each is a secret, read through
246
+ the secret provider like any other:
247
+
248
+ ```json
249
+ "notify": { "targets": ["webhook", "ntfy"], "repeatHours": 24 }
250
+ ```
251
+
252
+ | Target | Secret | What is sent |
253
+ | --------- | -------------------- | -------------------------------------------------------------------- |
254
+ | `webhook` | `notify-webhook-url` | A JSON POST: `source`, `event`, `node`, `checks` (check IDs), `text` |
255
+ | `ntfy` | `notify-ntfy-url` | A text POST to the topic URL |
256
+
257
+ A notification carries node and check IDs only. `devfleet fleet-watch --test`
258
+ sends one test notification to every target.
259
+
260
+ Every run stores what it saw, and when, as `watch.json` beside the fleet state
261
+ in the bucket. That time is the manager's heartbeat. A watcher on the manager
262
+ notices nothing when the manager itself is off, so call the reusable
263
+ `fleet-heartbeat.yml` workflow from the repository that holds your
264
+ `fleet.json` (its header shows the one `uses:` line): it fails, and GitHub
265
+ notifies you, when the heartbeat is older than `max-age-minutes`. `bun run health` remains the separate
210
266
  single read-only **cloud platform** report.
211
267
 
212
268
  ## Development
213
269
 
214
270
  ```sh
215
- bun run --cwd packages/fleet tc
216
- bun run --cwd packages/fleet lint
217
- bun run --cwd packages/fleet test
271
+ bun run tc
272
+ bun run lint
273
+ bun test
218
274
  ```
219
275
 
220
276
  Tests inject results/use temporary fixtures; they do not contact nodes, login or
@@ -238,7 +294,7 @@ Explicit `services` requirements retain Railway, Vercel, Cloudflare, AWS and npm
238
294
  checks. No raw account/auth output is retained. Version checks keep
239
295
  per-tool results, including the expected pinned version when available, and require
240
296
  Docker Engine/Compose/Buildx readiness. `workstation` delegates to read-only
241
- `personal workstation doctor --json --non-interactive`; provider configuration presence is not live
297
+ the checkout's `bootstrap/doctor.ts --json`; provider configuration presence is not live
242
298
  authorization. Warnings/skips fail the strict whole-tailnet health coverage gate.
243
299
 
244
300
  ## Repair planning and shared access
@@ -257,7 +313,7 @@ installation never substitutes for authentication or resource authorization.
257
313
 
258
314
  The workstation owner reads permitted shared provider keys from each machine's
259
315
  existing Vault session. For process-scoped shared secret access, use the existing
260
- `personal infra-vault run --config dev -- COMMAND` workflow on that machine with its intended
316
+ `vault` session on that machine with its intended
261
317
  project/config. Infrastructure, access policy and secret seeding remain owned by
262
318
  OpenTofu. No OAuth/session/keychain/SSH credential transfer is implemented.
263
319
 
@@ -270,7 +326,7 @@ reviewed configuration; without `--dry-run` it applies. `devfleet fleet-sync
270
326
  --revision FULL_SHA` rolls the same reviewed revision out to the enrolled fleet
271
327
  after approval. The [setup runbook](../agents/setup.md)
272
328
  covers canaries, offline/partial failures, per-node receipts and rollback. Both
273
- delegate rendering to personal workstation and installation to existing mise/bootstrap.
329
+ delegate installation to mise and `bootstrap/setup.ts`.
274
330
  They never reset the developer checkout or transfer credentials. Status delegates
275
331
  managed-profile drift evidence to the workstation owner; historical rollout receipts
276
332
  alone are not a live readiness assertion.