@crvouga/dev-fleet 0.8.0 → 0.9.1

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 (205) hide show
  1. package/README.md +65 -9
  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 +55 -9
  14. package/ansible/macos/manager.yml +154 -7
  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/monitoring/agent-darwin.yml +3 -0
  19. package/ansible/monitoring/agent.yml +2 -0
  20. package/ansible/monitoring/hub-darwin.yml +3 -0
  21. package/ansible/monitoring/hub-route-darwin.yml +1 -0
  22. package/ansible/net/remote-probe.sh +4 -1
  23. package/ansible/platforms.json +2 -2
  24. package/ansible/pyproject.toml +5 -0
  25. package/ansible/ubuntu/baseline.yml +39 -13
  26. package/ansible/ubuntu/disk-gc.py +6 -2
  27. package/ansible/ubuntu/firewall.nft.j2 +50 -0
  28. package/ansible/ubuntu/policy.json +125 -6
  29. package/ansible/ubuntu/reboot.yml +89 -0
  30. package/ansible/ubuntu/recovery.yml +13 -3
  31. package/ansible/ubuntu/t3/policy.json +3 -0
  32. package/ansible/ubuntu/t3-desktop.yml +23 -2
  33. package/ansible/ubuntu/t3-headless.yml +128 -0
  34. package/ansible/ubuntu/t3-health.sh +24 -10
  35. package/ansible/ubuntu/t3-idle.py +37 -2
  36. package/ansible/ubuntu/t3-policy.yml +4 -1
  37. package/ansible/ubuntu/t3-route.yml +1 -0
  38. package/ansible/ubuntu/tailscale.yml +54 -8
  39. package/ansible/ubuntu/tasks/browser.yml +3 -0
  40. package/ansible/ubuntu/tasks/containers-off.yml +43 -0
  41. package/ansible/ubuntu/tasks/docker.yml +53 -6
  42. package/ansible/ubuntu/tasks/dpkg.yml +4 -0
  43. package/ansible/ubuntu/tasks/features.yml +22 -0
  44. package/ansible/ubuntu/tasks/firewall.yml +172 -0
  45. package/ansible/ubuntu/tasks/install.yml +2 -1
  46. package/ansible/ubuntu/tasks/packages.yml +4 -2
  47. package/ansible/ubuntu/tasks/release.yml +6 -0
  48. package/ansible/ubuntu/tasks/resources.yml +6 -4
  49. package/ansible/ubuntu/tasks/services.yml +2 -2
  50. package/ansible/ubuntu/tasks/sizing.yml +49 -0
  51. package/ansible/ubuntu/tasks/snapshot.yml +14 -3
  52. package/ansible/ubuntu/tasks/ssh.yml +61 -4
  53. package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
  54. package/ansible/ubuntu/tasks/user.yml +1 -0
  55. package/ansible/ubuntu/worker-account.yml +117 -0
  56. package/ansible/uv.lock +265 -0
  57. package/bin/devfleet.mjs +42 -4
  58. package/bootstrap/fleet-remote-bootstrap.sh +91 -25
  59. package/bootstrap/global-config.ts +29 -3
  60. package/bootstrap/package-smoke.ts +127 -0
  61. package/bootstrap/pins.ts +189 -0
  62. package/bootstrap/release-notes.ts +88 -0
  63. package/bootstrap/release-version.ts +41 -7
  64. package/bootstrap/worker-features.ts +114 -0
  65. package/bootstrap/worker-seed.yaml +7 -3
  66. package/bootstrap/worker-tools.ts +39 -9
  67. package/cliproxy/README.md +1 -1
  68. package/cliproxy/docs/health-workflow.example.yml +20 -143
  69. package/docs/authentication.md +19 -11
  70. package/docs/consumer-setup.md +42 -27
  71. package/docs/convergence.md +67 -0
  72. package/docs/declared.md +61 -34
  73. package/docs/machine-setup.md +73 -7
  74. package/docs/manager-setup.md +10 -3
  75. package/docs/operations.md +64 -9
  76. package/docs/releasing.md +49 -1
  77. package/docs/runbooks.md +98 -7
  78. package/docs/state.md +23 -11
  79. package/docs/support-matrix.md +40 -4
  80. package/docs/topology.md +67 -6
  81. package/docs/ubuntu-sync.md +78 -28
  82. package/docs/worker-contract.md +122 -19
  83. package/docs/worker-requirements.md +107 -0
  84. package/examples/fleet.json +1 -0
  85. package/examples/workers-from-infrastructure/main.tf +132 -0
  86. package/examples/workers-from-infrastructure/worker-records.json +14 -0
  87. package/llms.txt +3 -1
  88. package/mise.lock +29 -0
  89. package/mise.toml +5 -3
  90. package/package.json +2 -2
  91. package/schema/check-ids.json +1967 -170
  92. package/schema/check-ids.schema.json +34 -0
  93. package/schema/fleet.schema.json +327 -8
  94. package/schema/output/check.schema.json +1 -1
  95. package/schema/output/fleet-state-definition.schema.json +14 -0
  96. package/schema/output/fleet-sync-schedule.schema.json +26 -0
  97. package/schema/output/fleet-sync.schema.json +18 -0
  98. package/schema/output/report.schema.json +3 -2
  99. package/schema/worker-record.schema.json +76 -0
  100. package/src/agents/converge.ts +140 -24
  101. package/src/agents/files.ts +2 -32
  102. package/src/agents/native-verify.ts +34 -3
  103. package/src/agents/secrets.ts +39 -4
  104. package/src/check-owners.ts +61 -2
  105. package/src/check-registry.ts +630 -0
  106. package/src/cli/agent-guide.ts +3 -3
  107. package/src/cli/cli.ts +4 -0
  108. package/src/cli/fleet-manifest.ts +9 -3
  109. package/src/cli/fleet-node-lifecycle.ts +313 -64
  110. package/src/cli/fleet-node-park.ts +238 -0
  111. package/src/cli/fleet-node-seed.ts +51 -35
  112. package/src/cli/fleet-state.ts +2 -1
  113. package/src/cli/fleet-sync-checkout.ts +7 -0
  114. package/src/cli/fleet-sync-enrollment.ts +193 -32
  115. package/src/cli/fleet-sync-forward.ts +4 -0
  116. package/src/cli/fleet-sync-manager.ts +103 -33
  117. package/src/cli/fleet-sync-output.ts +31 -11
  118. package/src/cli/fleet-sync-plan.ts +65 -9
  119. package/src/cli/fleet-sync-records.ts +113 -0
  120. package/src/cli/fleet-sync-remote.ts +151 -9
  121. package/src/cli/fleet-sync-schedule.ts +14 -4
  122. package/src/cli/fleet-sync-state.ts +28 -16
  123. package/src/cli/fleet-sync-vault.ts +77 -0
  124. package/src/cli/fleet-sync-worker.ts +214 -0
  125. package/src/cli/fleet-sync.ts +150 -19
  126. package/src/cli/fleet-validate.ts +59 -21
  127. package/src/cli/fleet-watch.ts +7 -1
  128. package/src/cli/fleet.ts +87 -5
  129. package/src/cli/managed-health.ts +25 -1
  130. package/src/cli/register-fleet.ts +115 -4
  131. package/src/cli/registry.ts +14 -0
  132. package/src/cli/tailnet.ts +43 -10
  133. package/src/cli/ui-data.ts +3 -1
  134. package/src/cli/worker-usage-report.ts +63 -0
  135. package/src/clock.ts +42 -0
  136. package/src/definition.ts +332 -9
  137. package/src/diagnostics.ts +5 -2
  138. package/src/exposure.ts +102 -0
  139. package/src/fleet-config.ts +3 -1
  140. package/src/git-identity.ts +76 -3
  141. package/src/ide/ide.ts +5 -0
  142. package/src/ide/t3.ts +12 -2
  143. package/src/input-hashes.ts +6 -0
  144. package/src/machine/machine-class.ts +59 -0
  145. package/src/machine/platform.ts +90 -11
  146. package/src/machine/state.ts +37 -15
  147. package/src/machine/support-matrix.ts +11 -9
  148. package/src/machine-health.ts +28 -5
  149. package/src/manager-health.ts +148 -2
  150. package/src/manager-sync.ts +65 -4
  151. package/src/monitoring/beszel.ts +8 -8
  152. package/src/monitoring/enroll.ts +8 -1
  153. package/src/monitoring/hub-access.ts +10 -4
  154. package/src/monitoring/systems.ts +138 -0
  155. package/src/network-definition.ts +13 -4
  156. package/src/node-converge-linux.ts +136 -27
  157. package/src/node-converge.ts +13 -3
  158. package/src/platform/ansible-receipt.ts +17 -2
  159. package/src/platform/ansible.ts +40 -4
  160. package/src/platform/manager-host.ts +25 -8
  161. package/src/platform/resource-verify.ts +18 -7
  162. package/src/platform/t3-policy.ts +24 -10
  163. package/src/platform/t3-service.ts +109 -34
  164. package/src/platform/ubuntu-release.ts +24 -0
  165. package/src/platform/ubuntu-verify.ts +31 -3
  166. package/src/platform/ubuntu-worker.ts +58 -0
  167. package/src/platform/worker-platform.ts +72 -0
  168. package/src/protocol.ts +1 -1
  169. package/src/public-reachability.ts +186 -0
  170. package/src/remote-bootstrap.ts +50 -12
  171. package/src/remote-operation.ts +18 -4
  172. package/src/repair-plan.ts +23 -12
  173. package/src/report.ts +2 -1
  174. package/src/schedule-check.ts +21 -2
  175. package/src/secrets.ts +4 -0
  176. package/src/service-auth.ts +55 -48
  177. package/src/ssh-connection.ts +141 -0
  178. package/src/ssh.ts +63 -24
  179. package/src/state/store.ts +4 -2
  180. package/src/state/sync.ts +35 -6
  181. package/src/state/workers.ts +140 -0
  182. package/src/t3/pairing.ts +59 -0
  183. package/src/t3/peer-link.ts +333 -0
  184. package/src/t3/version.ts +1 -1
  185. package/src/tailnet-cleanup.ts +185 -0
  186. package/src/tailnet-control.ts +346 -0
  187. package/src/tailscale.ts +1 -1
  188. package/src/toolchain-checks.ts +11 -1
  189. package/src/types.ts +4 -2
  190. package/src/worker-account.ts +63 -0
  191. package/src/worker-activity.ts +303 -0
  192. package/src/worker-capability-checks.ts +55 -16
  193. package/src/worker-capacity.ts +31 -0
  194. package/src/worker-exposure.ts +388 -0
  195. package/src/worker-features.ts +201 -0
  196. package/src/worker-host-checks.ts +71 -20
  197. package/src/worker-manifest.ts +27 -4
  198. package/src/worker-parking.ts +70 -0
  199. package/src/worker-policy.ts +17 -1
  200. package/src/worker-readiness.ts +15 -3
  201. package/src/worker-reboot.ts +129 -0
  202. package/src/worker-records.ts +305 -0
  203. package/src/worker-seed.ts +16 -11
  204. package/src/worker-sizing.ts +76 -0
  205. package/src/cli/schedule-logs.ts +0 -36
