@crvouga/dev-fleet 0.3.0 → 0.5.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 (97) hide show
  1. package/README.md +25 -17
  2. package/ansible/github/git.yml +66 -0
  3. package/ansible/macos/policy.json +12 -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/platforms.json +111 -0
  9. package/ansible/ubuntu/authorize-sudo.sh +7 -16
  10. package/ansible/ubuntu/baseline.yml +6 -28
  11. package/ansible/ubuntu/disk-gc.py +211 -7
  12. package/ansible/ubuntu/disk-guard.py +24 -5
  13. package/ansible/ubuntu/policy.json +68 -4
  14. package/ansible/ubuntu/t3-desktop.yml +2 -2
  15. package/ansible/ubuntu/tailscale.yml +20 -9
  16. package/ansible/ubuntu/tasks/browser.yml +56 -0
  17. package/ansible/ubuntu/tasks/docker.yml +52 -0
  18. package/ansible/ubuntu/tasks/dpkg.yml +46 -0
  19. package/ansible/ubuntu/tasks/install.yml +38 -0
  20. package/ansible/ubuntu/tasks/packages.yml +4 -55
  21. package/ansible/ubuntu/tasks/release.yml +25 -0
  22. package/ansible/ubuntu/tasks/resources.yml +7 -6
  23. package/ansible/ubuntu/tasks/services.yml +13 -0
  24. package/ansible/ubuntu/tasks/snapshot.yml +50 -0
  25. package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
  26. package/bootstrap/doctor.ts +32 -6
  27. package/bootstrap/fleet-remote-bootstrap.sh +49 -45
  28. package/bootstrap/setup.ts +23 -8
  29. package/bootstrap/worker-tools.ts +118 -0
  30. package/cliproxy/README.md +1 -1
  31. package/cliproxy/compose.yaml +4 -0
  32. package/cliproxy/scripts/checkpoint.py +1 -1
  33. package/docs/authentication.md +90 -0
  34. package/docs/consumer-setup.md +32 -30
  35. package/docs/declared.md +6 -5
  36. package/docs/machine-setup.md +22 -12
  37. package/docs/operations.md +87 -31
  38. package/docs/runbooks.md +377 -0
  39. package/docs/state.md +13 -7
  40. package/docs/support-matrix.md +36 -0
  41. package/docs/ubuntu-sync.md +16 -13
  42. package/docs/worker-contract.md +28 -1
  43. package/examples/fleet.json +19 -1
  44. package/llms.txt +2 -0
  45. package/mise.toml +7 -0
  46. package/package.json +2 -2
  47. package/schema/check-ids.json +526 -0
  48. package/schema/fleet.schema.json +185 -4
  49. package/schema/output/check.schema.json +23 -0
  50. package/schema/output/error.schema.json +14 -0
  51. package/schema/output/fleet-report.schema.json +79 -0
  52. package/schema/output/fleet-sync.schema.json +112 -0
  53. package/schema/output/report.schema.json +38 -0
  54. package/src/auth-checks.ts +3 -0
  55. package/src/cli/agent-guide.ts +5 -3
  56. package/src/cli/fleet-report.ts +126 -0
  57. package/src/cli/fleet-sync-manager.ts +11 -0
  58. package/src/cli/fleet-sync-plan.ts +200 -0
  59. package/src/cli/fleet-sync-remote.ts +4 -2
  60. package/src/cli/fleet-sync-schedule.ts +142 -35
  61. package/src/cli/fleet-sync.ts +90 -8
  62. package/src/cli/fleet-watch.ts +198 -0
  63. package/src/cli/fleet.ts +4 -0
  64. package/src/cli/register-fleet.ts +51 -0
  65. package/src/cli/registry.ts +13 -0
  66. package/src/cli/schedule-logs.ts +36 -0
  67. package/src/clock.ts +63 -0
  68. package/src/definition.ts +160 -12
  69. package/src/git-identity.ts +160 -0
  70. package/src/machine/platform.ts +48 -28
  71. package/src/machine/support-matrix.ts +76 -0
  72. package/src/machine-health.ts +51 -4
  73. package/src/manager-health.ts +205 -0
  74. package/src/manager-sync.ts +27 -9
  75. package/src/monitoring/hub-backup.ts +63 -17
  76. package/src/monitoring/manager-hub.ts +44 -2
  77. package/src/network-definition.ts +1 -1
  78. package/src/node-converge-linux.ts +16 -5
  79. package/src/node-converge.ts +1 -1
  80. package/src/notify.ts +239 -0
  81. package/src/platform/debian-version.ts +62 -0
  82. package/src/platform/ubuntu-audit.ts +11 -4
  83. package/src/platform/ubuntu-release.ts +41 -0
  84. package/src/remote-bootstrap.ts +89 -25
  85. package/src/remote-operation.ts +213 -0
  86. package/src/schedule-check.ts +40 -0
  87. package/src/secrets.ts +26 -8
  88. package/src/ssh.ts +10 -2
  89. package/src/state/history.ts +172 -0
  90. package/src/state/store.ts +11 -4
  91. package/src/tailscale.ts +51 -0
  92. package/src/toolchain-checks.ts +8 -3
  93. package/src/worker-capability-checks.ts +167 -0
  94. package/src/worker-capacity.ts +19 -7
  95. package/src/worker-host-checks.ts +68 -10
  96. package/src/worker-policy.ts +33 -0
  97. package/src/worker-readiness.ts +10 -1
