@crvouga/dev-fleet 0.7.0 → 0.9.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 (206) hide show
  1. package/README.md +88 -7
  2. package/agents/audit.md +18 -18
  3. package/ansible/agents/agents.yml +134 -0
  4. package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
  5. package/ansible/agents/templates/claude.settings.json.j2 +3 -0
  6. package/ansible/agents/templates/codex.config.toml.j2 +11 -0
  7. package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
  8. package/ansible/agents/templates/grok.config.toml.j2 +19 -0
  9. package/ansible/agents/templates/opencode.json.j2 +24 -0
  10. package/ansible/agents/templates/receipt.json.j2 +10 -0
  11. package/ansible/ansible.cfg +14 -0
  12. package/ansible/github/git.yml +82 -0
  13. package/ansible/macos/manager-privilege.yml +83 -0
  14. package/ansible/macos/manager.yml +164 -9
  15. package/ansible/macos/policy.json +13 -0
  16. package/ansible/macos/t3-policy.yml +5 -3
  17. package/ansible/macos/vault-renew.yml +85 -0
  18. package/ansible/macos/worker-host-key.yml +33 -0
  19. package/ansible/monitoring/agent-darwin.yml +3 -0
  20. package/ansible/monitoring/agent.yml +2 -0
  21. package/ansible/monitoring/hub-darwin.yml +3 -0
  22. package/ansible/monitoring/hub-route-darwin.yml +1 -0
  23. package/ansible/net/remote-probe.sh +4 -1
  24. package/ansible/platforms.json +2 -2
  25. package/ansible/pyproject.toml +5 -0
  26. package/ansible/ubuntu/baseline.yml +39 -13
  27. package/ansible/ubuntu/disk-gc.py +6 -2
  28. package/ansible/ubuntu/firewall.nft.j2 +50 -0
  29. package/ansible/ubuntu/policy.json +125 -6
  30. package/ansible/ubuntu/reboot.yml +89 -0
  31. package/ansible/ubuntu/recovery.yml +13 -3
  32. package/ansible/ubuntu/t3/policy.json +3 -0
  33. package/ansible/ubuntu/t3-desktop.yml +23 -2
  34. package/ansible/ubuntu/t3-headless.yml +128 -0
  35. package/ansible/ubuntu/t3-health.sh +24 -10
  36. package/ansible/ubuntu/t3-idle.py +37 -2
  37. package/ansible/ubuntu/t3-policy.yml +4 -1
  38. package/ansible/ubuntu/t3-route.yml +1 -0
  39. package/ansible/ubuntu/tailscale.yml +54 -8
  40. package/ansible/ubuntu/tasks/browser.yml +3 -0
  41. package/ansible/ubuntu/tasks/containers-off.yml +43 -0
  42. package/ansible/ubuntu/tasks/docker.yml +53 -6
  43. package/ansible/ubuntu/tasks/dpkg.yml +4 -0
  44. package/ansible/ubuntu/tasks/features.yml +22 -0
  45. package/ansible/ubuntu/tasks/firewall.yml +172 -0
  46. package/ansible/ubuntu/tasks/install.yml +2 -1
  47. package/ansible/ubuntu/tasks/packages.yml +4 -2
  48. package/ansible/ubuntu/tasks/release.yml +6 -0
  49. package/ansible/ubuntu/tasks/resources.yml +6 -4
  50. package/ansible/ubuntu/tasks/services.yml +2 -2
  51. package/ansible/ubuntu/tasks/sizing.yml +49 -0
  52. package/ansible/ubuntu/tasks/snapshot.yml +14 -3
  53. package/ansible/ubuntu/tasks/ssh.yml +61 -4
  54. package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
  55. package/ansible/ubuntu/tasks/user.yml +1 -0
  56. package/ansible/ubuntu/worker-account.yml +117 -0
  57. package/ansible/uv.lock +265 -0
  58. package/bin/devfleet.mjs +42 -4
  59. package/bootstrap/fleet-remote-bootstrap.sh +91 -25
  60. package/bootstrap/global-config.ts +29 -3
  61. package/bootstrap/package-smoke.ts +127 -0
  62. package/bootstrap/pins.ts +189 -0
  63. package/bootstrap/release-notes.ts +88 -0
  64. package/bootstrap/release-version.ts +41 -7
  65. package/bootstrap/worker-features.ts +114 -0
  66. package/bootstrap/worker-seed.yaml +65 -0
  67. package/bootstrap/worker-tools.ts +39 -9
  68. package/cliproxy/README.md +1 -1
  69. package/cliproxy/docs/health-workflow.example.yml +20 -143
  70. package/docs/authentication.md +21 -11
  71. package/docs/consumer-setup.md +74 -58
  72. package/docs/convergence.md +67 -0
  73. package/docs/declared.md +61 -34
  74. package/docs/machine-setup.md +73 -7
  75. package/docs/manager-setup.md +34 -0
  76. package/docs/operations.md +64 -9
  77. package/docs/releasing.md +49 -1
  78. package/docs/runbooks.md +112 -7
  79. package/docs/state.md +23 -11
  80. package/docs/support-matrix.md +40 -4
  81. package/docs/topology.md +82 -12
  82. package/docs/ubuntu-sync.md +98 -42
  83. package/docs/worker-contract.md +122 -19
  84. package/docs/worker-requirements.md +107 -0
  85. package/examples/fleet.json +1 -0
  86. package/examples/workers-from-infrastructure/main.tf +132 -0
  87. package/examples/workers-from-infrastructure/worker-records.json +14 -0
  88. package/llms.txt +3 -1
  89. package/mise.lock +29 -0
  90. package/mise.toml +5 -3
  91. package/package.json +2 -2
  92. package/schema/check-ids.json +1991 -162
  93. package/schema/check-ids.schema.json +34 -0
  94. package/schema/fleet.schema.json +349 -10
  95. package/schema/output/check.schema.json +1 -1
  96. package/schema/output/fleet-state-definition.schema.json +14 -0
  97. package/schema/output/fleet-sync-schedule.schema.json +26 -0
  98. package/schema/output/fleet-sync.schema.json +18 -0
  99. package/schema/output/report.schema.json +3 -2
  100. package/schema/worker-record.schema.json +76 -0
  101. package/src/agents/converge.ts +140 -24
  102. package/src/agents/files.ts +2 -32
  103. package/src/agents/native-verify.ts +34 -3
  104. package/src/agents/secrets.ts +39 -4
  105. package/src/check-owners.ts +62 -2
  106. package/src/check-registry.ts +630 -0
  107. package/src/cli/agent-guide.ts +3 -3
  108. package/src/cli/cli.ts +4 -0
  109. package/src/cli/fleet-manifest.ts +108 -0
  110. package/src/cli/fleet-node-lifecycle.ts +704 -0
  111. package/src/cli/fleet-node-park.ts +238 -0
  112. package/src/cli/fleet-node-seed.ts +167 -0
  113. package/src/cli/fleet-state.ts +2 -1
  114. package/src/cli/fleet-sync-checkout.ts +7 -0
  115. package/src/cli/fleet-sync-enrollment.ts +193 -32
  116. package/src/cli/fleet-sync-forward.ts +6 -0
  117. package/src/cli/fleet-sync-manager.ts +123 -8
  118. package/src/cli/fleet-sync-output.ts +31 -11
  119. package/src/cli/fleet-sync-plan.ts +65 -9
  120. package/src/cli/fleet-sync-records.ts +113 -0
  121. package/src/cli/fleet-sync-remote.ts +172 -15
  122. package/src/cli/fleet-sync-schedule.ts +14 -4
  123. package/src/cli/fleet-sync-state.ts +31 -10
  124. package/src/cli/fleet-sync-vault.ts +152 -26
  125. package/src/cli/fleet-sync-worker.ts +214 -0
  126. package/src/cli/fleet-sync.ts +172 -21
  127. package/src/cli/fleet-validate.ts +277 -0
  128. package/src/cli/fleet-watch.ts +7 -1
  129. package/src/cli/fleet.ts +87 -5
  130. package/src/cli/managed-health.ts +25 -1
  131. package/src/cli/register-fleet.ts +251 -4
  132. package/src/cli/registry.ts +47 -0
  133. package/src/cli/tailnet.ts +43 -10
  134. package/src/cli/ui-data.ts +3 -1
  135. package/src/cli/worker-usage-report.ts +63 -0
  136. package/src/clock.ts +42 -0
  137. package/src/definition.ts +762 -232
  138. package/src/diagnostics.ts +5 -2
  139. package/src/exposure.ts +102 -0
  140. package/src/fleet-config.ts +3 -1
  141. package/src/git-identity.ts +76 -3
  142. package/src/ide/ide.ts +5 -0
  143. package/src/ide/t3.ts +12 -2
  144. package/src/input-hashes.ts +6 -0
  145. package/src/machine/machine-class.ts +59 -0
  146. package/src/machine/platform.ts +90 -11
  147. package/src/machine/state.ts +70 -16
  148. package/src/machine/support-matrix.ts +11 -9
  149. package/src/machine-health.ts +28 -5
  150. package/src/manager-health.ts +148 -2
  151. package/src/manager-sync.ts +89 -8
  152. package/src/monitoring/beszel.ts +8 -8
  153. package/src/monitoring/enroll.ts +8 -1
  154. package/src/monitoring/hub-access.ts +10 -4
  155. package/src/monitoring/systems.ts +138 -0
  156. package/src/network-definition.ts +13 -4
  157. package/src/node-converge-linux.ts +136 -27
  158. package/src/node-converge.ts +13 -3
  159. package/src/platform/ansible-receipt.ts +17 -2
  160. package/src/platform/ansible.ts +40 -4
  161. package/src/platform/manager-host.ts +25 -8
  162. package/src/platform/resource-verify.ts +18 -7
  163. package/src/platform/t3-policy.ts +24 -10
  164. package/src/platform/t3-service.ts +109 -34
  165. package/src/platform/ubuntu-release.ts +24 -0
  166. package/src/platform/ubuntu-verify.ts +31 -3
  167. package/src/platform/ubuntu-worker.ts +58 -0
  168. package/src/platform/worker-platform.ts +72 -0
  169. package/src/protocol.ts +1 -1
  170. package/src/public-reachability.ts +186 -0
  171. package/src/remote-bootstrap.ts +50 -12
  172. package/src/remote-operation.ts +18 -4
  173. package/src/repair-plan.ts +23 -12
  174. package/src/report.ts +2 -1
  175. package/src/schedule-check.ts +21 -2
  176. package/src/secrets.ts +7 -0
  177. package/src/service-auth.ts +55 -48
  178. package/src/ssh-connection.ts +141 -0
  179. package/src/ssh.ts +63 -24
  180. package/src/state/store.ts +4 -2
  181. package/src/state/sync.ts +35 -6
  182. package/src/state/workers.ts +140 -0
  183. package/src/t3/pairing.ts +59 -0
  184. package/src/t3/peer-link.ts +333 -0
  185. package/src/t3/version.ts +1 -1
  186. package/src/tailnet-cleanup.ts +185 -0
  187. package/src/tailnet-control.ts +346 -0
  188. package/src/tailscale.ts +1 -1
  189. package/src/toolchain-checks.ts +11 -1
  190. package/src/types.ts +4 -2
  191. package/src/worker-account.ts +63 -0
  192. package/src/worker-activity.ts +303 -0
  193. package/src/worker-capacity.ts +31 -0
  194. package/src/worker-credentials.ts +201 -0
  195. package/src/worker-exposure.ts +388 -0
  196. package/src/worker-features.ts +201 -0
  197. package/src/worker-host-checks.ts +71 -20
  198. package/src/worker-manifest.ts +224 -0
  199. package/src/worker-parking.ts +70 -0
  200. package/src/worker-policy.ts +38 -36
  201. package/src/worker-readiness.ts +15 -3
  202. package/src/worker-reboot.ts +129 -0
  203. package/src/worker-records.ts +305 -0
  204. package/src/worker-seed.ts +143 -0
  205. package/src/worker-sizing.ts +76 -0
  206. package/src/cli/schedule-logs.ts +0 -36