@@ -10,7 +10,8 @@ import { fleetState, type FleetStateOptions } from './fleet-state';
10
10
  import { fleetAccounts, type FleetAccountsOptions } from './fleet-accounts';
11
11
  import { fleetReport, type FleetReportOptions } from './fleet-report';
12
12
  import { fleetWatch, watchServices } from './fleet-watch';
13
- import type { NodeReport } from '../types';
13
+ import type { Check, NodeReport } from '../types';
14
+ import { fleet } from '../fleet-config';
14
15
  import { registerGithubVerify } from './github-verify';
15
16
  import { fleetRestore, type FleetRestoreOptions } from './fleet-restore';
16
17
  import { fleetValidate, type FleetValidateOptions } from './fleet-validate';
@@ -30,6 +31,12 @@ import {
30
31
  lifecycleServices,
31
32
  type NodeLifecycleOptions,
32
33
  } from './fleet-node-lifecycle';
34
+ import {
35
+ fleetNodePark,
36
+ fleetNodeResume,
37
+ parkServices,
38
+ type NodeParkOptions,
39
+ } from './fleet-node-park';
33
40
 
34
41
  type Complete = (outcome: CommandOutcome) => void;
35
42
 
@@ -198,6 +205,10 @@ function registerSync({ program, defaults, complete }: Registration) {
198
205
  '--revision <sha>',
199
206
  'Converge to an exact source commit; defaults to the commit the dev-fleet package you run was published from'
200
207
  )
