@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.
- package/README.md +265 -37
- package/agents/setup.md +42 -12
- package/ansible/macos/manager.yml +10 -7
- package/ansible/macos/policy.json +2 -10
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/monitoring/README.md +86 -25
- package/ansible/monitoring/agent.yml +40 -2
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +5 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/baseline.yml +1 -1
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/policy.json +13 -3
- package/ansible/ubuntu/t3-desktop.yml +19 -0
- package/ansible/ubuntu/t3-health.sh +93 -22
- package/ansible/ubuntu/t3-idle.py +42 -0
- package/ansible/ubuntu/t3-policy.yml +4 -11
- package/ansible/ubuntu/tailscale.yml +8 -5
- package/ansible/ubuntu/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/resources.yml +61 -1
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/ansible/uv.lock +190 -0
- package/bootstrap/bootstrap.sh +25 -18
- package/bootstrap/doctor.ts +46 -1
- package/bootstrap/fleet-manager-run.sh +42 -15
- package/bootstrap/fleet-remote-bootstrap.sh +85 -12
- package/bootstrap/global-config.ts +64 -6
- package/bootstrap/install-launcher.sh +3 -0
- package/bootstrap/native-t3.ts +22 -11
- package/bootstrap/npm.ts +10 -2
- package/bootstrap/release-version.ts +157 -0
- package/cliproxy/README.md +3 -3
- package/cliproxy/scripts/accounts.py +216 -0
- package/cliproxy/scripts/checkpoint.py +29 -1
- package/cliproxy/scripts/cli.py +1 -1
- package/cliproxy/scripts/clientkeys.py +99 -0
- package/cliproxy/scripts/lease.py +80 -0
- package/cliproxy/scripts/ops +137 -25
- package/cliproxy/scripts/restore-verify +15 -0
- package/docs/accounts.md +154 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +91 -0
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +400 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +69 -0
- package/docs/ubuntu-sync.md +357 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +5 -0
- package/llms.txt +36 -0
- package/mise.lock +29 -0
- package/mise.toml +15 -2
- package/package.json +11 -2
- package/schema/fleet.schema.json +137 -15
- package/src/accounts/checks.ts +220 -0
- package/src/accounts/context.ts +76 -0
- package/src/accounts/propagate.ts +62 -0
- package/src/accounts/roster.ts +85 -0
- package/src/accounts/selection.ts +67 -0
- package/src/accounts/sessions.ts +120 -0
- package/src/agents/account-relay.ts +176 -26
- package/src/agents/fleet-verify.ts +9 -0
- package/src/agents/launch.ts +84 -5
- package/src/agents/native-verify.ts +293 -22
- package/src/agents/provider-errors.ts +9 -2
- package/src/agents/registry.ts +7 -1
- package/src/cli/agent-guide.ts +39 -5
- package/src/cli/app.ts +11 -4
- package/src/cli/cli.ts +7 -0
- package/src/cli/command-index.ts +7 -0
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-checkout.ts +56 -17
- package/src/cli/fleet-sync-enrollment.ts +19 -23
- package/src/cli/fleet-sync-forward.ts +1 -1
- package/src/cli/fleet-sync-manager.ts +84 -12
- package/src/cli/fleet-sync-monitoring.ts +5 -5
- package/src/cli/fleet-sync-output.ts +7 -3
- package/src/cli/fleet-sync-remote.ts +31 -18
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync.ts +159 -57
- package/src/cli/fleet.ts +4 -2
- package/src/cli/managed-health.ts +4 -4
- package/src/cli/node-sync.ts +7 -0
- package/src/cli/program.ts +20 -4
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +86 -1
- package/src/cli/registry.ts +41 -1
- package/src/cli/roster.ts +46 -30
- package/src/cli/settings.ts +15 -12
- package/src/cli/source-operations.ts +27 -3
- package/src/cli/tailnet.ts +2 -2
- package/src/definition.ts +116 -4
- package/src/diagnostics.ts +1 -1
- package/src/fleet-config.ts +105 -19
- package/src/ide/definition.ts +86 -0
- package/src/ide/ide.ts +43 -0
- package/src/ide/index.ts +5 -0
- package/src/ide/orchestrate.ts +50 -0
- package/src/ide/t3.ts +111 -0
- package/src/machine/source.ts +3 -3
- package/src/machine/state.ts +15 -27
- package/src/manager-sync.ts +9 -18
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +15 -3
- package/src/monitoring/beszel.ts +11 -4
- package/src/monitoring/enroll.ts +2 -1
- package/src/monitoring/hub-access.ts +4 -3
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +8 -8
- package/src/monitoring/hub.ts +3 -3
- package/src/monitoring/manager-hub.ts +30 -14
- package/src/node-converge-linux.ts +30 -7
- package/src/package-info.ts +45 -0
- package/src/platform/ansible.ts +19 -15
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- package/src/platform/resource-verify.ts +8 -4
- package/src/platform/t3-policy.ts +33 -6
- package/src/platform/t3-service.ts +104 -0
- package/src/platform/types.ts +0 -4
- package/src/platform/ubuntu-audit.ts +5 -2
- package/src/platform/windows.ts +0 -14
- package/src/remote-bootstrap.ts +17 -19
- package/src/remote-entry.ts +15 -1
- package/src/repair-catalog.ts +1 -1
- package/src/runner.ts +1 -1
- package/src/service-auth.ts +15 -5
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/t3/accounts.ts +7 -1
- package/src/t3/models.ts +133 -0
- package/src/t3/pairing.ts +31 -0
- package/src/t3/profile.ts +203 -0
- package/src/t3/version.ts +119 -0
- package/src/t3-endpoint.ts +64 -8
- package/src/t3-linked-check.ts +24 -2
- package/src/toolchain-checks.ts +25 -0
- package/src/transport.ts +3 -2
- package/src/types.ts +4 -0
- package/src/worker-access-checks.ts +56 -3
- package/src/worker-host-checks.ts +41 -13
- package/src/workstation-checks.ts +1 -1
- package/src/index.ts +0 -6
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
+
```sh
|
|
164
|
+
npx devfleet fleet-state # where the state is, and how it stands with the bucket
|
|
165
|
+
```
|
|
63
166
|
|
|
64
|
-
The
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
npx
|
|
171
|
+
npm ci
|
|
172
|
+
npx devfleet fleet-sync # with the two bucket credentials in the environment
|
|
70
173
|
```
|
|
71
174
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
104
|
-
[`
|
|
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
|
-
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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
|
-
|
|
11
|
-
|
|
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:
|
|
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: '{{
|
|
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
|
-
"
|
|
8
|
-
"autorestart": "1",
|
|
9
|
-
"womp": "1",
|
|
10
|
-
"powernap": "0"
|
|
6
|
+
"womp": "1"
|
|
11
7
|
},
|
|
12
|
-
"
|
|
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
|