@@ -1,3 +1,7 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
1
5
  /** A worker installs only this declared subset; every version still comes from mise.lock. */
2
6
  export function workerTools<T>(
3
7
  tools: Record<string, T>,
@@ -22,3 +26,117 @@ export function workerTools<T>(
22
26
  selected.map((key: string) => [key, tools[key]!])
23
27
  ) as Record<string, T>;
24
28
  }
29
+
30
+ /** Named tool sets a fleet adds to its workers on top of the base set
31
+ * (`worker_tools`). Each is `worker_profile_<name>` under [vars] in
32
+ * mise.toml; every version still comes from mise.lock.
33
+ */
34
+ export const TOOL_PROFILES = ['web', 'go', 'rust', 'jvm'] as const;
35
+ export type ToolProfile = (typeof TOOL_PROFILES)[number];
36
+
37
+ /** What a fleet configuration adds to one worker's base tools: profiles and
38
+ * single tools, each already pinned in mise.toml.
39
+ */
40
+ export interface ToolSelection {
41
+ profiles: string[];
42
+ tools: string[];
43
+ }
44
+ export const NO_SELECTION: ToolSelection = { profiles: [], tools: [] };
45
+
46
+ const list = (value: unknown): string[] =>
47
+ Array.isArray(value)
48
+ ? value.filter((item): item is string => typeof item === 'string')
49
+ : [];
50
+ const record = (value: unknown): value is Record<string, unknown> =>
51
+ typeof value === 'object' && value !== null && !Array.isArray(value);
52
+
53
+ /** The selection a fleet configuration makes for one worker: the fleet-wide
54
+ * `worker.toolProfiles` and `worker.tools`, plus that worker's own
55
+ * `toolProfiles`. Read from the plain document, so the bootstrap needs no
56
+ * fleet code.
57
+ */
58
+ export function workerSelection(
59
+ definition: unknown,
60
+ node?: string
61
+ ): ToolSelection {
62
+ if (!record(definition)) return NO_SELECTION;
63
+ const policy = record(definition['worker']) ? definition['worker'] : {};
64
+ const workers: unknown[] = Array.isArray(definition['workers'])
65
+ ? definition['workers']
66
+ : [];
67
+ const own = workers
68
+ .filter(record)
69
+ .filter((entry) => node !== undefined && entry['id'] === node)
70
+ .flatMap((entry) => list(entry['toolProfiles']));
71
+ return {
72
+ profiles: [...new Set([...list(policy['toolProfiles']), ...own])],
73
+ tools: [...new Set(list(policy['tools']))],
74
+ };
75
+ }
76
+
77
+ /** The selection of the fleet configuration fleet-sync installed on this
78
+ * machine; none when there is no configuration yet.
79
+ */
80
+ export function installedSelection(
81
+ node: string | undefined,
82
+ home: string = homedir()
83
+ ): ToolSelection {
84
+ const path = join(home, '.config/dev-fleet/fleet.json');
85
+ if (!existsSync(path)) return NO_SELECTION;
86
+ try {
87
+ return workerSelection(JSON.parse(readFileSync(path, 'utf8')), node);
88
+ } catch {
89
+ return NO_SELECTION;
90
+ }
91
+ }
92
+
93
+ /** The tools of one worker: the base set, then each selected profile's, then
94
+ * the single tools, in that order and each once. A profile mise.toml does not
95
+ * declare, or a tool it does not pin, is refused.
96
+ */
97
+ export function workerToolset<T>(
98
+ tools: Record<string, T>,
99
+ vars: Record<string, unknown>,
100
+ selection: ToolSelection = NO_SELECTION
101
+ ): Record<string, T> {
102
+ const names = (value: unknown) =>
103
+ typeof value === 'string' ? value.split(',').filter(Boolean) : [];
104
+ const base = names(vars['worker_tools']);
105
+ const added = selection.profiles.flatMap((profile) => {
106
+ const declared = vars[`worker_profile_${profile}`];
107
+ if (typeof declared !== 'string' || !declared)
108
+ throw new Error(
109
+ `Unknown worker tool profile "${profile}". Choose from ${TOOL_PROFILES.join(', ')}.`
110
+ );
111
+ return names(declared);
112
+ });
113
+ for (const tool of selection.tools)
114
+ if (!Object.hasOwn(tools, tool))
115
+ throw new Error(
116
+ `The worker tool "${tool}" has no pin in dev-fleet's mise.toml. Ask for it in an issue.`
117
+ );
118
+ return workerTools(tools, [
119
+ ...new Set([...base, ...added, ...selection.tools]),
120
+ ]);
121
+ }
122
+
123
+ /** `--profiles web,go --tools fd` for the doctor and the setup. */
124
+ export function selectionArguments(selection: ToolSelection): string[] {
125
+ return [
126
+ ...(selection.profiles.length
127
+ ? ['--profiles', selection.profiles.join(',')]
128
+ : []),
129
+ ...(selection.tools.length ? ['--tools', selection.tools.join(',')] : []),
130
+ ];
131
+ }
132
+
133
+ /** The selection named by `--profiles` and `--tools`. */
134
+ export function parseSelectionArguments(argv: string[]): ToolSelection {
135
+ const value = (flag: string) => {
136
+ const index = argv.indexOf(flag);
137
+ return index < 0
138
+ ? []
139
+ : (argv[index + 1] ?? '').split(',').filter((item) => item.length > 0);
140
+ };
141
+ return { profiles: value('--profiles'), tools: value('--tools') };
142
+ }
@@ -150,7 +150,7 @@ devfleet cliproxy inference messages EXACT_CLAUDE_MODEL_ID
150
150
 
