@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,219 @@
1
+ import { verifyT3Linked } from '../t3-linked-check';
2
+ import { verifyT3Endpoint } from '../t3-endpoint';
3
+ import type { Check, NodeReport, Runner } from '../types';
4
+ import { fleet as definition } from '../fleet-config';
5
+ import { enrolledRoster } from '../definition';
6
+ import type { Tailnet } from '../tailscale';
7
+ import type { FleetNode } from '../types';
8
+ import {
9
+ publicHubUrl,
10
+ verifyBeszel,
11
+ type BeszelRequest,
12
+ } from '../monitoring/beszel';
13
+ import { hubAccess } from '../monitoring/hub-access';
14
+ import { hubUrl } from '../topology';
15
+
16
+ export interface DashboardAccess {
17
+ (hub: string): Promise<{ request: BeszelRequest }>;
18
+ }
19
+
20
+ /** The hub always runs on the manager, reached by its MagicDNS name. */
21
+ export function dashboardUrl(tailnet: Tailnet): string | null {
22
+ const canonical = definition;
23
+ const visible = tailnet.computers.some(
24
+ (computer) =>
25
+ computer.dns.replace(/\.$/, '') ===
26
+ canonical.manager.network.tailscaleName
27
+ );
28
+ return visible ? hubUrl(canonical) : null;
29
+ }
30
+
31
+ /** Centralized dashboard evidence for every canonical worker: the hub must answer
32
+ * privately, and each node must be registered, connected and reporting fresh
33
+ * samples. A missing manager Vault session is reported, never silently passed.
34
+ */
35
+ export async function appendDashboardEvidence(
36
+ reports: NodeReport[],
37
+ tailnet: Tailnet,
38
+ options: { access?: DashboardAccess; fetcher?: typeof fetch } = {}
39
+ ): Promise<string | null> {
40
+ const hub = dashboardUrl(tailnet);
41
+ const dashboard: NodeReport = { node: 'dashboard', checks: [] };
42
+ reports.push(dashboard);
43
+ if (!hub) {
44
+ dashboard.checks.push(
45
+ fail(
46
+ 'beszel:hub',
47
+ 'BESZEL_HUB_SELECTION_REQUIRED',
48
+ 'The hub runs on the manager, which is not visible in Tailscale. Wake the manager and reconnect it to the tailnet.'
49
+ )
50
+ );
51
+ return null;
52
+ }
53
+ const fetcher = options.fetcher ?? fetch;
54
+ dashboard.checks.push(await hubReachable(hub, fetcher));
55
+ dashboard.checks.push(await publicEndpoint(fetcher));
56
+ await nodeEvidence(reports, dashboard, hub, options.access ?? hubAccess);
57
+ return hub;
58
+ }
59
+
60
+ async function nodeEvidence(
61
+ reports: NodeReport[],
62
+ dashboard: NodeReport,
63
+ hub: string,
64
+ access: DashboardAccess
65
+ ): Promise<void> {
66
+ const canonical = definition;
67
+ try {
68
+ const { request } = await access(hub);
69
+ for (const machine of canonical.workers) {
70
+ const evidence = await verifyBeszel(hub, machine.id, request);
71
+ const report =
72
+ reports.find(
73
+ (entry) =>
74
+ entry.node === machine.network?.tailscaleName ||
75
+ entry.node === machine.id
76
+ ) ?? dashboard;
77
+ report.checks.push(...evidence.checks);
78
+ }
79
+ } catch (error) {
80
+ dashboard.checks.push({
81
+ id: 'beszel:access',
82
+ status: 'skip',
83
+ summary:
84
+ error instanceof Error && error.message.startsWith('BESZEL_')
85
+ ? error.message
86
+ : 'BESZEL_ACCOUNT_REQUIRED',
87
+ remedy: {
88
+ action:
89
+ 'Authenticate this manager to Vault so BESZEL_FLEET_AUTH can be read, then rerun fleet-status --workers. Node registration and metric freshness are unverified until then.',
90
+ },
91
+ });
92
+ }
93
+ }
94
+
95
+ async function hubReachable(
96
+ hub: string,
97
+ fetcher: typeof fetch
98
+ ): Promise<Check> {
99
+ try {
100
+ const response = await fetcher(`${hub}/api/health`, {
101
+ redirect: 'error',
102
+ signal: AbortSignal.timeout(10_000),
103
+ });
104
+ if (response.ok)
105
+ return {
106
+ id: 'beszel:hub',
107
+ status: 'pass',
108
+ summary: `Dashboard answers privately at ${hub}.`,
109
+ };
110
+ } catch {
111
+ /* Connection details are not evidence; fall through. */
112
+ }
113
+ return fail(
114
+ 'beszel:hub',
115
+ 'BESZEL_HUB_UNREACHABLE',
116
+ `The private dashboard did not answer at ${hub}/api/health. The hub runs on the manager: keep it awake with Docker Desktop running, then run devfleet fleet-sync there.`
117
+ );
118
+ }
119
+
120
+ /** The Cloudflare hostname is convenience access; the private hub stays canonical,
121
+ * so an offline connector warns instead of failing the audit.
122
+ */
123
+ async function publicEndpoint(fetcher: typeof fetch): Promise<Check> {
124
+ try {
125
+ const response = await fetcher(`${publicHubUrl()}/api/health`, {
126
+ redirect: 'error',
127
+ signal: AbortSignal.timeout(10_000),
128
+ });
129
+ if (response.ok)
130
+ return {
131
+ id: 'beszel:public-endpoint',
132
+ status: 'pass',
133
+ summary: `Dashboard answers publicly at ${publicHubUrl()}.`,
134
+ };
135
+ } catch {
136
+ /* Connection details are not evidence; fall through. */
137
+ }
138
+ return {
139
+ id: 'beszel:public-endpoint',
140
+ status: 'warn',
141
+ summary: 'BESZEL_PUBLIC_ENDPOINT_OFFLINE',
142
+ remedy: {
143
+ action: `${publicHubUrl()} did not answer. Create the Cloudflare tunnel and store its token in the fleet's secrets, then run fleet-sync on the manager so the hub starts its Cloudflare connector. The private dashboard is unaffected.`,
144
+ },
145
+ };
146
+ }
147
+
148
+ function fail(id: string, summary: string, action: string): Check {
149
+ return { id, status: 'fail', summary, remedy: { action } };
150
+ }
151
+
152
+ /** Explicit developer scope; the ordinary health command still audits all peers. */
153
+ export function managedHealthScope(
154
+ tailnet: Tailnet,
155
+ roster: FleetNode[],
156
+ includeManager = true
157
+ ) {
158
+ const canonical = definition;
159
+ const declared = roster.filter((node) =>
160
+ canonical.workers.some((entry) => entry.id === node.id)
161
+ );
162
+ const nodes = declared.length
163
+ ? enrolledRoster(canonical, { version: 1, nodes: declared }).nodes
164
+ : [];
165
+ const computers = tailnet.computers.filter(
166
+ (peer) =>
167
+ (includeManager && peer.self) ||
168
+ canonical.workers.some(
169
+ (node) =>
170
+ peer.dns === node.network?.tailscaleName ||
171
+ peer.dns.split('.')[0] === node.ssh ||
172
+ nodes.some(
173
+ (entry) => entry.id === node.id && entry.tailscaleId === peer.id
174
+ )
175
+ )
176
+ );
177
+ const missing = canonical.workers.filter(
178
+ (node) => !nodes.some((entry) => entry.id === node.id)
179
+ );
180
+ return {
181
+ nodes,
182
+ tailnet: {
183
+ ...tailnet,
184
+ computers,
185
+ checks: [
186
+ ...tailnet.checks,
187
+ ...missing.map((node) => ({
188
+ id: `fleet:enrollment:${node.id}`,
189
+ status: 'fail' as const,
190
+ summary:
191
+ 'Canonical developer node lacks a verified SSH/check-out registration.',
192
+ remedy: {
193
+ action: `Enroll ${node.id} with devfleet fleet-node-add, or use interactive fleet-sync enrollment.`,
194
+ },
195
+ })),
196
+ ],
197
+ },
198
+ excluded: tailnet.computers
199
+ .filter((peer) => !computers.includes(peer))
200
+ .map((peer) => peer.dns),
201
+ };
202
+ }
203
+
204
+ export async function appendManagerLinks(
205
+ reports: NodeReport[],
206
+ registered: FleetNode[],
207
+ run: Runner
208
+ ): Promise<void> {
209
+ for (const node of registered) {
210
+ const machine = definition.workers.find((entry) => entry.id === node.id);
211
+ const report = reports.find(
212
+ (entry) =>
213
+ entry.node === machine?.network?.tailscaleName || entry.node === node.id
214
+ );
215
+ if (!report) continue;
216
+ report.checks.push(...(await verifyT3Endpoint(node, run)));
217
+ report.checks.push(await verifyT3Linked(node, run));
218
+ }
219
+ }
@@ -0,0 +1,34 @@
1
+ import { join } from 'node:path';
2
+ import { read, object, write } from '../t3/files';
3
+ import { FleetUsageError } from '../errors';
4
+
5
+ export async function deployment(root: string) {
6
+ const raw = await read(join(root, 'node_modules/.personal-node.json'));
7
+ if (raw === null) return null;
8
+ const data = object(raw);
9
+ if (
10
+ data['version'] !== 1 ||
11
+ typeof data['revision'] !== 'string' ||
12
+ !/^[a-f0-9]{40}$/.test(data['revision']) ||
13
+ typeof data['node'] !== 'string' ||
14
+ !/^[a-z0-9][a-z0-9-]{0,62}$/.test(data['node'])
15
+ )
16
+ throw new FleetUsageError(
17
+ 'Invalid local node receipt. Restore it before reconciling.'
18
+ );
19
+ return {
20
+ workspace: join(root, 'node_modules/.fleet-revisions', data['revision']),
21
+ node: data['node'],
22
+ revision: data['revision'],
23
+ };
24
+ }
25
+
26
+ export async function recordDeployment(
27
+ root: string,
28
+ node: string,
29
+ revision: string
30
+ ) {
31
+ const path = join(root, 'node_modules/.personal-node.json');
32
+ const content = JSON.stringify({ version: 1, node, revision }) + '\n';
33
+ if ((await read(path)) !== content) await write(path, content);
34
+ }
@@ -0,0 +1,121 @@
1
+ import { join } from 'node:path';
2
+ import { configureWorkspace } from '../configure';
3
+ import { runCommand } from '../runner';
4
+ import { FleetUsageError } from '../errors';
5
+ import { packageInfo } from '../package-info';
6
+ import type { Runner } from '../types';
7
+ import type { Settings } from './settings';
8
+ import type { CommandOutcome } from './contracts';
9
+ import { syncT3 } from './node-t3';
10
+ import { nodeId, type T3Options } from '../t3/desired';
11
+ import { recordDeployment } from './node-deployment';
12
+
13
+ export interface NodeSyncOptions extends T3Options {
14
+ revision?: string;
15
+ }
16
+
17
+ /** One local convergence: locked tools, public launcher, profiles, then T3. */
18
+ export async function syncNode(
19
+ options: NodeSyncOptions,
20
+ settings: Settings,
21
+ run: Runner = runCommand
22
+ ): Promise<CommandOutcome> {
23
+ if (process.platform === 'win32' && !options.dryRun)
24
+ throw new FleetUsageError(
25
+ 'Codex proxy node sync requires macOS/Linux/WSL. Use dev-fleet commands for native Windows configuration.'
26
+ );
27
+ const node = nodeId(options.id);
28
+ // An installed package is not a checkout, and the Git repository around it
29
+ // is the operator's own project: its HEAD is not a dev-fleet revision.
30
+ if (packageInfo().revision)
31
+ throw new FleetUsageError(
32
+ 'fleet-node-sync works on a checkout of dev-fleet. Run fleet-sync instead: it converges this machine from a checkout it owns.'
33
+ );
34
+ const head = await run(['git', 'rev-parse', '--verify', 'HEAD'], {
35
+ cwd: settings.workspace,
36
+ });
37
+ const revision = options.revision ?? head.stdout.trim();
38
+ if (!head.ok || !/^[a-f0-9]{40}$/.test(revision))
39
+ throw new FleetUsageError(
40
+ 'An existing personal checkout is required; set --workspace PATH.'
41
+ );
42
+ const request = { node, revision, tool: 'codex', profile: 'proxy' };
43
+ if (options.dryRun)
44
+ return {
45
+ exitCode: 0,
46
+ data: {
47
+ version: 1,
48
+ command: 'fleet-node-sync',
49
+ ok: true,
50
+ applied: false,
51
+ ...request,
52
+ steps: [
53
+ 'Reconcile locked tools',
54
+ 'Install personal and retire owned ws launcher',
55
+ 'Upsert Codex proxy profile',
56
+ 'Reconcile declared T3 instances and remove stale owned entries',
57
+ ],
58
+ },
59
+ };
60
+ const checks = await configureWorkspace(settings.workspace, request, run);
61
+ if (checks.some((check) => check.status !== 'pass'))
62
+ return {
63
+ exitCode: 1,
64
+ data: { version: 1, command: 'fleet-node-sync', ok: false, checks },
65
+ };
66
+ return finishSync(options, settings, { request, checks }, run);
67
+ }
68
+
69
+ async function finishSync(
70
+ options: NodeSyncOptions,
71
+ settings: Settings,
72
+ input: {
73
+ request: { node: string; revision: string; tool: string; profile: string };
74
+ checks: Awaited<ReturnType<typeof configureWorkspace>>;
75
+ },
76
+ run: Runner
77
+ ): Promise<CommandOutcome> {
78
+ const { request, checks } = input;
79
+ const { node, revision } = request;
80
+ const workspace = join(
81
+ settings.workspace,
82
+ 'node_modules/.fleet-revisions',
83
+ revision
84
+ );
85
+ const installed = await run(['sh', 'bootstrap/install-launcher.sh'], {
86
+ cwd: workspace,
87
+ timeoutMs: 30_000,
88
+ });
89
+ if (!installed.ok)
90
+ return {
91
+ exitCode: 1,
92
+ data: {
93
+ version: 1,
94
+ ok: false,
95
+ checks,
96
+ error:
97
+ 'Launcher installation failed; profile changes may already have applied.',
98
+ },
99
+ };
100
+ const t3 = await syncT3({ ...options, id: node }, { ...settings, workspace });
101
+ if (t3.exitCode !== 0) return t3;
102
+ await recordDeployment(settings.workspace, node, revision);
103
+ return {
104
+ exitCode: 0,
105
+ data: {
106
+ version: 1,
107
+ command: 'fleet-node-sync',
108
+ ok: true,
109
+ applied: true,
110
+ ...request,
111
+ checks,
112
+ t3: t3.data,
113
+ readiness: {
114
+ authentication: 'unverified',
115
+ inference: 'unverified',
116
+ pairing: 'unverified',
117
+ },
118
+ next: 'Use devfleet t3-status for configuration drift and devfleet fleet-status for machine health.',
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,31 @@
1
+ import { FleetUsageError } from '../errors';
2
+ import type { Settings } from './settings';
3
+ import type { CommandOutcome } from './contracts';
4
+ import { t3Command } from './t3/command';
5
+ import type { T3Options } from '../t3/desired';
6
+
7
+ export async function syncT3(
8
+ options: T3Options,
9
+ settings: Settings
10
+ ): Promise<CommandOutcome> {
11
+ try {
12
+ return await t3Command('t3-apply', options, settings);
13
+ } catch (error) {
14
+ return {
15
+ exitCode: 1,
16
+ data: {
17
+ version: 1,
18
+ command: 'fleet-node-sync',
19
+ ok: false,
20
+ applied: true,
21
+ stage: 't3',
22
+ error:
23
+ error instanceof FleetUsageError
24
+ ? error.message
25
+ : 'T3 reconciliation failed; inspect local configuration and ownership state.',
26
+ progress:
27
+ 'Tools, launcher and profile steps completed. Node receipt was not advanced. Fix the T3 conflict and repeat fleet-node-sync.',
28
+ },
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,141 @@
1
+ import { CAPABILITIES } from '../config';
2
+ import { FleetUsageError } from '../errors';
3
+ import type { Capability } from '../types';
4
+ import type { Settings } from './settings';
5
+
6
+ export interface CliOptions extends Partial<Settings> {
7
+ managed?: boolean;
8
+ workers?: boolean;
9
+ tailnet?: boolean;
10
+ list?: boolean;
11
+ onboard?: boolean;
12
+ schema?: boolean;
13
+ json?: boolean;
14
+ nonInteractive?: boolean;
15
+ interactive?: boolean;
16
+ local?: boolean;
17
+ all?: boolean;
18
+ node?: string[];
19
+ require?: string;
20
+ timeout?: string;
21
+ strict?: boolean;
22
+ repair?: string;
23
+ script?: boolean;
24
+ planHash?: string;
25
+ apply?: boolean;
26
+ yes?: boolean;
27
+ id?: string;
28
+ ssh?: string;
29
+ path?: string;
30
+ runtime?: string;
31
+ search?: string;
32
+ group?: string;
33
+ }
34
+
35
+ export function capabilities(value?: string): Capability[] {
36
+ const values =
37
+ value === 'none'
38
+ ? []
39
+ : (value?.split(',') ??
40
+ CAPABILITIES.filter((entry) => entry !== 'worker'));
41
+ if (values.some((entry) => !CAPABILITIES.includes(entry as Capability)))
42
+ throw new FleetUsageError(
43
+ `--require expects ${CAPABILITIES.join(',')} or none.`
44
+ );
45
+ return [...new Set(values)] as Capability[];
46
+ }
47
+
48
+ export function timeout(options: CliOptions): number {
49
+ const value = Number(
50
+ options.timeout ?? (options.apply ? 1_500_000 : 120_000)
51
+ );
52
+ if (!Number.isInteger(value) || value < 1000 || value > 1_800_000)
53
+ throw new FleetUsageError('--timeout must be 1000–1800000 milliseconds.');
54
+ return value;
55
+ }
56
+
57
+ /** Validation runs on the internal action selected by fleet-* flags. */
58
+ export function validateOptions(command: string, options: CliOptions): void {
59
+ if (options.tailnet && options.workers)
60
+ throw new FleetUsageError('Choose --tailnet or --workers, not both.');
61
+ if (options.onboard && options.schema)
62
+ throw new FleetUsageError('Choose --onboard or --schema, not both.');
63
+ validateMutation(command, options);
64
+ validateRepair(command, options);
65
+ validateScope(command, options);
66
+ if (
67
+ (['discover', 'health'].includes(command) ||
68
+ (command === 'repair' && !options.local)) &&
69
+ options.require !== undefined
70
+ )
71
+ throw new FleetUsageError(
72
+ '--require is supported for roster status and local repair only; tailnet checks use declared roster requirements.'
73
+ );
74
+ if (options.timeout !== undefined) timeout(options);
75
+ }
76
+
77
+ function validateMutation(command: string, options: CliOptions): void {
78
+ if (options.apply && !['add', 'repair'].includes(command))
79
+ throw new FleetUsageError(
80
+ '--apply is only valid for fleet-node-add and fleet-repair.'
81
+ );
82
+ if (options.apply && !options.yes)
83
+ throw new FleetUsageError('Mutation requires both --apply and --yes.');
84
+ }
85
+
86
+ function validateScope(command: string, options: CliOptions): void {
87
+ const selected = [options.local, options.all, options.node?.length].filter(
88
+ Boolean
89
+ );
90
+ if (['discover', 'health'].includes(command) && selected.length)
91
+ throw new FleetUsageError(
92
+ 'Tailscale discovery, --tailnet and --workers always cover all visible computers; --local, --all and --node are not supported.'
93
+ );
94
+ if (selected.length > 1)
95
+ throw new FleetUsageError(
96
+ 'Choose --local, --all, or --node, not a combination.'
97
+ );
98
+ validateRosterScope(command, options);
99
+ }
100
+
101
+ function validateRosterScope(command: string, options: CliOptions): void {
102
+ if ((options.local || options.all) && !['doctor', 'repair'].includes(command))
103
+ throw new FleetUsageError(
104
+ '--local and --all are only valid for fleet-status and fleet-repair.'
105
+ );
106
+ }
107
+
108
+ function validateRepair(command: string, options: CliOptions): void {
109
+ if (
110
+ options.planHash !== undefined &&
111
+ !/^[a-f0-9]{64}$/.test(options.planHash)
112
+ )
113
+ throw new FleetUsageError(
114
+ '--plan-hash expects the SHA-256 value from a repair preview.'
115
+ );
116
+ const repairFlags =
117
+ options.repair !== undefined ||
118
+ options.script === true ||
119
+ options.planHash !== undefined;
120
+ if (repairFlags && command !== 'repair')
121
+ throw new FleetUsageError(
122
+ '--repair, --script and --plan-hash are only valid for fleet-repair without --list.'
123
+ );
124
+ if (options.script && options.apply)
125
+ throw new FleetUsageError(
126
+ '--script only generates a preview; run a reviewed repair separately with --apply --yes.'
127
+ );
128
+ validateCatalog(command, options);
129
+ }
130
+
131
+ function validateCatalog(command: string, options: CliOptions): void {
132
+ const filtered =
133
+ options.local ||
134
+ options.all ||
135
+ Boolean(options.node?.length) ||
136
+ options.require !== undefined;
137
+ if (command === 'repairs' && filtered)
138
+ throw new FleetUsageError(
139
+ 'fleet-repair --list is a static catalog and does not accept target or capability filters.'
140
+ );
141
+ }
@@ -0,0 +1,7 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { bundleProbe } from '../transport';
3
+
4
+ export async function probeSource(): Promise<string> {
5
+ if (typeof Bun !== 'undefined') return bundleProbe();
6
+ return readFile(new URL('./probe.mjs', import.meta.url), 'utf8');
7
+ }