@@ -0,0 +1,630 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, relative, resolve } from 'node:path';
3
+ import agents from '../agents/registry.json';
4
+ import ubuntu from '../ansible/ubuntu/policy.json';
5
+ import { NATIVE_AGENTS } from './agents/native-verify';
6
+ import { CHECK_OWNERS } from './check-owners';
7
+ import { CAPABILITIES } from './config';
8
+ import { FINGERPRINT_FILES } from './input-hashes';
9
+ import { HEALTH_CHECK_IDS } from './machine-health';
10
+ import { NOTIFY_TARGETS } from './notify';
11
+ import { packagePins, ubuntuRelease } from './platform/ubuntu-release';
12
+ import { AUTOMATED_REPAIRS } from './repair-catalog';
13
+ import { checkRepair } from './repair-plan';
14
+ import { PROXY_ROUTED, SERVICE_IDS } from './service-auth';
15
+ import { DOCUMENTS } from './state/store';
16
+
17
+ /** schema/check-ids.json is generated from this module:
18
+ * `bun src/check-registry.ts > schema/check-ids.json`.
19
+ * features/rules/contracts.feature fails when the committed file differs.
20
+ */
21
+ export const PACKAGE_ROOT = resolve(import.meta.dir, '..');
22
+
23
+ // ── Where the code writes a check ID ─────────────────────────────────────
24
+
25
+ /** The `id` of a check (`id: 'ssh:auth'`, `id: given ?? 'ssh:auth'`), a
26
+ * variable named `id` a check takes (`const id = 'ssh:auth'`), or the first
27
+ * argument of a function that builds one (`notApplicable('worker:docker-run',
28
+ * …)`, `attempt('ssh:auth', …)`). A name filled in at run time is a `*`.
29
+ */
30
+ const LITERAL = String.raw`'([^'\n]*)'|"([^"\n]*)"|\x60([^\x60\n]*)\x60`;
31
+ const SITE = new RegExp(
32
+ String.raw`(\bid: (?:[^,\n]*\?\? )?|\b(?:const|let) id = |\b[A-Za-z_$][\w$]*\(\s*)(?:${LITERAL})`,
33
+ 'g'
34
+ );
35
+ /** A shell script prints a check as JSON: `{"id":"bootstrap:%s",…}`. */
36
+ const SHELL_SITE = /()"id":"([^"\n]*)"()()/g;
37
+ const NAMESPACED = /^[a-z0-9-]+(?::[A-Za-z0-9.*_-]+)+$/;
38
+ /** Strings with a colon that name something other than a check. */
39
+ const NOT_CHECKS = new Set(['node', 'personal']);
40
+
41
+ /** Where checks are made. The owners of the checks, the repair recipes and
42
+ * this registry name them; they make none. */
43
+ const SOURCES = ['src', 'bootstrap', 'agents'];
44
+ const NOT_MADE_HERE = new Set([
45
+ 'src/check-owners.ts',
46
+ 'src/repair-catalog.ts',
47
+ 'src/check-registry.ts',
48
+ ]);
49
+
50
+ function sources(root: string): string[] {
51
+ const found: string[] = [];
52
+ const walk = (directory: string): void => {
53
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
54
+ const path = join(directory, entry.name);
55
+ if (entry.isDirectory()) walk(path);
56
+ else if (/\.(ts|sh)$/.test(entry.name))
57
+ found.push(relative(root, path).split('\\').join('/'));
58
+ }
59
+ };
60
+ for (const directory of SOURCES) walk(join(root, directory));
61
+ return found.filter((path) => !NOT_MADE_HERE.has(path)).sort();
62
+ }
63
+
64
+ /** A check without a namespace is one an owner names exactly. */
65
+ const unnamespaced = (): Set<string> =>
66
+ new Set(
67
+ CHECK_OWNERS.map((owner) => owner.pattern).filter(
68
+ (pattern) => !/[:*]/.test(pattern)
69
+ )
70
+ );
71
+
72
+ /** The check IDs one source writes, as written: a `*` for a name filled in
73
+ * at run time. */
74
+ export function checkIdsIn(text: string, shell = false): string[] {
75
+ const plain = unnamespaced();
76
+ const found = new Set<string>();
77
+ for (const match of text.matchAll(shell ? SHELL_SITE : SITE)) {
78
+ const id = (match[2] ?? match[3] ?? match[4]!).replace(
79
+ /\$\{[^}]*\}+|%s/g,
80
+ '*'
81
+ );
82
+ if (plain.has(id)) found.add(id);
83
+ else if (NAMESPACED.test(id) && !NOT_CHECKS.has(id.split(':')[0]!))
84
+ found.add(id);
85
+ }
86
+ return [...found].sort();
87
+ }
88
+
89
+ /** Every check ID the code writes. */
90
+ export function writtenCheckIds(root = PACKAGE_ROOT): string[] {
91
+ return [
92
+ ...new Set(
93
+ sources(root).flatMap((path) =>
94
+ checkIdsIn(readFileSync(join(root, path), 'utf8'), path.endsWith('.sh'))
95
+ )
96
+ ),
97
+ ].sort();
98
+ }
99
+
100
+ // ── Names the code fills in from a list it holds ─────────────────────────
101
+
102
+ const BOOTSTRAP = 'bootstrap/fleet-remote-bootstrap.sh';
103
+
104
+ /** The phases of the remote bootstrap, each reported as `bootstrap:<phase>`
105
+ * when the bootstrap stops in it. */
106
+ function bootstrapPhases(root: string): string[] {
107
+ return [
108
+ ...readFileSync(join(root, BOOTSTRAP), 'utf8').matchAll(
109
+ /^phase='?([a-z0-9-]+)'?$/gm
110
+ ),
111
+ ].map((match) => match[1]!);
112
+ }
113
+
114
+ const pinnedPackages = (): string[] => [
115
+ ...new Set([
116
+ ...ubuntu.releases.flatMap((entry) =>
117
+ Object.keys(packagePins(ubuntuRelease(entry.release)))
118
+ ),
119
+ 'tailscale',
120
+ ]),
121
+ ];
122
+
123
+ /** A `*` the code fills in from a list it holds is that list. A `*` filled
124
+ * in from the fleet configuration, a machine or a tool's own output (a node,
125
+ * a tool, an account) stays a `*`. */
126
+ function expansions(root: string): Record<string, string[]> {
127
+ const providers = Object.keys(agents.proxy.routes);
128
+ const natives = NATIVE_AGENTS.map((agent) => agent.name);
129
+ return {
130
+ 'auth:*': [...CAPABILITIES, ...SERVICE_IDS, ...Object.keys(PROXY_ROUTED)],
131
+ 'agent:*': Object.keys(agents.tools),
132
+ 'agent:*:version': natives,
133
+ 'agent:*:session': natives,
134
+ 'bootstrap:*': bootstrapPhases(root),
135
+ 'notify:*': Object.keys(NOTIFY_TARGETS),
136
+ 'provider:*:authentication': providers,
137
+ 'provider:*:inference': providers,
138
+ 'repair:*': AUTOMATED_REPAIRS,
139
+ 'state:*': [...DOCUMENTS],
140
+ 'sync:*': FINGERPRINT_FILES,
141
+ 'worker:capacity:*': HEALTH_CHECK_IDS,
142
+ 'worker:package:*': pinnedPackages(),
143
+ 'worker:service:*': ubuntu.alwaysOnServices,
144
+ };
145
+ }
146
+
147
+ /** Every check ID the code reports, with the `*` it was written with when
148
+ * the code fills that `*` in from a list. */
149
+ export function reportedCheckIds(
150
+ root = PACKAGE_ROOT
151
+ ): { id: string; from?: string }[] {
152
+ const lists = expansions(root);
153
+ const ids = new Map<string, string | undefined>();
154
+ for (const written of writtenCheckIds(root)) {
155
+ const names = lists[written];
156
+ if (!names) {
157
+ if (!ids.has(written)) ids.set(written, undefined);
158
+ continue;
159
+ }
160
+ for (const name of names) {
161
+ const id = written.replace('*', name);
162
+ if (!ids.has(id)) ids.set(id, written);
163
+ }
164
+ }
165
+ return [...ids]
166
+ .map(([id, from]) => (from ? { id, from } : { id }))
167
+ .sort((a, b) => (a.id < b.id ? -1 : 1));
168
+ }
169
+
170
+ // ── What each check means, and whether it can block readiness ───────────
171
+
172
+ /** What a passing check means. A `*` entry is the meaning of every ID filled
173
+ * in from it that has none of its own. */
174
+ export const CHECK_MEANINGS: Record<string, string> = {
175
+ 'accounts:native-login':
176
+ 'This machine holds no provider login of its own outside the proxy.',
177
+ 'accounts:roster-current':
178
+ "This machine offers exactly the accounts of the proxy's current account roster.",
179
+ 'accounts:verification':
180
+ 'The provider accounts this machine offers were verified; fails when verification could not run.',
181
+ 'agent:*':
182
+ 'A coding agent CLI is installed at its pinned version on this machine.',
183
+ 'agent:*:*:*':
184
+ 'A coding agent launches with its fleet profile on this machine.',
185
+ 'agent:*:account-*:session':
186
+ 'An agent instance of one proxy account is signed in and usable on this machine.',
187
+ 'agent:*:removed-*:session':
188
+ 'This machine offers no agent instance for an account the proxy no longer holds.',
189
+ 'agent:*:session':
190
+ 'The shared account of a coding agent is signed in and usable.',
191
+ 'agent:*:version': 'A coding agent runs the version the fleet pins.',
192
+ 'agent:profiles': "The fleet's agent profiles are written on this machine.",
193
+ 'agent:service-environment':
194
+ 'The environment agent services start with is complete.',
195
+ 'agent:verification':
196
+ 'Every coding agent was verified; fails when verification could not run.',
197
+ 'auth:*': 'A required login or capability works in this process context.',
198
+ 'auth:copilot':
199
+ "Copilot CLI uses the node's GitHub sign-in with a token it accepts.",
200
+ 'auth:github': 'gh answers auth status in this context.',
201
+ 'auth:toolchain':
202
+ 'The pinned toolchain, dependencies and Docker could be checked.',
203
+ 'auth:vault': 'This machine has its own Vault session.',
204
+ 'auth:vault-isolation':
205
+ "A worker reads its Vault token from a document of its own, not from the fleet's shared one.",
206
+ 'auth:worker-token':
207
+ "A worker has a Vault token of its own that only it and the manager can read, or a retired worker's token is revoked.",
208
+ 'beszel:access': 'The monitoring dashboard answers privately.',
209
+ 'beszel:api':
210
+ "The monitoring hub's API answers this account for this machine.",
211
+ 'beszel:backup': "The monitoring hub's data is backed up.",
212
+ 'beszel:connection': 'The monitoring hub reports this machine connected.',
213
+ 'beszel:disk': "The monitoring hub's disk use could be verified.",
214
+ 'beszel:enrollment': 'This machine is enrolled in the monitoring hub.',
215
+ 'beszel:freshness':
216
+ 'The monitoring hub received a sample from this machine within 180 seconds.',
217
+ 'beszel:hub': 'The monitoring hub runs on the manager.',
218
+ 'beszel:hub-connection': 'The monitoring agent reaches the hub.',
219
+ 'beszel:installation':
220
+ 'The monitoring agent is installed at its pinned version.',
221
+ 'beszel:leftover':
222
+ 'The monitoring hub watches no system that matches no declared worker.',
223
+ 'beszel:lock': "Monitoring changes could take the machine's operation lock.",
224
+ 'beszel:operation': 'The monitoring operation completed.',
225
+ 'beszel:protocol':
226
+ "The machine's monitoring evidence is a valid fleet document.",
227
+ 'beszel:public-endpoint': 'The public monitoring dashboard answers.',
228
+ 'beszel:public-viewer': 'The public read-only viewer account works.',
229
+ 'beszel:registration':
230
+ 'The monitoring hub holds exactly one system record for this machine.',
231
+ 'beszel:service': 'The monitoring agent service runs.',
232
+ 'beszel:system':
233
+ "The monitoring hub's system for a replaced worker watches the new machine, or a retired worker's system is gone.",
234
+ 'beszel:version': 'The monitoring hub runs the pinned Beszel release.',
235
+ 'bootstrap:attached':
236
+ "The manager followed this machine's running operation of the same revision and reports its result.",
237
+ 'bootstrap:bootstrap':
238
+ 'The worker is a supported Ubuntu release and architecture, not a container or WSL2, reached as a non-root account, and its fleet state directory is safe.',
239
+ 'bootstrap:cleanup':
240
+ 'Revision checkouts beyond the current, the applied and the two most recent were removed.',
241
+ 'bootstrap:configuration':
242
+ 'The node converged at the revision: its configuration, services and evidence.',
243
+ 'bootstrap:configuration-install':
244
+ 'The fleet configuration was written on the worker under its operation lock.',
245
+ 'bootstrap:exit': 'The remote bootstrap exited without failure.',
246
+ 'bootstrap:first-boot':
247
+ "The machine's own first boot (cloud-init) has finished, or it has none.",
248
+ 'bootstrap:lock': "The bootstrap holds the worker's fleet operation lock.",
249
+ 'bootstrap:prerequisites':
250
+ 'The packages the bootstrap needs are installed; an interrupted package run was finished first.',
251
+ 'bootstrap:privilege':
252
+ 'The development account may use sudo without a password.',
253
+ 'bootstrap:protocol':
254
+ 'The remote bootstrap answered with a valid fleet document.',
255
+ 'bootstrap:resources':
256
+ 'The home file system has the space and inodes the bootstrap needs.',
257
+ 'bootstrap:source-transfer':
258
+ 'The source bundle arrived with the digest the manager sent.',
259
+ 'bootstrap:source-verification':
260
+ 'The fleet-owned checkout holds exactly the revision, with no change fleet-sync did not make.',
261
+ 'bootstrap:tailnet-key':
262
+ 'The single-use tailnet key was handed to the worker in a root-only file.',
263
+ 'bootstrap:toolchain': 'The pinned toolchain converged.',
264
+ 'configuration:apply':
265
+ "The node's configuration (agent profiles, launcher) was applied.",
266
+ 'configuration:operation': 'The remote configuration operation completed.',
267
+ 'definition:inventory':
268
+ 'Ansible reads the inventory the fleet definition describes.',
269
+ 'definition:playbooks':
270
+ 'Every playbook passes ansible-playbook --syntax-check.',
271
+ 'definition:schema': 'fleet.json follows every rule of the fleet definition.',
272
+ dependencies:
273
+ "The checkout's dependencies are installed as its lockfile and inputs declare.",
274
+ 'enrollment:connection':
275
+ 'The manager has an enrolled connection to this worker.',
276
+ 'enrollment:record': 'A worker whose record disappeared was retired.',
277
+ 'fleet:enrollment:*':
278
+ 'A declared worker has a verified SSH and checkout registration.',
279
+ 'fleet:probe': 'The remote probe could be prepared and completed.',
280
+ 'fleet:registration': 'The machine is registered in the roster.',
281
+ 'fleet:roster': 'A fleet roster is configured.',
282
+ 'git:clean': 'The checkout has no uncommitted changes.',
283
+ 'git:configuration':
284
+ "The declared Git identity and the GitHub credential helper are written in the node's global Git configuration.",
285
+ 'github:expiry':
286
+ 'The GitHub token does not expire within the warning window.',
287
+ 'github:git-credential': 'git asks gh for its GitHub credential.',
288
+ 'github:org-access':
289
+ 'Single sign-on has authorised the GitHub token for each declared organisation.',
290
+ 'github:permissions':
291
+ 'The GitHub token has every declared scope and can push to every declared repository.',
292
+ 'github:principal': 'The node is signed in to GitHub as its declared login.',
293
+ 'github:rate-limit': 'The GitHub rate limit left is above the warning floor.',
294
+ 'github:sync':
295
+ 'fleet-sync signed this node in to GitHub from the stored credential.',
296
+ 'github:token-current': 'The node uses the GitHub token the store holds now.',
297
+ 'github:workflow':
298
+ 'fleet-github-verify completed clone, branch, commit, push, pull request, checks and comment on the sandbox repository.',
299
+ 'manager:boot-recovery':
300
+ 'The manager comes back by itself after a power cut or reboot.',
301
+ 'manager:clock': 'The manager keeps its clock on network time.',
302
+ 'manager:container-watchdog':
303
+ 'No container on the manager was capped or restarted by the container watchdog within the last hour.',
304
+ 'manager:disk':
305
+ "The manager's data volume has the free space the macOS policy asks for.",
306
+ 'manager:docker': 'The manager runs the pinned Docker Desktop.',
307
+ 'manager:identity': 'This machine is the declared fleet manager.',
308
+ 'manager:job-logs':
309
+ "newsyslog keeps the logs of the manager's scheduled jobs under the size the macOS policy names.",
310
+ 'manager:playbook':
311
+ 'The manager playbook passed its check-mode run and was applied, or is unchanged since and its postconditions hold.',
312
+ 'manager:power': 'The manager never sleeps on AC power.',
313
+ 'manager:preflight':
314
+ 'fleet-sync could start on the manager; its summary names the reason when not.',
315
+ 'manager:release':
316
+ 'The manager runs at least the macOS release the macOS policy requires.',
317
+ 'manager:schedule':
318
+ "The manager's schedule converged the fleet within two of its intervals.",
319
+ 'manager:updates': 'No macOS update is pending on the manager.',
320
+ 'manager:vault-renewal':
321
+ 'The manager is logged in to Vault and its launch agent renews the session.',
322
+ 'manager:watch':
323
+ 'fleet-watch runs on the manager on a schedule, so the heartbeat reaches the bucket.',
324
+ 'notify:*': 'A notification target fleet.json names took the notification.',
325
+ 'notify:configured':
326
+ 'Every notification target fleet.json names has its address available as a secret.',
327
+ 'platform:environment':
328
+ 'The worker runs on a physical or virtual machine of its own, not in a container or WSL2.',
329
+ 'platform:machine-class':
330
+ 'The probe recorded the class of the worker (physical or virtual machine) before anything is installed; a virtual machine reports firmware power recovery and lid policy as not applicable.',
331
+ 'platform:unsupported': 'The machine runs a supported operating system.',
332
+ 'platform:worker-size':
333
+ 'The worker has at least the stated minimum memory, root filesystem and CPUs (sizing.minimumWorker in the Ubuntu policy), measured before anything is installed.',
334
+ 'provider:*:authentication': 'A proxy provider authenticates unattended.',
335
+ 'provider:*:inference':
336
+ 'A proxy provider answers a bounded inference round trip.',
337
+ 'provider:verification':
338
+ 'Every proxy provider was verified; fails when verification could not run.',
339
+ 'proxy:*': "A phase of the CLI proxy's reconciliation completed.",
340
+ 'proxy:account:*':
341
+ 'A provider has at least one usable account signed in on the CLI proxy.',
342
+ 'proxy:configuration': "The CLI proxy's configuration was reconciled.",
343
+ 'proxy:reachable-from': 'This machine reaches the CLI proxy, and how fast.',
344
+ 'repair:*': 'An automated repair completed.',
345
+ 'repair:inputs':
346
+ 'The checkout inputs are unchanged since the diagnostics a repair plan used.',
347
+ 'repair:operation': 'A repair could be prepared and completed.',
348
+ 'repair:recheck': 'Diagnostics completed again after a repair.',
349
+ 'repair:workspace': 'A complete checkout exists for repairs.',
350
+ 'restore:applied':
351
+ 'A replacement manager holds the fleet state from the bucket.',
352
+ 'restore:bucket':
353
+ 'The bucket is named in fleet.json, with credentials for it.',
354
+ 'restore:configuration': "The project's fleet.json is found and valid.",
355
+ 'restore:container-engine':
356
+ 'The container engine answers on the replacement manager.',
357
+ 'restore:git': 'Git is installed on the replacement manager.',
358
+ 'restore:host-name':
359
+ 'The replacement manager has the host name fleet.json gives the manager.',
360
+ 'restore:state': 'The bucket answers and holds the fleet state.',
361
+ 'restore:tailscale': 'The replacement manager is signed in to the tailnet.',
362
+ revision: "The computer's checkout is at the manager's commit.",
363
+ 'rollout:evidence': "A worker's rollout produced verification evidence.",
364
+ 'rollout:next':
365
+ 'Names the command that converges a worker after it was replaced.',
366
+ 'rollout:node':
367
+ 'The fleet-sync of one worker that fleet-node-resume runs ended ready.',
368
+ 'rollout:operation': 'The remote rollout operation completed.',
369
+ 'rollout:prerequisites': "A worker met the rollout's prerequisites.",
370
+ 'rollout:promotion':
371
+ 'The canary proved the revision, so promotion to this worker went ahead.',
372
+ 'source:bootstrap':
373
+ 'The approved bootstrap source is available on the manager.',
374
+ 'ssh:agent': 'The SSH agent exposes at least one identity.',
375
+ 'ssh:auth': 'SSH key or agent authentication succeeded.',
376
+ 'ssh:disconnected': 'The SSH session was not lost while an operation ran.',
377
+ 'ssh:dns': 'The SSH host name resolved.',
378
+ 'ssh:fresh-session': 'A fresh, host-key-verified SSH session succeeded.',
379
+ 'ssh:host-key': "The worker's SSH host key is known and unchanged.",
380
+ 'ssh:missing': 'The local SSH client could be started.',
381
+ 'ssh:network': 'The SSH endpoint is reachable.',
382
+ 'ssh:probe': 'A remote probe over SSH succeeded.',
383
+ 'ssh:protocol': 'The remote answer is a supported fleet document.',
384
+ 'ssh:runtime':
385
+ 'The remote workspace and runtime exist for non-login SSH sessions.',
386
+ 'ssh:session': 'A noninteractive SSH session and probe succeeded.',
387
+ 'ssh:tailscale-check':
388
+ 'Tailscale SSH did not require interactive reauthentication.',
389
+ 'ssh:timeout': 'SSH and the remote probe finished within their deadline.',
390
+ 'state:*':
391
+ 'A document of the fleet state (definition, roster, enrollment) is stored.',
392
+ 'state:bucket': 'A bucket for the fleet state is configured and reachable.',
393
+ 'state:replica': "The bucket holds this manager's fleet state.",
394
+ 'state:retired': 'The fleet state records that a retired worker was retired.',
395
+ 'sync:*':
396
+ "An input of the computer's checkout (a lockfile, a tool pin, a manifest) matches the manager's.",
397
+ 't3:api': 'The local T3 API answers.',
398
+ 't3:end-to-end': 'T3 relay and peer access, which need an operator test.',
399
+ 't3:link':
400
+ "The manager's T3 link to a replaced or retired worker's old environment is gone, and the manager's T3 is paired with a replacement's new environment.",
401
+ 't3:manager-endpoint':
402
+ "The manager reaches the worker's private T3 endpoint.",
403
+ 't3:manager-origin': "The manager's T3 origin is configured.",
404
+ 't3:models':
405
+ 'T3 offers only the declared models for every account instance the fleet manages.',
406
+ 't3:paired-access': "The manager's T3 is linked to this worker.",
407
+ 't3:policy-loaded': 'The running T3 loaded the managed policy.',
408
+ 't3:policy-staged': 'The managed T3 policy is installed.',
409
+ 't3:prerequisites': 'The T3 baseline prerequisites are present.',
410
+ 't3:private-route': "T3's private HTTPS route is served.",
411
+ 't3:profile': 'The declared T3 settings are in place on this machine.',
412
+ 't3:provider-names': "T3 lists the fleet's provider names.",
413
+ 't3:providers': "T3's provider instances are the declared ones.",
414
+ 't3:running-version':
415
+ 'The running T3 server is the installed release; a restart the release needs waits for running agent turns.',
416
+ 't3:service': 'The pinned T3 app runs.',
417
+ 't3:settings': 'T3 settings are readable.',
418
+ 't3:updates': "T3's own updater is off, as the fleet's policy declares.",
419
+ 't3:version': 'This machine runs the T3 version the fleet pins.',
420
+ 'tailscale:configuration': 'Tailscale is configured as declared.',
421
+ 'tailscale:device':
422
+ "A replaced or retired worker's old tailnet device was removed.",
423
+ 'tailscale:discovery': 'Tailscale computers could be discovered.',
424
+ 'tailscale:enrolled-identity':
425
+ 'The Tailscale identity matches the enrollment.',
426
+ 'tailscale:enrolment-key':
427
+ "A worker that is not on the tailnet yet was handed a single-use, pre-authorised, tagged key minted by the fleet's control plane.",
428
+ 'tailscale:identity': "A machine's Tailscale identity is unambiguous.",
429
+ 'tailscale:key-expiry':
430
+ "The machine's Tailscale node key does not expire, or not within the warning window.",
431
+ 'tailscale:key-expiry-policy':
432
+ 'The control plane disabled node-key expiry for an enrolled worker whose key had one.',
433
+ 'tailscale:leftover':
434
+ "Every device on the tailnet that carries the fleet's worker tag is a declared worker (parked ones included) or a device the roster binds; warns, naming the device, for one that is not.",
435
+ 'tailscale:membership': 'A registered machine is in the visible tailnet.',
436
+ 'tailscale:online': 'Tailscale is online on this machine.',
437
+ 'tailscale:reachability': 'The machine is online in the tailnet.',
438
+ 'tool:*':
439
+ 'A pinned tool, or the global mise configuration (tool:global-mise-config), is as the revision declares.',
440
+ 'tool:docker-cli':
441
+ 'Not applicable on a worker whose containers feature is off: the Docker CLI is not one of its tools.',
442
+ 'ubuntu:baseline': 'The Ubuntu baseline policy is applied.',
443
+ 'ubuntu:boot-policy': 'The no-sleep and session boot policy is active.',
444
+ 'ubuntu:lid-policy':
445
+ 'Not applicable on a virtual machine, which has no lid; on a physical machine the lid policy is part of ubuntu:boot-policy.',
446
+ 'ubuntu:prerequisites': 'The Ubuntu prerequisites are installed.',
447
+ 'ubuntu:release': 'The worker runs the supported Ubuntu release.',
448
+ 'ubuntu:resource-safety':
449
+ 'The OOM killer, disk guard and resource caps are active.',
450
+ 'vscode:forwarding':
451
+ 'VS Code Remote-SSH forwarding, which needs an operator test.',
452
+ 'watch:heartbeat':
453
+ "fleet-watch stored its heartbeat and each machine's health in the bucket.",
454
+ 'worker:account':
455
+ "The declared worker account was created over a root login, with the manager's key and the fleet's sudo rule.",
456
+ 'worker:agent-sessions':
457
+ 'How many agent sessions this worker runs, and how many its memory policy supports at the estimated size of a session; warns when more run.',
458
+ 'worker:auto-park':
459
+ 'fleet-watch parked a worker idle longer than declared (only with parking.autoPark).',
460
+ 'worker:browser':
461
+ 'A headless browser starts on a worker whose tool profiles include web.',
462
+ 'worker:capacity': 'CPU, memory and disk evidence is available.',
463
+ 'worker:capacity:*':
464
+ 'A worker meets one capacity floor (load, memory, disk, inodes).',
465
+ 'worker:clock': 'The worker keeps its clock on network time.',
466
+ 'worker:clock-offset':
467
+ "The worker's clock is within the offset the Ubuntu policy allows of the manager's.",
468
+ 'worker:credential-renewal': 'Worker credential renewal timer is active.',
469
+ 'worker:desired-source': 'The worker runs the declared revision.',
470
+ 'worker:docker-run':
471
+ 'A container starts and exits on a worker whose containers feature is on.',
472
+ 'worker:exposure':
473
+ 'Nothing listens on the worker beyond loopback and the tailnet but what the declared exposure policy allows, and the fleet firewall that closes the rest is loaded; not applicable without an address outside the tailnet.',
474
+ 'worker:features':
475
+ 'The worker has the features fleet.json declares, and nothing dev-fleet installed for a feature that is off (containers) remains.',
476
+ 'worker:git-identity':
477
+ "The worker's global Git identity is the one fleet.json declares.",
478
+ 'worker:git-push-auth':
479
+ 'The worker can push to the declared repositories with the fleet credentials.',
480
+ 'worker:git-signing':
481
+ 'The worker signs commits as fleet.json declares, or leaves signing off.',
482
+ 'worker:github-sync':
483
+ 'The worker is signed in to GitHub from the stored credential as the declared login.',
484
+ 'worker:idle':
485
+ "An active worker runs an agent turn, or has run one within fleet.json's parking.idleAfterHours; warns once it has been idle longer.",
486
+ 'worker:manifest':
487
+ 'A worker is made of what the declared pins and a fresh build of the same revision are made of.',
488
+ 'worker:package:*':
489
+ 'The pinned Ubuntu package is installed at or past its pin.',
490
+ 'worker:park-hook':
491
+ 'The stop or start command fleet.json declares under parking ran for the worker.',
492
+ 'worker:parked':
493
+ 'A worker parked on purpose: listed with the time since, not contacted and never the canary (not-applicable). As a command result, the worker was parked or resumed.',
494
+ 'worker:parked-online': 'A parked worker is not online on the tailnet.',
495
+ 'worker:power-loss-recovery': 'The worker powers on after AC loss.',
496
+ 'worker:proxy-authentication':
497
+ 'The worker authenticates to the shared proxy.',
498
+ 'worker:proxy-singleton':
499
+ 'The worker is a proxy client, not a second gateway.',
500
+ 'worker:public-reachability':
501
+ "From the manager, the worker's SSH, T3 and monitoring ports accept no connection on its public address; optional, and not applicable when Tailscale knows no public address for it.",
502
+ 'worker:readiness': 'The worker readiness checks could run.',
503
+ 'worker:reboot':
504
+ 'The worker was rebooted while it ran no agent turn (after its first converge, or for maintenance), answered again and was converged once more.',
505
+ 'worker:reboot-pending': 'The worker needs no reboot to finish an update.',
506
+ 'worker:service:*':
507
+ 'A service every worker runs is active, enabled for boot and restarts unconditionally.',
508
+ 'worker:service-recovery': 'Worker services recover after failure.',
509
+ 'worker:sudo': 'Designated worker user has noninteractive sudo.',
510
+ 'worker:t3-health':
511
+ 'T3 local API answers with its environment identity within 10 seconds.',
512
+ 'worker:t3-recovery':
513
+ "The worker's T3 service is active, enabled for boot and restarts after a failure.",
514
+ 'worker:ubuntu': 'Required Ubuntu release and architecture.',
515
+ 'worker:vault-renewal': 'The worker renews its Vault identity unattended.',
516
+ 'workstation:configuration': 'The devfleet launcher is installed and linked.',
517
+ 'workstation:installation': 'The devfleet launcher could be installed.',
518
+ // An agent CLI that signs in through the proxy has no login to check.
519
+ ...Object.fromEntries(
520
+ Object.keys(PROXY_ROUTED).map((id) => [
521
+ `auth:${id}`,
522
+ `Not applicable: ${id} has no sign-in of its own on this machine; its summary names the check that proves its access instead.`,
523
+ ])
524
+ ),
525
+ };
526
+
527
+ /** Checks that never block readiness: the code reports them only as a
528
+ * warning, or with `required: false`. Every other check is required: its
529
+ * failure blocks readiness unless the run says the check is out of scope. */
530
+ export const ADVISORY_CHECKS = [
531
+ 'beszel:leftover',
532
+ 'github:rate-limit',
533
+ 'manager:docker',
534
+ 'manager:job-logs',
535
+ 'manager:updates',
536
+ 't3:models',
537
+ 't3:updates',
538
+ 'tailscale:device',
539
+ 'tailscale:leftover',
540
+ 'worker:auto-park',
541
+ 'worker:idle',
542
+ 'worker:parked-online',
543
+ 'worker:public-reachability',
544
+ ...Object.keys(PROXY_ROUTED).map((id) => `auth:${id}`),
545
+ ];
546
+
547
+ /** Checks that enforce a requirement of docs/worker-requirements.md; that
548
+ * page names each, and features/rules/worker-requirements.feature fails when
549
+ * one has no row there. */
550
+ export const WORKER_REQUIREMENTS = [
551
+ 'bootstrap:privilege',
552
+ 'platform:environment',
553
+ 'platform:worker-size',
554
+ 'proxy:reachable-from',
555
+ 'ssh:auth',
556
+ 'ssh:host-key',
557
+ 'tailscale:configuration',
558
+ 'ubuntu:release',
559
+ 'worker:account',
560
+ 'worker:exposure',
561
+ 'worker:github-sync',
562
+ 'worker:public-reachability',
563
+ 'worker:reboot',
564
+ ];
565
+
566
+ // ── The registry ─────────────────────────────────────────────────────────
567
+
568
+ export type Severity = 'required' | 'advisory';
569
+ export type Repair = ReturnType<typeof checkRepair>;
570
+ export interface RegisteredCheck {
571
+ id: string;
572
+ meaning: string;
573
+ severity: Severity;
574
+ repair: Repair;
575
+ requirement?: true;
576
+ }
577
+
578
+ export interface CheckRegistry {
579
+ checks: RegisteredCheck[];
580
+ /** IDs the code reports that have no meaning. */
581
+ missing: string[];
582
+ /** Meanings no reported ID uses. */
583
+ unused: string[];
584
+ }
585
+
586
+ export function checkRegistry(root = PACKAGE_ROOT): CheckRegistry {
587
+ const used = new Set<string>();
588
+ const missing: string[] = [];
589
+ const checks = reportedCheckIds(root).map(({ id, from }): RegisteredCheck => {
590
+ const key =
591
+ id in CHECK_MEANINGS ? id : from && from in CHECK_MEANINGS ? from : null;
592
+ if (key) used.add(key);
593
+ else missing.push(id);
594
+ return {
595
+ id,
596
+ meaning: key ? CHECK_MEANINGS[key]! : '',
597
+ severity: ADVISORY_CHECKS.includes(id) ? 'advisory' : 'required',
598
+ repair: checkRepair(id),
599
+ ...(WORKER_REQUIREMENTS.includes(id)
600
+ ? { requirement: true as const }
601
+ : {}),
602
+ };
603
+ });
604
+ return {
605
+ checks,
606
+ missing,
607
+ unused: Object.keys(CHECK_MEANINGS).filter((key) => !used.has(key)),
608
+ };
609
+ }
610
+
611
+ /** schema/check-ids.json. */
612
+ export function renderCheckRegistry(root = PACKAGE_ROOT): string {
613
+ const { checks, missing, unused } = checkRegistry(root);
614
+ if (missing.length || unused.length)
615
+ throw new Error(
616
+ `Check IDs without a meaning: ${missing.join(', ') || 'none'}; meanings no check uses: ${unused.join(', ') || 'none'}.`
617
+ );
618
+ return `${JSON.stringify(
619
+ {
620
+ $comment:
621
+ 'Generated by `bun src/check-registry.ts > schema/check-ids.json` from the checks the code reports; features/rules/contracts.feature fails when this file differs. Every check ID dev-fleet reports: what a passing check means; severity, required (a failure blocks readiness unless the run marks the check out of scope) or advisory (reported as a warning or as not required, never blocking); and repair, the fleet-repair recipe that repairs it or the reason only a person can (manual); requirement, a requirement docs/worker-requirements.md lists. A * stands for a name taken from the fleet configuration, a machine or a tool at run time.',
622
+ version: 1,
623
+ checks,
624
+ },
625
+ null,
626
+ 2
627
+ )}\n`;
628
+ }
629
+
630
+ if (import.meta.main) process.stdout.write(renderCheckRegistry());
@@ -91,10 +91,10 @@ export function agentGuide(release: Release) {
91
91
  errors:
92
92
  'schema/output/error.schema.json: {version:1,ok:false,code,error}; raw subprocess output and exception details are withheld',
93
93
  reports:
94
- 'Each output has a JSON schema in this package; parse by it and by its version. Diagnostic reports (fleet-status, fleet-state, fleet-repair): schema/output/report.schema.json, version 1. fleet-sync: schema/output/fleet-sync.schema.json, version 3 (a --dry-run adds plan). fleet-report: schema/output/fleet-report.schema.json, version 1.',
94
+ 'These outputs have a JSON schema in this package; parse by it and by its version. Diagnostic reports of fleet-status, fleet-state and fleet-repair (whose report names its command repair): schema/output/report.schema.json, version 1. fleet-sync: schema/output/fleet-sync.schema.json, version 3 (a --dry-run adds plan). fleet-sync --schedule: schema/output/fleet-sync-schedule.schema.json, version 2. fleet-state --example and --export: schema/output/fleet-state-definition.schema.json, version 1. fleet-report: schema/output/fleet-report.schema.json, version 1. Any other --json document carries its version, and command and ok where it reports an outcome, but has no published schema yet.',
95
95
  checks:
96
- 'schema/check-ids.json lists every check ID with what a passing check means; * stands for a name. Each failing check carries its own remedy.',
97
- statuses: ['pass', 'warn', 'fail', 'skip'],
96
+ 'schema/check-ids.json (schema/check-ids.schema.json), generated from the code, lists every check ID: meaning, what a passing check means; severity, required (a failure blocks readiness unless the run marks the check out of scope) or advisory (a warning or not required, never blocking); repair, {recipe} the fleet-repair recipe that repairs it or {manual} why only a person can. A * stands for a name taken at run time (a node, a tool, an account). Each failing check also carries its own remedy.',
97
+ statuses: ['pass', 'warn', 'fail', 'skip', 'not-applicable'],
98
98
  coverage:
99
99
  'Default fleet-status tests the manager and all configured nodes. --local tests only this manager; --tailnet and --workers cover Tailscale computers. A missing roster fails explicitly. T3/VS Code end-to-end checks remain manual.',
100
100
  },
package/src/cli/cli.ts CHANGED
@@ -4,8 +4,12 @@ import { useProjectConfig } from './project';
4
4
  import { actAsOperator } from '../state/operator';
5
5
  import { databasePath } from '../state/paths';
6
6
  import { readDocument } from '../state/store';
7
+ import { readPinsWith } from '../ssh-connection';
8
+ import { readPins } from '../state/workers';
7
9
 
8
10
  actAsOperator((root) => readDocument(databasePath(root), 'definition'));
11
+ // Host keys pinned in the fleet state reach the fleet-owned known_hosts.
12
+ readPinsWith(() => readPins());
9
13
  useProjectConfig();
10
14
 
11
15
  try {