151
151
  **Account routing.** Credential files are the only list of accounts. `ops declared` (run by every fleet-sync) gives each Claude, Codex and Antigravity credential the model prefix `<type>-<email>` through the proxy's management API (`scripts/accounts.py`; a credential file is never edited directly). A request for `<type>-<email>/<model>` is served only by that account, and `/v1/models` lists those ids, which is how every fleet node discovers the accounts to offer in T3. Nothing in Git names an account.
152
152
 
153
- Unprefixed routing is **fill-first** (`routing.strategy: fill-first`): one account serves until its quota is exhausted and it cools down, then the next account, with session affinity keeping a session on its account while available and bounded retries across eligible accounts for the requested model. `cliproxy apply` requires this policy and the host monitor reports drift; see [Multi-account routing](docs/model-health.md#multi-account-routing-fill-first) for applying it to a running proxy. Automatic preview-model switching is disabled. No model aliases are configured: clients use the upstream model ids exactly as `/v1/models` lists them. [`models.json`](models.json) declares the model ids that are health-checked (`claude-opus-5-5`, `claude-sonnet-5-5`, `gpt-6-astra`, `gpt-6.1-sol`), and the checks fail loudly when one is not served. Usage statistics are enabled but are not a durable billing ledger. Available quota details depend on the provider.
153
+ Unprefixed routing is **fill-first** (`routing.strategy: fill-first`): one account serves until its quota is exhausted and it cools down, then the next account, with session affinity keeping a session on its account while available and bounded retries across eligible accounts for the requested model. `cliproxy apply` requires this policy and the host monitor reports drift; see [Multi-account routing](docs/model-health.md#multi-account-routing-fill-first) for applying it to a running proxy. Automatic preview-model switching is disabled. The only model aliases are the ones `config.template.yaml` declares under `oauth.model-alias` (`ops` applies them and waits until `/v1/models` serves them); every other model id is the upstream id exactly as `/v1/models` lists it. [`models.json`](models.json) declares the model ids that are health-checked (`claude-opus-5-5`, `claude-sonnet-5-5`, `gpt-6-astra`, `gpt-6.1-sol`), and the checks fail loudly when one is not served. Usage statistics are enabled but are not a durable billing ledger. Available quota details depend on the provider.
154
154
 
155
155
  ## Fleet clients: macOS, Ubuntu and Windows/WSL
156
156
 
@@ -3,6 +3,10 @@ services:
3
3
  proxy:
4
4
  image: ${CLI_PROXY_IMAGE:?image must be pinned}
5
5
  restart: unless-stopped
6
+ # Docker restarts a proxy that exits; the container watchdog on the
7
+ # manager restarts one that stays unhealthy.
8
+ labels:
9
+ dev.devfleet.watchdog: restart
6
10
  user: '${CLIPROXY_UID:-1000}:${CLIPROXY_GID:-1000}'
7
11
  working_dir: /state
8
12
  command: ['/CLIProxyAPI/CLIProxyAPI', '-config', '/state/config.yaml']
@@ -68,7 +68,7 @@ def validate(root):
68
68
  raise ValueError("Provider cache directory missing")
69
69
  for path in (state / "runtime/auths").iterdir():
70
70
  if path.name == "kv.db" and path.is_file() and path.stat().st_size > 0:
71
- continue # Opaque upstream bbolt state, captured while the process is stopped.
71
+ continue # Opaque upstream bbolt state, copied while the proxy runs (see capture).
72
72
  # .json: provider credential; .cds: upstream per-account cooldown
73
73
  # records (JSON object, no credential), kept so limits survive moves.
74
74
  if path.suffix not in (".json", ".cds") or not path.is_file():
@@ -0,0 +1,90 @@
1
+ # Authentication and accounts
2
+
3
+ Where each credential dev-fleet uses lives, who can read it, how it reaches
4
+ each node, how it rotates and what happens when it fails. The three tables
5
+ are generated from the code; `features/rules/authentication.feature` fails
6
+ when one is out of date (`DEVFLEET_UPDATE_DOCS=1 bun test features/rules`
7
+ rewrites them).
8
+
9
+ ## Where credentials live
10
+
11
+ | Credential | Lives in | Read by |
12
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
13
+ | The fleet's secrets (table below) | The provider `secrets` names in `fleet.json` | The node that needs it, through `src/secrets.ts` |
14
+ | The bucket's two keys | The environment of the `devfleet` command; `~/.config/dev-fleet/bucket.json` (0600) on the manager after `fleet-state --init` | The manager only |
15
+ | A node's Vault token | That node's own Vault session (`~/.vault-token`) | That node only; never copied |
16
+ | A node's GitHub login | `gh`'s own store on that node | `gh`, and git through its credential helper |
17
+ | Provider accounts (Claude, Codex …) | The CLI proxy's store on the manager | The proxy; nodes hold only the proxy client key |
18
+ | Tailscale and T3 pairing | Each machine, signed in by a person | That machine |
19
+
20
+ ## How they reach each node
21
+
22
+ - **Vault.** A person logs each machine in once (`fleet-sync` asks on the
23
+ manager and passes the password to the worker over SSH on standard input).
24
+ From then on the worker's `personal-worker-auth` timer renews its own token
25
+ (`bootstrap/worker-auth.ts`). Nothing renders provider configuration from
26
+ Vault on a worker: agent profiles come from `fleet-sync`.
27
+ - **GitHub.** The same job restores the declared account (`worker.githubLogin`)
28
+ with the GitHub service token it reads from its own Vault session
29
+ (`bootstrap/worker-github.ts`, on standard input, never argv). `fleet-sync`
30
+ makes `gh` git's credential helper and writes the declared identity
31
+ (`git` in `fleet.json`, `ansible/github/git.yml`) on every node.
32
+ - **The proxy client key** is read by each node from its own secrets provider
33
+ whenever a launcher starts an agent.
34
+ - **The monitoring hub's backup storage keys** are read on the manager by
35
+ `fleet-sync` and handed to the hub's playbook in a private variables file.
36
+
37
+ ## Rotation and failure
38
+
39
+ Rotate a secret where the provider keeps it; the next read uses it, nothing
40
+ caches it. A Vault token that is not renewed expires: `worker:vault-renewal`
41
+ fails and the fix is one login of that machine. A GitHub token that stops
42
+ working fails `auth:github` and `worker:git-push-auth`; store the new one in
43
+ Vault and the worker restores itself within ten minutes. During a proxy
44
+ outage agents on workers wait: no worker is configured for native clients.
45
+ Every failure code is in [the runbooks](runbooks.md).
46
+
47
+ ## The fleet's secrets
48
+
49
+ <!-- generated: secrets -->
50
+
51
+ | Secret | Required | `env` provider variable | `file` provider file |
52
+ | ------------------------------ | ---------------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
53
+ | `proxy-client-key` | yes | `DEVFLEET_SECRET_PROXY_CLIENT_KEY` | `~/.config/dev-fleet/secrets/proxy-client-key` |
54
+ | `monitoring-account-password` | yes | `DEVFLEET_SECRET_MONITORING_ACCOUNT_PASSWORD` | `~/.config/dev-fleet/secrets/monitoring-account-password` |
55
+ | `monitoring-tunnel-token` | yes | `DEVFLEET_SECRET_MONITORING_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/monitoring-tunnel-token` |
56
+ | `proxy-tunnel-token` | yes | `DEVFLEET_SECRET_PROXY_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/proxy-tunnel-token` |
57
+ | `monitoring-backup-access-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_ACCESS_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-access-key` |
58
+ | `monitoring-backup-secret-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_SECRET_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-secret-key` |
59
+ | `notify-webhook-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_WEBHOOK_URL` | `~/.config/dev-fleet/secrets/notify-webhook-url` |
60
+ | `notify-ntfy-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_NTFY_URL` | `~/.config/dev-fleet/secrets/notify-ntfy-url` |
61
+
62
+ <!-- end: secrets -->
63
+
64
+ ## Checks of credentials
65
+
66
+ <!-- generated: checks -->
67
+
68
+ | Check | Measured in |
69
+ | ----------------------------- | ------------------------------------------ |
70
+ | `auth:vault` | `src/cli/fleet-sync-vault.ts` |
71
+ | `worker:git-identity` | `src/git-identity.ts` |
72
+ | `worker:git-push-auth` | `src/definition.ts`, `src/git-identity.ts` |
73
+ | `worker:github-principal` | `src/worker-access-checks.ts` |
74
+ | `worker:proxy-authentication` | `src/worker-access-checks.ts` |
75
+ | `worker:vault-renewal` | `src/worker-access-checks.ts` |
76
+
77
+ <!-- end: checks -->
78
+
79
+ ## Timers
80
+
81
+ <!-- generated: timers -->
82
+
83
+ | Timer | Declared in | Schedule |
84
+ | -------------------------------- | -------------------------------------- | ------------------------------------- |
85
+ | `personal-fleet-disk-gc.timer` | `ansible/ubuntu/tasks/resources.yml` | OnBootSec=10min, OnCalendar=hourly |
86
+ | `personal-worker-auth.timer` | `ansible/ubuntu/tasks/credentials.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
87
+ | `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=1min, OnUnitActiveSec=1min |
88
+ | `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=5min, OnUnitActiveSec=15min |
89
+
90
+ <!-- end: timers -->
@@ -153,35 +153,35 @@ npx devfleet fleet-state --example > fleet.json
153
153
  Then replace every placeholder. Each field, from
154
154
  [`schema/fleet.schema.json`](../schema/fleet.schema.json):
155
155
 
156
- | Field | Value |
157
- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
158
- | `version` | `3` |
159
- | `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
160
- | `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
161
- | `secrets` | See [secrets](#secrets). |
162
- | `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
163
- | `manager.os` | `"macOS"` |
164
- | `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
165
- | `manager.network.user` | The manager's login name (`id -un`). |
166
- | `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
167
- | `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
168
- | `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
169
- | `workers[].os` | `"linux"` |
170
- | `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
171
- | `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
172
- | `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
173
- | `workers[].network.t3Port` | `3773` |
174
- | `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
175
- | `services.monitoring.hubPort` | `8443` |
176
- | `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
177
- | `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
178
- | `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
179
- | `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }` (fixed). |
180
- | `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
181
- | `worker.githubLogin` | The GitHub login every worker must be signed in as. |
182
- | `worker.vaultUser` | The Vault user each worker's token is issued for. |
183
- | `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
184
- | `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
156
+ | Field | Value |
157
+ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
158
+ | `version` | `3` |
159
+ | `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
160
+ | `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
161
+ | `secrets` | See [secrets](#secrets). |
162
+ | `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
163
+ | `manager.os` | `"macOS"` |
164
+ | `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
165
+ | `manager.network.user` | The manager's login name (`id -un`). |
166
+ | `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
167
+ | `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
168
+ | `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
169
+ | `workers[].os` | `"linux"` |
170
+ | `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
171
+ | `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
172
+ | `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
173
+ | `workers[].network.t3Port` | `3773` |
174
+ | `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
175
+ | `services.monitoring.hubPort` | `8443` |
176
+ | `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
177
+ | `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
178
+ | `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
179
+ | `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }`: a release of the [support matrix](support-matrix.md). |
180
+ | `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
181
+ | `worker.githubLogin` | The GitHub login every worker must be signed in as. |
182
+ | `worker.vaultUser` | The Vault user each worker's token is issued for. |
183
+ | `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
184
+ | `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
185
185
 
186
186
  Use the same values in every place they repeat. Validate before going on:
187
187
 
@@ -352,7 +352,9 @@ Read `summary` and `remedy.action` of the failing check. The common ones:
352
352
  | `TAILSCALE_NODE_OFFLINE` | The worker is off or off the tailnet. It is listed and skipped; others continue. |
353
353
  | `TAILSCALE_DEVICE_REPLACED` | The tailnet device behind a worker changed. Nothing is written to it until the human confirms the replacement. |
354
354
  | `SUDO_AUTHORIZATION_REQUIRED` | The worker account cannot use sudo without a password. Run the first sync interactively. |
355
- | `UBUNTU_BASELINE_MISMATCH` | The worker is not Ubuntu 26.04. |
355
+ | `UBUNTU_BASELINE_MISMATCH` | The worker is not an Ubuntu release the [support matrix](support-matrix.md) lists. |
356
+ | `WORKER_ENVIRONMENT_UNSUPPORTED` | The worker is WSL2 or a container. Use a physical or virtual machine of its own. |
357
+ | `PACKAGE_NEWER_THAN_PIN` (warning) | Ubuntu's security updates moved a pinned package past its pin. It is kept; the pin is a minimum. |
356
358
  | `ROOT_DEVELOPMENT_USER_REJECTED` | The worker's SSH user is root. Use a normal account with sudo. |
357
359
  | `BOOTSTRAP_DISK_CAPACITY_REQUIRED` | Less than 2 GiB free (or too few inodes) where the fleet keeps its files on the worker. |
358
360
  | `FLEET_REMOTE_LOCKED` | Another operation is running on that worker. Wait and run again. |
package/docs/declared.md CHANGED
@@ -31,6 +31,8 @@ may only shrink.
31
31
  | Ansible on a worker | `ansible-core` in `ansible/ubuntu/policy.json` | Exact version | `ansible/ubuntu/tasks/packages.yml` | `package:ansible-core` |
32
32
  | The CLI proxy | `cliproxy/compose.yaml`, `image.lock.json`, `requirements.lock`, `launchd/`, `config.template.yaml` | Image digests; hashed Python dependencies | `devfleet cliproxy install`, and `fleet-sync` after it | The proxy's audit |
33
33
  | Agent profiles and T3 providers | `agents/registry.json`, `agents/t3.json` | The commit | `fleet-sync` | Profile receipts; the plan after every apply |
34
+ | Worker time service | `ansible/ubuntu/tasks/services.yml`, offsets in `clock` in `ansible/ubuntu/policy.json` | The commit | `fleet-sync` | `worker:clock` |
35
+ | The manager's container watchdog | `ansible/monitoring/container-watchdog.py`, `containerWatchdog` in `ansible/macos/policy.json` | The commit | `fleet-sync` (`hub-darwin.yml`) | `manager:container-watchdog` |
34
36
  | Docker Desktop on the manager | `docker_desktop_*` under `[vars]` in `mise.toml` | Version and build | You | `manager:docker` (warns) |
35
37
  | CI | `.github/workflows/` | Each action at a commit; tools from `mise.lock` | GitHub | `features/rules/` |
36
38
 
@@ -45,11 +47,10 @@ stays in the playbook: the playbook is its declaration.
45
47
  launchd job from TypeScript and has never worked from the published package.
46
48
  It should become a playbook.
47
49
  - **The rest of a worker's packages.** Only the packages in `policy.json` are
48
- exact. `systemd-oomd` and the T3 desktop libraries are installed at whatever
49
- version the machine's own archive holds, and the machine's own updates move
50
- everything else. Pinning them needs every worker on the snapshot with its
51
- own updates off, so security fixes arrive only when the snapshot moves. That
52
- is a policy choice, not a missing line.
50
+ pinned. The T3 desktop libraries are installed at whatever version the
51
+ machine's own archive holds, and Ubuntu's security updates move everything
52
+ else, pinned packages included: a pin is a minimum
53
+ ([worker contract](worker-contract.md#package-pins-and-security-updates)).
53
54
  - **What a worker needs before Ansible exists.** `fleet-remote-bootstrap.sh`
54
55
  installs Git, curl, CA certificates, Python and `ansible-core` from the
55
56
  machine's own archive (the snapshot is HTTPS, and a new machine has no CA
@@ -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.