208
+ .option(
209
+ '--rollback',
210
+ 'Converge to the revision applied before the current one (fleet state); the revision, tools and T3 go back, system packages roll forward only'
211
+ )
201
212
  .option(
202
213
  '--node <id>',
203
214
  'Explicit enrolled remote target; repeatable',
@@ -208,6 +219,10 @@ function registerSync({ program, defaults, complete }: Registration) {
208
219
  '--vault-password-login',
209
220
  'Opt-in: log a worker without a token of its own in with the shared Vault user, on its own terminal'
210
221
  )
222
+ .option(
223
+ '--restart-busy',
224
+ 'Restart a worker T3 for a new release or policy even while agent turns run, which ends them; by default the restart waits until T3 is idle'
225
+ )
211
226
  .option(
212
227
  '--canary <id>',
213
228
  'The worker that proves a new revision first when it is reachable; defaults to canary in fleet.json, then the first worker'
@@ -225,6 +240,14 @@ function registerSync({ program, defaults, complete }: Registration) {
225
240
  'Accepted for compatibility; fleet-sync applies by default (schedule writes still need --apply --yes)'
226
241
  )
227
242
  .option('--yes', 'Accepted for compatibility with --apply')
243
+ .option(
244
+ '--maintenance-reboot',
245
+ 'Reboot each worker whose installed updates wait for a reboot: one at a time, canary first, only while it runs no agent turn, verified ready before the next'
246
+ )
247
+ .option(
248
+ '--force',
249
+ 'With --maintenance-reboot: reboot a worker even while agent turns run (they end)'
250
+ )
228
251
  .option(
229
252
  '--take-over',
230
253
  'Make this machine the manager while another one still holds the manager lease'
@@ -309,11 +332,11 @@ function registerState({ program, complete }: Registration) {
309
332
  .option('--yes', 'With --restore: discard changes the bucket does not hold')
310
333
  .option(
311
334
  '--timestamp <time>',
312
- 'With --restore: the version the bucket held at this time, uploaded again as the current one (a damaged replica)'
335
+ 'With --restore: the version the bucket held at this time, uploaded again as the current one (a damaged replica); refused while another manager holds the manager lease, unless with --take-over'
313
336
  )
314
337
  .option(
315
338
  '--take-over',
316
- 'With --init or --import: change the fleet state while another manager still holds the manager lease'
339
+ 'With --init, --import or --restore --timestamp: change the fleet state while another manager still holds the manager lease'
317
340
  )
318
341
  .addHelpText(
319
342
  'after',
@@ -361,10 +384,52 @@ function registerWatch({ program, defaults, complete }: Registration) {
361
384
  const data = report.data as { nodes?: NodeReport[] };
362
385
  return data.nodes ?? [];
363
386
  };
387
+ const autoPark = async (reports: NodeReport[]): Promise<Check[]> => {
388
+ const checks: Check[] = [];
389
+ for (const report of reports) {
390
+ if (
391
+ !report.checks.some(
392
+ (check) => check.id === 'worker:idle' && check.status === 'warn'
393
+ )
394
+ )
395
+ continue;
396
+ const worker = fleet.workers.find(
397
+ (entry) =>
398
+ entry.id === report.node ||
399
+ entry.network?.tailscaleName === report.node
400
+ );
401
+ if (!worker) continue;
402
+ const parked = await fleetNodePark(
403
+ { id: worker.id },
404
+ parkServices(settings.config, () =>
405
+ Promise.reject(new Error('fleet-watch never resumes a worker'))
406
+ )
407
+ );
408
+ checks.push({
409
+ id: 'worker:auto-park',
410
+ status: parked.exitCode === 0 ? 'pass' : 'warn',
411
+ summary:
412
+ parked.exitCode === 0
413
+ ? `${worker.id} was idle longer than declared and is parked.`
414
+ : 'WORKER_AUTO_PARK_REFUSED',
415
+ ...(parked.exitCode === 0
416
+ ? {}
417
+ : {
418
+ remedy: {
419
+ action: `fleet-node-park refused ${worker.id}; run devfleet fleet-node-park --id ${worker.id} to see why.`,
420
+ },
421
+ }),
422
+ });
423
+ }
424
+ return checks;
425
+ };
364
426
  complete(
365
427
  await fleetWatch(
366
428
  { ...(options.test ? { test: true } : {}) },
367
- watchServices(status)
429
+ {
430
+ ...watchServices(status),
431
+ ...(fleet.parking?.autoPark ? { autoPark } : {}),
432
+ }
368
433
  )
369
434
  );
370
435
  });
@@ -448,6 +513,52 @@ function registerLifecycle({ program, defaults, complete }: Registration) {
448
513
  )
449
514
  .option('--yes', 'Confirm an explicitly requested --apply operation')
450
515
  .action(action('retire'));
516
+ const parking =
517
+ (name: 'park' | 'resume') =>
518
+ async (_options: unknown, command: Command) => {
519
+ const options = command.optsWithGlobals<NodeParkOptions & CliOptions>();
520
+ const settings = await withSettings(defaults, options);
521
+ const services = parkServices(settings.config, (id) =>
522
+ fleetSync(
523
+ {
524
+ node: [id],
525
+ apply: true,
526
+ yes: true,
527
+ nonInteractive: true,
528
+ ...(options.json ? { json: true } : {}),
529
+ },
530
+ {
531
+ ...defaults,
532
+ ...(options.config ? { config: options.config } : {}),
533
+ }
534
+ )
535
+ );
536
+ complete(
537
+ name === 'park'
538
+ ? await fleetNodePark(options, services)
539
+ : await fleetNodeResume(options, services)
540
+ );
541
+ };
542
+ program
543
+ .command('fleet-node-park')
544
+ .description(
545
+ 'Park a worker that is stopped or destroyed on purpose: it is no longer contacted, never the canary and does not fail the fleet'
546
+ )
547
+ .requiredOption('--id <name>', 'The declared worker to park')
548
+ .option(
549
+ '--force',
550
+ 'Park although agent turns are under way on it, or cannot be counted'
551
+ )
552
+ .option('--dry-run', 'Say what would change; change nothing')
553
+ .action(parking('park'));
554
+ program
555
+ .command('fleet-node-resume')
556
+ .description(
557
+ 'Bring a parked worker back: run the declared start command, then fleet-sync that worker and verify it'
558
+ )
559
+ .requiredOption('--id <name>', 'The declared worker to resume')
560
+ .option('--dry-run', 'Say what would change; change nothing')
561
+ .action(parking('resume'));
451
562
  program
452
563
  .command('fleet-node-seed')
453
564
  .description(
@@ -207,6 +207,20 @@ export const COMMANDS: CommandSpec[] = [
207
207
  ['seed', 'cloud-init', 'autoinstall', 'new', 'bootstrap', 'provision'],
208
208
  true,
209
209
  ],
210
+ [
211
+ 'fleet-node-park',
212
+ 'Park a worker that is off on purpose',
213
+ 'Record in the fleet state that a declared worker is stopped or destroyed on purpose (a rented machine nobody uses): fleet-sync and fleet-status list it as parked with the time since, never contact it or make it the canary, and it does not change the exit code. Refuses while agent turns run on it, unless --force. Runs the stop command fleet.json declares under parking.stop with the worker ID. --dry-run previews.',
214
+ ['park', 'stop', 'pause', 'idle', 'cost', 'rented', 'shutdown'],
215
+ true,
216
+ ],
217
+ [
218
+ 'fleet-node-resume',
219
+ 'Resume a parked worker',
220
+ 'Run the start command fleet.json declares under parking.start with the worker ID, mark the worker active in the fleet state, then run fleet-sync --node ID, which converges and verifies it. The exit code is the sync’s. --dry-run previews.',
221
+ ['resume', 'start', 'unpark', 'wake', 'rented'],
222
+ true,
223
+ ],
210
224
  [
211
225
  'fleet-node-retire',
212
226
  'Retire a worker',
@@ -5,13 +5,19 @@ import {
5
5
  appendManagerLinks,
6
6
  appendDashboardEvidence,
7
7
  } from './managed-health';
8
- import { discoverTailnet } from '../tailscale';
8
+ import { discoverTailnet, type Tailnet } from '../tailscale';
9
+ import type { FleetNode, NodeReport } from '../types';
10
+ import { appendPublicReachability } from '../public-reachability';
9
11
  import { tailnetHealth } from '../tailnet-health';
10
12
  import { makeReport, renderReport, reportExitCode } from '../report';
11
13
  import { readRoster } from './roster';
12
14
  import { publicHubUrl } from '../monitoring/beszel';
13
15
  import { stableUrls } from './fleet-sync-output';
14
16
  import { timeout } from './options';
17
+ import { appendUsage } from './worker-usage-report';
18
+ import { appendTailnetLeftovers } from '../tailnet-cleanup';
19
+ import { fleet } from '../fleet-config';
20
+ import { withUsage } from '../worker-activity';
15
21
  import type { FleetServices } from './fleet';
16
22
  import type { Settings } from './settings';
17
23
  import type { CliOptions } from './options';
@@ -57,12 +63,21 @@ export async function executeTailnet(
57
63
  );
58
64
  if (options.managed)
59
65
  await appendManagerLinks(nodes, registered, services.run);
66
+ // From the manager, outside the tailnet: optional, it never blocks readiness.
67
+ if (options.workers) await appendPublicReachability(nodes, services.run);
60
68
  // The hub runs on the manager, which worker scope deliberately filters out of
61
69
  // `tailnet`; its visibility comes from the unfiltered discovery.
62
70
  const dashboard = options.workers
63
- ? await appendDashboardEvidence(nodes, discovered)
71
+ ? await workerFleetEvidence(nodes, discovered, registered, services)
64
72
  : null;
65
- const report = makeReport('health', nodes);
73
+ // Parked workers, and what each active one is doing (uptime, agent turns).
74
+ const usage = options.managed
75
+ ? await appendUsage(nodes, registered, {
76
+ run: services.run,
77
+ computers: discovered.computers,
78
+ })
79
+ : null;
80
+ const report = { ...makeReport('health', nodes), ...(usage ?? {}) };
66
81
  const exitCode = reportExitCode(report, true);
67
82
  const verified = exitCode === 0;
68
83
  return {
@@ -77,16 +92,34 @@ export async function executeTailnet(
77
92
  inspected: nodes.filter((node) => node.snapshot).length,
78
93
  },
79
94
  },
80
- text: options.workers
81
- ? workerHealthText(
82
- { ...report, ok: verified },
83
- coverage,
84
- publicDashboard(dashboard)
85
- )
86
- : `${stableUrls(renderReport({ ...report, ok: verified }))}\nCoverage: ${coverage.computers} visible computers; ${coverage.excludedMobile} mobile devices excluded.\n${coverage.limitation}`,
95
+ text: withUsage(
96
+ options.workers
97
+ ? workerHealthText(
98
+ { ...report, ok: verified },
99
+ coverage,
100
+ publicDashboard(dashboard)
101
+ )
102
+ : `${stableUrls(renderReport({ ...report, ok: verified }))}\nCoverage: ${coverage.computers} visible computers; ${coverage.excludedMobile} mobile devices excluded.\n${coverage.limitation}`,
103
+ usage
104
+ ),
87
105
  };
88
106
  }
89
107
 
108
+ /** What the canonical worker fleet is checked for beyond each worker: the
109
+ * dashboard's evidence, and devices tagged as workers that are no declared
110
+ * worker (#123). Returns the private hub address.
111
+ */
112
+ async function workerFleetEvidence(
113
+ nodes: NodeReport[],
114
+ discovered: Tailnet,
115
+ registered: FleetNode[],
116
+ services: FleetServices
117
+ ): Promise<string | null> {
118
+ const dashboard = await appendDashboardEvidence(nodes, discovered);
119
+ await appendTailnetLeftovers(nodes, fleet, services.tailnet, registered);
120
+ return dashboard;
121
+ }
122
+
90
123
  /** Shown to people: the stable public name, never the tailnet address. */
91
124
  function publicDashboard(privateHub: string | null): string | null {
92
125
  return privateHub ? publicHubUrl() : null;
@@ -86,7 +86,9 @@ function nodeChecks(value: unknown): CheckRow[] {
86
86
  }
87
87
 
88
88
  export function isStatus(value: unknown): value is string {
89
- return ['pass', 'warn', 'fail', 'skip'].includes(String(value));
89
+ return ['pass', 'warn', 'fail', 'skip', 'not-applicable'].includes(
90
+ String(value)
91
+ );
90
92
  }
91
93
 
92
94
  export function isCancellation(error: unknown): boolean {
@@ -0,0 +1,63 @@
1
+ import { fleet } from '../fleet-config';
2
+ import { readParked } from '../state/workers';
3
+ import type { TailnetComputer } from '../tailscale';
4
+ import type { FleetNode, NodeReport, Runner } from '../types';
5
+ import { fleetUsage, usageSummary, type WorkerUsage } from '../worker-activity';
6
+ import { parkedCheck, parkedOnline } from '../worker-parking';
7
+
8
+ /** The workers fleet-status contacts: every one that is not parked. */
9
+ export function activeNodes(nodes: FleetNode[]): FleetNode[] {
10
+ const parked = readParked();
11
+ return nodes.filter((node) => !parked[node.id]);
12
+ }
13
+
14
+ function reportOf(reports: NodeReport[], id: string): NodeReport {
15
+ const worker = fleet.workers.find((entry) => entry.id === id);
16
+ const found = reports.find(
17
+ (entry) =>
18
+ entry.node === id ||
19
+ (worker?.network && entry.node === worker.network.tailscaleName)
20
+ );
21
+ if (found) return found;
22
+ const added: NodeReport = { node: id, checks: [] };
23
+ reports.push(added);
24
+ return added;
25
+ }
26
+
27
+ /** Adds to fleet-status what it says about each worker beyond its health:
28
+ * parked workers as parked (and a warning when one is online anyway), and
29
+ * for every active worker it reaches its uptime, agent turns, last agent
30
+ * activity and `worker:idle`. Returns the `workers` of the JSON output and
31
+ * its `usage` summary.
32
+ */
33
+ export async function appendUsage(
34
+ reports: NodeReport[],
35
+ nodes: FleetNode[],
36
+ {
37
+ run,
38
+ computers = [],
39
+ now = new Date(),
40
+ }: { run: Runner; computers?: TailnetComputer[]; now?: Date }
41
+ ): Promise<{ workers: WorkerUsage[]; usage: ReturnType<typeof usageSummary> }> {
42
+ const parked = readParked();
43
+ for (const worker of fleet.workers) {
44
+ const at = parked[worker.id];
45
+ if (!at) continue;
46
+ const report = reportOf(reports, worker.id);
47
+ report.checks.push(parkedCheck(worker.id, at, now));
48
+ const online = parkedOnline(
49
+ worker.id,
50
+ worker.network?.tailscaleName,
51
+ computers
52
+ );
53
+ if (online) report.checks.push(online);
54
+ }
55
+ const { workers, checks } = await fleetUsage(fleet, {
56
+ nodes: nodes.filter((node) => !parked[node.id]),
57
+ parked,
58
+ run,
59
+ now,
60
+ });
61
+ for (const [id, check] of checks) reportOf(reports, id).checks.push(check);
62
+ return { workers, usage: usageSummary(workers) };
63
+ }
package/src/clock.ts CHANGED
@@ -53,6 +53,48 @@ export function clockCheck(
53
53
  };
54
54
  }
55
55
 
56
+ /** The offset of a worker's clock from the manager's, measured by the
57
+ * manager around one SSH round trip: the worker read its clock between
58
+ * `sentAt` and `receivedAt` (milliseconds on the manager's clock), so the
59
+ * offset is only known to lie in that window. The check judges the smallest
60
+ * offset the window allows, so a slow link never reads as a skewed clock.
61
+ */
62
+ export function clockOffsetCheck(
63
+ remoteSeconds: number,
64
+ sentAt: number,
65
+ receivedAt: number
66
+ ): Check {
67
+ const remote = remoteSeconds * 1000;
68
+ const least =
69
+ remote > receivedAt
70
+ ? (remote - receivedAt) / 1000
71
+ : remote < sentAt
72
+ ? (sentAt - remote) / 1000
73
+ : 0;
74
+ const direction = remote > receivedAt ? 'ahead of' : 'behind';
75
+ const measured = `${least.toFixed(1)} s ${direction} the manager`;
76
+ const remedy = clockRemedy(undefined);
77
+ if (least > failOffsetSeconds)
78
+ return {
79
+ id: 'worker:clock-offset',
80
+ status: 'fail',
81
+ summary: `CLOCK_SKEWED ${measured}`,
82
+ remedy,
83
+ };
84
+ if (least > warnOffsetSeconds)
85
+ return {
86
+ id: 'worker:clock-offset',
87
+ status: 'warn',
88
+ summary: `CLOCK_DRIFTING ${measured}`,
89
+ remedy,
90
+ };
91
+ return {
92
+ id: 'worker:clock-offset',
93
+ status: 'pass',
94
+ summary: `The clock is within ${warnOffsetSeconds} s of the manager's.`,
95
+ };
96
+ }
97
+
56
98
  function clockRemedy(timeService: number | undefined): { action: string } {
57
99
  return {
58
100
  action: