@crvouga/dev-fleet 0.0.0-stage → 0.2.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 (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,231 @@
1
+ import { compareFingerprints } from './fingerprint';
2
+ import { probe } from './probe';
3
+ import { inspectRemote } from './transport';
4
+ import { mapBounded } from './report';
5
+ import { matchComputers } from './tailnet-matching';
6
+ import type { Tailnet, TailnetComputer } from './tailscale';
7
+ import type {
8
+ Capability,
9
+ Fingerprint,
10
+ Check,
11
+ FleetNode,
12
+ NodeReport,
13
+ Runner,
14
+ Snapshot,
15
+ } from './types';
16
+
17
+ export const HEALTH_CAPABILITIES: Capability[] = [
18
+ 't3',
19
+ 'toolchain',
20
+ 'workstation',
21
+ ];
22
+ export interface TailnetHealthOptions {
23
+ includeManagerReport?: boolean;
24
+ reference?: Snapshot | undefined;
25
+ root: string;
26
+ timeoutMs: number;
27
+ source: () => Promise<string>;
28
+ }
29
+
30
+ export async function tailnetHealth(
31
+ tailnet: Tailnet,
32
+ nodes: FleetNode[],
33
+ options: TailnetHealthOptions,
34
+ run: Runner
35
+ ): Promise<NodeReport[]> {
36
+ const matches = await matchComputers(nodes, tailnet.computers, run);
37
+ const self = tailnet.computers.find((peer) => peer.self);
38
+ const candidates = self ? (matches.get(self.id) ?? []) : [];
39
+ const required = uniqueMapping(candidates, matches)
40
+ ? candidates[0]!.required
41
+ : HEALTH_CAPABILITIES;
42
+ const manager =
43
+ options.reference ??
44
+ (await probe(
45
+ options.root,
46
+ options.includeManagerReport === false && !self ? [] : required,
47
+ run
48
+ ));
49
+ let source: Promise<string> | undefined;
50
+ const reports = await mapBounded(tailnet.computers, async (peer) => {
51
+ const checks = await connectivity(peer, run);
52
+ if (peer.self)
53
+ return {
54
+ node: peer.dns,
55
+ snapshot: manager,
56
+ checks: [...checks, ...manager.checks],
57
+ };
58
+ const candidates = matches.get(peer.id)!;
59
+ if (!uniqueMapping(candidates, matches))
60
+ return missingMapping(peer, checks, candidates.length);
61
+ if (!peer.online) return { node: peer.dns, checks };
62
+ const node = candidates[0]!;
63
+ const report = await inspectComputer(
64
+ node,
65
+ {
66
+ action: 'probe',
67
+ expected: manager.fingerprint,
68
+ source: (source ??= Promise.resolve().then(options.source)),
69
+ timeoutMs: options.timeoutMs,
70
+ },
71
+ run
72
+ );
73
+ return combine(peer, checks, report, manager);
74
+ });
75
+ const absent = nodes.filter(
76
+ (node) => ![...matches.values()].some((matched) => matched.includes(node))
77
+ );
78
+ return [
79
+ { node: 'tailscale', checks: tailnet.checks },
80
+ ...(options.includeManagerReport !== false &&
81
+ !tailnet.computers.some((peer) => peer.self)
82
+ ? [
83
+ {
84
+ node: 'manager',
85
+ snapshot: manager,
86
+ checks: manager.checks,
87
+ },
88
+ ]
89
+ : []),
90
+ ...reports,
91
+ ...absent.map(absentNode),
92
+ ];
93
+ }
94
+
95
+ export function uniqueMapping(
96
+ candidates: FleetNode[],
97
+ matches: Map<string, FleetNode[]>
98
+ ): boolean {
99
+ return (
100
+ candidates.length === 1 &&
101
+ [...matches.values()].filter((nodes) => nodes.includes(candidates[0]!))
102
+ .length === 1
103
+ );
104
+ }
105
+
106
+ async function inspectComputer(
107
+ node: FleetNode,
108
+ options: {
109
+ action: 'probe';
110
+ source: Promise<string>;
111
+ timeoutMs: number;
112
+ expected?: Fingerprint;
113
+ },
114
+ run: Runner
115
+ ): Promise<NodeReport> {
116
+ try {
117
+ return await inspectRemote(
118
+ node,
119
+ { ...options, source: await options.source },
120
+ run
121
+ );
122
+ } catch {
123
+ return {
124
+ node: node.id,
125
+ checks: [
126
+ {
127
+ id: 'fleet:probe',
128
+ status: 'fail',
129
+ summary:
130
+ 'The current remote probe could not be prepared or completed; this computer remains unverified.',
131
+ remedy: {
132
+ action:
133
+ 'Reinstall the validated Personal CLI package and retry. No raw exception or remote output is retained.',
134
+ },
135
+ },
136
+ ],
137
+ };
138
+ }
139
+ }
140
+
141
+ async function connectivity(
142
+ peer: TailnetComputer,
143
+ run: Runner
144
+ ): Promise<Check[]> {
145
+ const online: Check = {
146
+ id: 'tailscale:online',
147
+ status: peer.online ? 'pass' : 'fail',
148
+ summary: peer.online
149
+ ? 'Computer is online in Tailscale status.'
150
+ : 'Computer is offline or Tailscale cannot confirm its online state.',
151
+ };
152
+ if (peer.self || !peer.online) return [online];
153
+ const ping = await run(
154
+ ['tailscale', 'ping', '--c=1', '--timeout=5s', peer.ips[0]!],
155
+ { timeoutMs: 8_000 }
156
+ );
157
+ return [
158
+ online,
159
+ {
160
+ id: 'tailscale:reachability',
161
+ status: ping.ok ? 'pass' : 'fail',
162
+ summary: ping.ok
163
+ ? 'Tailscale peer answered a bounded reachability probe.'
164
+ : 'Tailscale peer did not answer; online inventory is not proof of connectivity.',
165
+ },
166
+ ];
167
+ }
168
+
169
+ function missingMapping(
170
+ peer: TailnetComputer,
171
+ checks: Check[],
172
+ count: number
173
+ ): NodeReport {
174
+ return {
175
+ node: peer.dns,
176
+ checks: [
177
+ ...checks,
178
+ {
179
+ id: 'fleet:registration',
180
+ status: 'fail',
181
+ summary: count
182
+ ? 'Roster mapping is ambiguous between entries or computers; no SSH probe was run.'
183
+ : 'Computer is not registered with a verified SSH alias and checkout path; dependencies and authentication are unverified.',
184
+ remedy: {
185
+ action:
186
+ 'Run fleet-node-add with its existing SSH alias and actual checkout path, then rerun fleet-status --tailnet.',
187
+ },
188
+ },
189
+ ],
190
+ };
191
+ }
192
+
193
+ function combine(
194
+ peer: TailnetComputer,
195
+ checks: Check[],
196
+ report: NodeReport,
197
+ reference: Snapshot
198
+ ): NodeReport {
199
+ return {
200
+ ...report,
201
+ node: peer.dns,
202
+ checks: [
203
+ ...checks,
204
+ ...report.checks,
205
+ ...(report.snapshot
206
+ ? compareFingerprints(
207
+ reference.fingerprint,
208
+ report.snapshot.fingerprint
209
+ )
210
+ : []),
211
+ ],
212
+ };
213
+ }
214
+
215
+ function absentNode(node: FleetNode): NodeReport {
216
+ return {
217
+ node: node.id,
218
+ checks: [
219
+ {
220
+ id: 'tailscale:membership',
221
+ status: 'fail',
222
+ summary:
223
+ 'Registered node is absent from visible Tailscale inventory. It was not silently omitted.',
224
+ remedy: {
225
+ action:
226
+ 'Check the node account, tailnet visibility, and the registered SSH alias.',
227
+ },
228
+ },
229
+ ],
230
+ };
231
+ }
@@ -0,0 +1,37 @@
1
+ import type { FleetNode, Runner } from './types';
2
+ import type { TailnetComputer } from './tailscale';
3
+
4
+ export function matchesComputer(host: string, peer: TailnetComputer): boolean {
5
+ const name = host.toLowerCase().replace(/\.$/, '');
6
+ return [peer.dns, peer.dns.split('.')[0], ...peer.ips].includes(name);
7
+ }
8
+
9
+ export async function matchComputers(
10
+ nodes: FleetNode[],
11
+ peers: TailnetComputer[],
12
+ run: Runner
13
+ ): Promise<Map<string, FleetNode[]>> {
14
+ const matches = new Map(peers.map((peer) => [peer.id, [] as FleetNode[]]));
15
+ for (const node of nodes) {
16
+ const resolved = await run(
17
+ [
18
+ 'ssh',
19
+ '-G',
20
+ '-o',
21
+ 'CanonicalizeHostname=no',
22
+ '-o',
23
+ 'PermitLocalCommand=no',
24
+ node.ssh,
25
+ ],
26
+ { timeoutMs: 5_000 }
27
+ );
28
+ const hostname = resolved.ok
29
+ ? /^hostname ([^\r\n]+)$/im.exec(resolved.stdout)?.[1]?.trim()
30
+ : undefined;
31
+ const targets = peers.filter((peer) =>
32
+ matchesComputer(hostname ?? node.ssh, peer)
33
+ );
34
+ for (const peer of targets) matches.get(peer.id)!.push(node);
35
+ }
36
+ return matches;
37
+ }
@@ -0,0 +1,141 @@
1
+ import { isIP } from 'node:net';
2
+ import { record } from './config';
3
+ import type { Check, Runner } from './types';
4
+
5
+ export interface TailnetComputer {
6
+ id: string;
7
+ name: string;
8
+ dns: string;
9
+ os: string;
10
+ ips: string[];
11
+ online: boolean;
12
+ self: boolean;
13
+ }
14
+ export interface Tailnet {
15
+ computers: TailnetComputer[];
16
+ excludedMobile: number;
17
+ checks: Check[];
18
+ }
19
+
20
+ export async function discoverTailnet(run: Runner): Promise<Tailnet> {
21
+ const result = await run(['tailscale', 'status', '--json'], {
22
+ timeoutMs: 15_000,
23
+ });
24
+ if (!result.ok)
25
+ return unavailable(
26
+ 'Tailscale status is unavailable; check the CLI, daemon and account login.'
27
+ );
28
+ try {
29
+ return parseTailnet(JSON.parse(result.stdout) as unknown);
30
+ } catch {
31
+ return unavailable(
32
+ 'Tailscale returned an unsupported status document. Update the CLI and check its daemon.'
33
+ );
34
+ }
35
+ }
36
+
37
+ export function parseTailnet(data: unknown): Tailnet {
38
+ if (!record(data) || data['BackendState'] !== 'Running')
39
+ return unavailable(
40
+ 'Tailscale is not running and signed in. Review tailscale status locally.'
41
+ );
42
+ validateStatusFields(data);
43
+ const peers = Object.values(
44
+ (data['Peer'] as Record<string, unknown> | null) ?? {}
45
+ );
46
+ if (peers.length > 1024) throw new Error('Too many peers');
47
+ const all = [
48
+ parseComputer(data['Self'], true),
49
+ ...peers.map((peer) => parseComputer(peer, false)),
50
+ ];
51
+ if (new Set(all.map((peer) => peer.id)).size !== all.length)
52
+ throw new Error('Duplicate peer');
53
+ const mobile = (peer: TailnetComputer) =>
54
+ ['ios', 'android', 'tvos'].includes(peer.os.toLowerCase());
55
+ const computers = all
56
+ .filter((peer) => !mobile(peer))
57
+ .sort((a, b) => a.name.localeCompare(b.name, 'en'));
58
+ const healthy = healthyDiscovery(data, computers.length);
59
+ return {
60
+ computers,
61
+ excludedMobile: all.filter(mobile).length,
62
+ checks: [
63
+ {
64
+ id: 'tailscale:discovery',
65
+ status: healthy ? 'pass' : 'fail',
66
+ summary: healthy
67
+ ? 'Signed-in Tailscale daemon returned its visible computer inventory.'
68
+ : 'Tailscale reports a daemon/coordination health problem or no computers; discovery may be incomplete.',
69
+ ...(!healthy
70
+ ? {
71
+ remedy: {
72
+ action:
73
+ 'Review tailscale status and network connectivity. Resolve coordination/daemon health before claiming complete coverage.',
74
+ },
75
+ }
76
+ : {}),
77
+ },
78
+ ],
79
+ };
80
+ }
81
+
82
+ function healthyDiscovery(
83
+ data: Record<string, unknown>,
84
+ count: number
85
+ ): boolean {
86
+ const health = (data['Health'] as unknown[] | null | undefined) ?? [];
87
+ return health.length === 0 && count > 0;
88
+ }
89
+
90
+ function validateStatusFields(data: Record<string, unknown>): void {
91
+ const health = data['Health'] ?? [];
92
+ const peers = data['Peer'] ?? {};
93
+ if (!record(data['Self']) || !record(peers) || !Array.isArray(health))
94
+ throw new Error('Unsupported Tailscale status');
95
+ }
96
+
97
+ function parseComputer(value: unknown, self: boolean): TailnetComputer {
98
+ if (!record(value)) throw new Error('Invalid peer');
99
+ const {
100
+ ID: id,
101
+ HostName: name,
102
+ DNSName: dns,
103
+ OS: os,
104
+ TailscaleIPs: ips,
105
+ Online: online,
106
+ } = value;
107
+ if (
108
+ ![id, name, os].every(
109
+ (field) =>
110
+ typeof field === 'string' &&
111
+ field.length > 0 &&
112
+ field.length <= 256 &&
113
+ !/[\x00-\x1f\x7f]/.test(field)
114
+ ) ||
115
+ typeof dns !== 'string' ||
116
+ !/^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}\.?$/.test(dns) ||
117
+ !Array.isArray(ips) ||
118
+ ips.length < 1 ||
119
+ ips.length > 16 ||
120
+ !ips.every((ip) => typeof ip === 'string' && isIP(ip) !== 0) ||
121
+ typeof online !== 'boolean'
122
+ )
123
+ throw new Error('Invalid peer fields');
124
+ return {
125
+ id: id as string,
126
+ name: name as string,
127
+ dns: dns.replace(/\.$/, '').toLowerCase(),
128
+ os: os as string,
129
+ ips: ips as string[],
130
+ online,
131
+ self,
132
+ };
133
+ }
134
+
135
+ function unavailable(summary: string): Tailnet {
136
+ return {
137
+ computers: [],
138
+ excludedMobile: 0,
139
+ checks: [{ id: 'tailscale:discovery', status: 'fail', summary }],
140
+ };
141
+ }
@@ -0,0 +1,108 @@
1
+ import { join } from 'node:path';
2
+ import { record } from './config';
3
+ import type { Check, Runner } from './types';
4
+
5
+ export async function toolchainChecks(
6
+ root: string,
7
+ run: Runner,
8
+ worker?: boolean
9
+ ): Promise<Check[]> {
10
+ const result = await run(doctorArguments(root, worker), {
11
+ cwd: root,
12
+ timeoutMs: 90_000,
13
+ });
14
+ const fallback: Check = {
15
+ id: 'auth:toolchain',
16
+ status: 'fail',
17
+ summary: 'Pinned tool/dependency and Docker checks could not be verified.',
18
+ remedy: {
19
+ action:
20
+ 'Run the checkout bootstrap/doctor.ts --json --docker on this computer. Reconcile versions only through the shared mise setup.',
21
+ },
22
+ };
23
+ try {
24
+ const data: unknown = JSON.parse(result.stdout);
25
+ if (
26
+ !record(data) ||
27
+ typeof data['ok'] !== 'boolean' ||
28
+ !Array.isArray(data['checks']) ||
29
+ !data['checks'].length ||
30
+ data['checks'].length > 150
31
+ )
32
+ return [fallback];
33
+ const checks = data['checks'].map(toolCheck);
34
+ if (checks.some((check) => check === null)) return [fallback];
35
+ return [
36
+ ...(!result.ok || !data['ok'] ? [fallback] : []),
37
+ ...(checks as Check[]),
38
+ ];
39
+ } catch {
40
+ return [fallback];
41
+ }
42
+ }
43
+
44
+ /** Checks of the doctor that are not a tool version, with their own words. */
45
+ const OWN: Record<string, { pass: string; fail: string; action: string }> = {
46
+ 'global-mise-config': {
47
+ pass: 'The global mise configuration is the one fleet-sync publishes.',
48
+ fail: 'The global mise configuration is missing, was replaced by another project or tool, or is not the one this revision publishes.',
49
+ action:
50
+ 'Run devfleet fleet-sync from the project that holds fleet.json. On a fleet machine the global mise configuration belongs to dev-fleet: a project pins its own tools in a project mise.toml and never publishes a global one.',
51
+ },
52
+ };
53
+
54
+ /** A doctor check with its own words, or null for a tool version. */
55
+ function ownCheck(id: string, status: Check['status']): Check | null {
56
+ if (!Object.hasOwn(OWN, id)) return null;
57
+ const own = OWN[id]!;
58
+ const ok = status === 'pass';
59
+ return {
60
+ id: `tool:${id}`,
61
+ status,
62
+ summary: ok ? own.pass : own.fail,
63
+ ...(!ok ? { remedy: { action: own.action } } : {}),
64
+ };
65
+ }
66
+
67
+ function toolCheck(value: unknown): Check | null {
68
+ if (
69
+ !record(value) ||
70
+ typeof value['id'] !== 'string' ||
71
+ !/^[a-zA-Z0-9@:/._-]{1,75}$/.test(value['id']) ||
72
+ !['pass', 'fail', 'warn', 'skip'].includes(String(value['status']))
73
+ )
74
+ return null;
75
+ const own = ownCheck(value['id'], value['status'] as Check['status']);
76
+ if (own) return own;
77
+ const ok = value['status'] === 'pass';
78
+ const version =
79
+ typeof value['detail'] === 'string' &&
80
+ /^v?\d{1,6}(?:\.\d{1,6}){1,3}$/.test(value['detail'])
81
+ ? ` ${value['detail']}`
82
+ : '';
83
+ return {
84
+ id: `tool:${value['id'].toLowerCase().replace(/[^a-z0-9:._-]/g, '_')}`,
85
+ status: value['status'] as Check['status'],
86
+ summary: ok
87
+ ? `Expected tool version or runtime check succeeded${version}.`
88
+ : 'Expected tool version, runtime or authentication check did not pass.',
89
+ ...(!ok
90
+ ? {
91
+ remedy: {
92
+ action:
93
+ 'Review bootstrap/doctor.ts --json --docker locally. Use shared mise/workstation setup to reconcile this computer.',
94
+ },
95
+ }
96
+ : {}),
97
+ };
98
+ }
99
+
100
+ function doctorArguments(root: string, worker?: boolean): string[] {
101
+ return [
102
+ 'bun',
103
+ join(root, 'bootstrap/doctor.ts'),
104
+ '--json',
105
+ '--docker',
106
+ ...(worker ? ['--worker'] : []),
107
+ ];
108
+ }
@@ -0,0 +1,24 @@
1
+ import type { FleetDefinition } from './definition';
2
+
3
+ /** Fleet topology: one singleton manager (the human's machine) and stateless
4
+ * Linux workers. Every singleton service runs on the manager; workers only
5
+ * consume them and can be turned off, replaced or re-imaged at will. */
6
+ export type Role = 'manager' | 'worker';
7
+
8
+ export function roleOf(definition: FleetDefinition, id: string): Role | null {
9
+ if (definition.manager.id === id) return 'manager';
10
+ return definition.workers.some((entry) => entry.id === id) ? 'worker' : null;
11
+ }
12
+
13
+ /** Private Beszel hub URL on the manager; agents never use the public name. */
14
+ export function hubUrl(definition: FleetDefinition): string {
15
+ return `https://${definition.manager.network.tailscaleName}:${definition.services.monitoring.hubPort}`;
16
+ }
17
+
18
+ /** Normalizes a macOS LocalHostName / hostname to a fleet ID for identity checks. */
19
+ export function hostId(hostname: string): string {
20
+ return hostname
21
+ .toLowerCase()
22
+ .replace(/\.local$/, '')
23
+ .replace(/[^a-z0-9-]/g, '-');
24
+ }
@@ -0,0 +1,122 @@
1
+ import { managedProbeTarget } from './managed-source';
2
+ import type { ConfigurationRequest } from './configure';
3
+ import { remoteCommand, sshFailure, sshArgs } from './ssh';
4
+ import { cleanCheck, decodeSnapshot, validCheck } from './protocol';
5
+ import { record } from './config';
6
+ import { fileURLToPath } from 'node:url';
7
+ import type { FleetNode, NodeReport, Runner, Fingerprint } from './types';
8
+ import type { AutomatedRepair } from './repair-catalog';
9
+
10
+ export async function bundleProbe(): Promise<string> {
11
+ const result = await Bun.build({
12
+ entrypoints: [fileURLToPath(new URL('./remote-entry.ts', import.meta.url))],
13
+ target: 'node',
14
+ minify: true,
15
+ });
16
+ if (!result.success || !result.outputs[0])
17
+ throw new Error('Cannot bundle the fleet probe.');
18
+ return result.outputs[0].text();
19
+ }
20
+
21
+ export interface RemoteOptions {
22
+ source: string;
23
+ timeoutMs: number;
24
+ action: 'probe' | 'sync' | 'repair' | 'configure';
25
+ configuration?: ConfigurationRequest;
26
+ repairs?: AutomatedRepair[];
27
+ expected?: Fingerprint;
28
+ pull?: boolean;
29
+ }
30
+
31
+ export async function inspectRemote(
32
+ node: FleetNode,
33
+ options: RemoteOptions,
34
+ run: Runner
35
+ ): Promise<NodeReport> {
36
+ if (node.required.includes('worker')) {
37
+ const preflight = await run([...sshArgs(node), 'true'], {
38
+ timeoutMs: 15_000,
39
+ });
40
+ if (!preflight.ok)
41
+ return { node: node.id, checks: [sshFailure(preflight)] };
42
+ }
43
+ const target =
44
+ options.action === 'probe' && options.expected
45
+ ? await managedProbeTarget(node, options.expected.head, run)
46
+ : { node, checks: [] };
47
+ const command = remoteCommand(target.node, {
48
+ action: options.action,
49
+ configuration: options.configuration,
50
+ node: target.node,
51
+ apply: options.action !== 'probe',
52
+ repairs: options.repairs,
53
+ expected: options.expected,
54
+ pull: options.pull === true,
55
+ });
56
+ const result = await run(command, {
57
+ input: options.source,
58
+ timeoutMs: options.timeoutMs,
59
+ });
60
+ if (!result.ok)
61
+ return { node: node.id, checks: [...target.checks, sshFailure(result)] };
62
+ if (options.action !== 'probe') return decodeSync(node.id, result.stdout);
63
+ const snapshot = decodeSnapshot(result.stdout);
64
+ if (!snapshot) return protocolFailure(node.id);
65
+ return {
66
+ node: node.id,
67
+ snapshot,
68
+ checks: [
69
+ ...target.checks,
70
+ {
71
+ id: 'ssh:session',
72
+ status: 'pass',
73
+ summary:
74
+ 'Fresh noninteractive SSH session and Node-compatible probe succeeded with verified host keys and no agent forwarding.',
75
+ },
76
+ ...snapshot.checks,
77
+ {
78
+ id: 'vscode:forwarding',
79
+ status: 'skip',
80
+ required: node.required.includes('vscode'),
81
+ summary:
82
+ 'VS Code Remote-SSH tunnel/server installation is not tested by a shell session. Use the onboarding forwarding test.',
83
+ },
84
+ ],
85
+ };
86
+ }
87
+
88
+ function decodeSync(node: string, output: string): NodeReport {
89
+ try {
90
+ const data: unknown = JSON.parse(output);
91
+ if (
92
+ record(data) &&
93
+ data['version'] === 1 &&
94
+ Array.isArray(data['checks']) &&
95
+ data['checks'].length > 0 &&
96
+ data['checks'].length <= 200 &&
97
+ data['checks'].every(validCheck)
98
+ ) {
99
+ return { node, checks: data['checks'].map(cleanCheck) };
100
+ }
101
+ } catch {
102
+ return protocolFailure(node);
103
+ }
104
+ return protocolFailure(node);
105
+ }
106
+
107
+ function protocolFailure(node: string): NodeReport {
108
+ return {
109
+ node,
110
+ checks: [
111
+ {
112
+ id: 'ssh:protocol',
113
+ status: 'fail',
114
+ summary: 'Remote response is not a supported fleet JSON document.',
115
+ remedy: {
116
+ action:
117
+ 'Remove stdout banners from noninteractive shell startup; check the Node/Bun runtime. No raw remote output is retained.',
118
+ },
119
+ },
120
+ ],
121
+ };
122
+ }