@crvouga/dev-fleet 0.0.0-stage → 0.1.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 (333) 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 +157 -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 +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -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/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
package/src/config.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Capability, FleetConfig, FleetNode } from './types';
3
+ import { FleetUsageError } from './errors';
4
+
5
+ export const CAPABILITIES: Capability[] = [
6
+ 'github',
7
+ 'vault',
8
+ 't3',
9
+ 'vscode',
10
+ 'toolchain',
11
+ 'codex',
12
+ 'claude',
13
+ 'services',
14
+ 'workstation',
15
+ 'worker',
16
+ ];
17
+ export const DEFAULT_CONFIG = '.fleet.local.json';
18
+
19
+ export function record(value: unknown): value is Record<string, unknown> {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ function fields(value: Record<string, unknown>, allowed: string[]): void {
24
+ if (Object.keys(value).some((key) => !allowed.includes(key))) {
25
+ throw new FleetUsageError(
26
+ 'Unknown configuration field. Run fleet-nodes --schema.'
27
+ );
28
+ }
29
+ }
30
+
31
+ function absolutePath(value: unknown): value is string {
32
+ return (
33
+ typeof value === 'string' &&
34
+ value.startsWith('/') &&
35
+ value.length <= 4096 &&
36
+ !/[\x00-\x1f\x7f]/.test(value)
37
+ );
38
+ }
39
+
40
+ function parseNode(value: unknown): FleetNode {
41
+ if (!record(value)) throw new FleetUsageError('Each node must be an object.');
42
+ fields(value, [
43
+ 'id',
44
+ 'ssh',
45
+ 'workspace',
46
+ 'runtime',
47
+ 'required',
48
+ 'tailscaleId',
49
+ 't3EnvironmentId',
50
+ 'bootstrapViaSsh',
51
+ ]);
52
+ if (
53
+ value['t3EnvironmentId'] !== undefined &&
54
+ (typeof value['t3EnvironmentId'] !== 'string' ||
55
+ !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(
56
+ value['t3EnvironmentId']
57
+ ))
58
+ )
59
+ throw new FleetUsageError('Invalid enrolled T3 environment identity.');
60
+ const names = nodeNames(value);
61
+ if (!absolutePath(value['workspace']))
62
+ throw new FleetUsageError('workspace must be an absolute POSIX path.');
63
+ if (value['runtime'] !== undefined && !absolutePath(value['runtime'])) {
64
+ throw new FleetUsageError(
65
+ 'runtime must be an absolute POSIX path to Node or Bun.'
66
+ );
67
+ }
68
+ return {
69
+ ...names,
70
+ ...enrollmentOptions(value),
71
+ ...(typeof value['t3EnvironmentId'] === 'string'
72
+ ? { t3EnvironmentId: value['t3EnvironmentId'] }
73
+ : {}),
74
+ workspace: value['workspace'],
75
+ ...(typeof value['runtime'] === 'string'
76
+ ? { runtime: value['runtime'] }
77
+ : {}),
78
+ required: requiredCapabilities(value['required']),
79
+ };
80
+ }
81
+
82
+ function enrollmentOptions(
83
+ value: Record<string, unknown>
84
+ ): Pick<FleetNode, 'tailscaleId' | 'bootstrapViaSsh'> {
85
+ if (
86
+ value['bootstrapViaSsh'] !== undefined &&
87
+ typeof value['bootstrapViaSsh'] !== 'boolean'
88
+ )
89
+ throw new FleetUsageError(
90
+ 'bootstrapViaSsh must explicitly authorize an existing trusted SSH bootstrap route.'
91
+ );
92
+ if (
93
+ value['tailscaleId'] !== undefined &&
94
+ (typeof value['tailscaleId'] !== 'string' ||
95
+ !/^[a-zA-Z0-9:_-]{1,128}$/.test(value['tailscaleId']))
96
+ )
97
+ throw new FleetUsageError(
98
+ 'tailscaleId must be the explicitly enrolled stable Tailscale device ID.'
99
+ );
100
+ return {
101
+ ...(value['bootstrapViaSsh'] === true ? { bootstrapViaSsh: true } : {}),
102
+ ...(typeof value['tailscaleId'] === 'string'
103
+ ? { tailscaleId: value['tailscaleId'] }
104
+ : {}),
105
+ };
106
+ }
107
+
108
+ function nodeNames(
109
+ value: Record<string, unknown>
110
+ ): Pick<FleetNode, 'id' | 'ssh'> {
111
+ if (
112
+ typeof value['id'] !== 'string' ||
113
+ !/^[a-z][a-z0-9-]{0,62}$/.test(value['id'])
114
+ ) {
115
+ throw new FleetUsageError(
116
+ 'Node IDs must be lowercase names (1–63 characters).'
117
+ );
118
+ }
119
+ if (
120
+ typeof value['ssh'] !== 'string' ||
121
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value['ssh'])
122
+ ) {
123
+ throw new FleetUsageError(
124
+ 'ssh must be a plain SSH alias or hostname, not options or user@host.'
125
+ );
126
+ }
127
+ return { id: value['id'], ssh: value['ssh'] };
128
+ }
129
+
130
+ function requiredCapabilities(value: unknown): Capability[] {
131
+ const required =
132
+ value === undefined ? ['github', 'vault', 't3', 'toolchain'] : value;
133
+ if (
134
+ !Array.isArray(required) ||
135
+ required.some((item) => !CAPABILITIES.includes(item as Capability))
136
+ ) {
137
+ throw new FleetUsageError(
138
+ 'required must contain only capability names from fleet-nodes --schema.'
139
+ );
140
+ }
141
+ if (new Set(required).size !== required.length)
142
+ throw new FleetUsageError('required capabilities must be unique.');
143
+ return required as Capability[];
144
+ }
145
+
146
+ export function parseConfig(value: unknown): FleetConfig {
147
+ if (!record(value))
148
+ throw new FleetUsageError('Fleet configuration must be an object.');
149
+ fields(value, ['$schema', 'version', 'nodes']);
150
+ if (value['$schema'] !== undefined && typeof value['$schema'] !== 'string')
151
+ throw new FleetUsageError('$schema must be a string.');
152
+ if (value['version'] !== 1 || !Array.isArray(value['nodes'])) {
153
+ throw new FleetUsageError('Expected version 1 and a nodes array.');
154
+ }
155
+ if (value['nodes'].length < 1 || value['nodes'].length > 64) {
156
+ throw new FleetUsageError('Configure between 1 and 64 developer nodes.');
157
+ }
158
+ const nodes = value['nodes'].map(parseNode);
159
+ if (new Set(nodes.map((node) => node.id)).size !== nodes.length) {
160
+ throw new FleetUsageError('Node IDs must be unique.');
161
+ }
162
+ return { version: 1, nodes };
163
+ }
164
+
165
+ export async function loadConfig(path: string): Promise<FleetConfig> {
166
+ let value: unknown;
167
+ try {
168
+ value = JSON.parse(await readFile(path, 'utf8'));
169
+ } catch {
170
+ throw new FleetUsageError(
171
+ 'Cannot read fleet config. Run devfleet fleet-node-add or use --config PATH.'
172
+ );
173
+ }
174
+ return parseConfig(value);
175
+ }
176
+
177
+ export function selectNodes(config: FleetConfig, ids: string[]): FleetNode[] {
178
+ if (ids.some((id) => !config.nodes.some((node) => node.id === id))) {
179
+ throw new FleetUsageError(
180
+ 'Unknown node selection. Run fleet-nodes --json.'
181
+ );
182
+ }
183
+ return ids.length === 0
184
+ ? config.nodes
185
+ : config.nodes.filter((node) => ids.includes(node.id));
186
+ }
@@ -0,0 +1,255 @@
1
+ import { join } from 'node:path';
2
+ import { mkdir, writeFile, rename, readFile } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { record } from './config';
5
+ import type { Check, Runner } from './types';
6
+
7
+ export type ConfigurationRequest = {
8
+ revision: string;
9
+ tool: string;
10
+ profile: string;
11
+ node: string;
12
+ };
13
+
14
+ export function validConfiguration(
15
+ value: unknown
16
+ ): value is ConfigurationRequest {
17
+ return (
18
+ record(value) &&
19
+ typeof value['revision'] === 'string' &&
20
+ /^[a-f0-9]{40}$/.test(value['revision']) &&
21
+ ['codex', 'claude', 'opencode', 'gemini', 'grok', 'antigravity'].includes(
22
+ String(value['tool'])
23
+ ) &&
24
+ ['direct', 'proxy'].includes(String(value['profile'])) &&
25
+ typeof value['node'] === 'string' &&
26
+ /^[a-z0-9][a-z0-9-]{0,62}$/.test(value['node'])
27
+ );
28
+ }
29
+
30
+ /** A pinned deployment worktree leaves the developer's branch and dirty files intact. */
31
+ export async function configureWorkspace(
32
+ root: string,
33
+ request: ConfigurationRequest,
34
+ run: Runner,
35
+ options: { additionalTools: string[]; bootstrappedDeployment: boolean } = {
36
+ additionalTools: [],
37
+ bootstrappedDeployment: false,
38
+ }
39
+ ): Promise<Check[]> {
40
+ if (
41
+ options.additionalTools.some(
42
+ (tool) => !validConfiguration({ ...request, tool })
43
+ )
44
+ )
45
+ return failure('Invalid additional profile.');
46
+ if (!validConfiguration(request))
47
+ return failure('Invalid reviewed revision or configuration selection.');
48
+ const directory = deploymentDirectory(
49
+ root,
50
+ request.revision,
51
+ options.bootstrappedDeployment
52
+ );
53
+ const head = await run(
54
+ ['git', 'rev-parse', '--verify', `${request.revision}^{commit}`],
55
+ { cwd: root }
56
+ );
57
+ if (!head.ok || head.stdout.trim() !== request.revision)
58
+ return failure(
59
+ 'Reviewed commit is unavailable locally. Fetch it explicitly and retry.'
60
+ );
61
+ const prepared = await prepareRevision(
62
+ root,
63
+ directory,
64
+ request.revision,
65
+ run
66
+ );
67
+ if (prepared.length) return prepared;
68
+ if (!options.bootstrappedDeployment) {
69
+ const setup = await setupRevision(directory, run);
70
+ if (setup.length) return setup;
71
+ }
72
+ const checks: Check[] = [];
73
+ for (const tool of [request.tool, ...options.additionalTools]) {
74
+ checks.push(
75
+ ...(await applyProfile(root, directory, { ...request, tool }, run))
76
+ );
77
+ }
78
+ return checks;
79
+ }
80
+ async function setupRevision(directory: string, run: Runner): Promise<Check[]> {
81
+ const setup = await run(
82
+ [
83
+ 'mise',
84
+ 'exec',
85
+ '--locked',
86
+ 'bun',
87
+ '--',
88
+ 'bun',
89
+ 'bootstrap/setup.ts',
90
+ '--non-interactive',
91
+ ],
92
+ { cwd: directory, timeoutMs: 1_200_000 }
93
+ );
94
+ if (!setup.ok)
95
+ return failure(
96
+ 'Existing setup owner failed. Partial tool/dependency installation is possible; no configuration receipt was advanced.'
97
+ );
98
+ return [];
99
+ }
100
+
101
+ async function applyProfile(
102
+ root: string,
103
+ directory: string,
104
+ request: ConfigurationRequest,
105
+ run: Runner
106
+ ): Promise<Check[]> {
107
+ const applied = await applyOwner(directory, request, run);
108
+ if (!applied.ok)
109
+ return failure(
110
+ 'Agent profile configuration failed. Inspect preview on this node; no rollout receipt was advanced.'
111
+ );
112
+ const receipt = decodeReceipt(applied.stdout, request.revision);
113
+ if (!receipt)
114
+ return failure(
115
+ 'The agent profile owner returned an invalid receipt; rollout remains unverified.'
116
+ );
117
+ await recordConfiguration(root, request, receipt);
118
+ return [
119
+ {
120
+ id: 'configuration:apply',
121
+ status: 'pass',
122
+ summary: `agent profile applied at revision ${request.revision}; configuration fingerprint ${receipt}. Authentication and T3 end-to-end remain separate gates.`,
123
+ },
124
+ ];
125
+ }
126
+
127
+ export function decodeReceipt(output: string, revision: string): string | null {
128
+ try {
129
+ const data: unknown = JSON.parse(output);
130
+ return record(data) &&
131
+ data['ok'] === true &&
132
+ data['revision'] === revision &&
133
+ typeof data['fingerprint'] === 'string' &&
134
+ /^[a-f0-9]{64}$/.test(data['fingerprint'])
135
+ ? data['fingerprint']
136
+ : null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function failure(summary: string): Check[] {
143
+ return [{ id: 'configuration:apply', status: 'fail', summary }];
144
+ }
145
+
146
+ async function prepareRevision(
147
+ root: string,
148
+ directory: string,
149
+ revision: string,
150
+ run: Runner
151
+ ): Promise<Check[]> {
152
+ if (root === directory) return verifyRevision(directory, revision, run);
153
+ await mkdir(join(root, 'node_modules', '.fleet-revisions'), {
154
+ recursive: true,
155
+ });
156
+ let deployed = await run(['git', 'rev-parse', 'HEAD'], { cwd: directory });
157
+ if (!deployed.ok) {
158
+ const created = await run(
159
+ ['git', 'worktree', 'add', '--detach', directory, revision],
160
+ { cwd: root, timeoutMs: 30_000 }
161
+ );
162
+ if (!created.ok)
163
+ return failure(
164
+ 'Cannot create pinned deployment worktree. Existing data was preserved.'
165
+ );
166
+ deployed = await run(['git', 'rev-parse', 'HEAD'], { cwd: directory });
167
+ }
168
+ return verifyRevision(directory, revision, run);
169
+ }
170
+ async function verifyRevision(
171
+ directory: string,
172
+ revision: string,
173
+ run: Runner
174
+ ): Promise<Check[]> {
175
+ const deployed = await run(['git', 'rev-parse', 'HEAD'], { cwd: directory });
176
+ const clean = await run(['git', 'status', '--porcelain'], { cwd: directory });
177
+ if (
178
+ !deployed.ok ||
179
+ deployed.stdout.trim() !== revision ||
180
+ !clean.ok ||
181
+ clean.stdout.trim()
182
+ )
183
+ return failure(
184
+ 'Pinned deployment checkout differs or is dirty. Inspect it; fleet never resets it.'
185
+ );
186
+ const top = await run(['git', 'rev-parse', '--show-toplevel'], {
187
+ cwd: directory,
188
+ });
189
+ if (!top.ok || top.stdout.trim() !== directory)
190
+ return failure('Deployment path is not its own Git worktree.');
191
+ return [];
192
+ }
193
+
194
+ async function recordConfiguration(
195
+ root: string,
196
+ request: ConfigurationRequest,
197
+ receipt: string
198
+ ): Promise<void> {
199
+ const receipts = join(root, 'node_modules', '.fleet-configuration');
200
+ await mkdir(receipts, { recursive: true, mode: 0o700 });
201
+ const path = join(
202
+ receipts,
203
+ `${request.node}-${request.tool}-${request.profile}.json`
204
+ );
205
+ const contents =
206
+ JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + '\n';
207
+ try {
208
+ if ((await readFile(path, 'utf8')) === contents) return;
209
+ } catch (error) {
210
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
211
+ }
212
+ const temp = `${path}.${randomUUID()}.tmp`;
213
+ await writeFile(temp, contents, { flag: 'wx', mode: 0o600 });
214
+ await rename(temp, path);
215
+ }
216
+
217
+ async function applyOwner(
218
+ directory: string,
219
+ request: ConfigurationRequest,
220
+ run: Runner
221
+ ) {
222
+ return run(
223
+ [
224
+ 'mise',
225
+ 'exec',
226
+ '--locked',
227
+ 'bun',
228
+ '--',
229
+ 'bun',
230
+ 'src/agents/cli.ts',
231
+ '--json',
232
+ '--yes',
233
+ '--non-interactive',
234
+ 'agents',
235
+ 'apply',
236
+ '--node',
237
+ request.node,
238
+ '--tool',
239
+ request.tool,
240
+ '--profile',
241
+ request.profile,
242
+ ],
243
+ { cwd: directory, timeoutMs: 120_000 }
244
+ );
245
+ }
246
+
247
+ function deploymentDirectory(
248
+ root: string,
249
+ revision: string,
250
+ bootstrapped: boolean
251
+ ): string {
252
+ return bootstrapped
253
+ ? root
254
+ : join(root, 'node_modules', '.fleet-revisions', revision);
255
+ }
@@ -0,0 +1,260 @@
1
+ import { parseWorkerPolicy, type WorkerPolicy } from './worker-policy';
2
+ import { readFileSync } from 'node:fs';
3
+ import { FleetUsageError } from './errors';
4
+ import { parseConfig, record } from './config';
5
+ import type { FleetConfig } from './types';
6
+ import { parseSecretsConfig, type SecretsConfig } from './secrets';
7
+
8
+ export interface MachineDefinition {
9
+ id: string;
10
+ ssh: string;
11
+ os: 'linux' | 'macOS' | 'windows';
12
+ network?: {
13
+ tailscaleName: string;
14
+ user: string;
15
+ sshPort: number;
16
+ t3Port: number;
17
+ };
18
+ }
19
+ /** The singleton manager: the human's machine. It runs fleet-sync and hosts
20
+ * every singleton service; it is never converged over SSH. */
21
+ export interface ManagerDefinition {
22
+ id: string;
23
+ os: 'macOS';
24
+ network: { tailscaleName: string; user: string };
25
+ neverSleepOnPower: true;
26
+ }
27
+ export interface FleetDefinition {
28
+ version: 3;
29
+ /** The Git repository that holds this fleet's configuration and pins. */
30
+ source: { repository: string };
31
+ /** Where this fleet keeps its secrets. */
32
+ secrets: SecretsConfig;
33
+ manager: ManagerDefinition;
34
+ /** Stateless, replaceable Linux boxes; clients of the manager's services. */
35
+ workers: MachineDefinition[];
36
+ services: {
37
+ /** Public HTTPS origin of the CLI proxy every node uses. */
38
+ proxy: { endpoint: string };
39
+ monitoring: {
40
+ hubPort: 8443;
41
+ /** Public dashboard origin. */
42
+ publicUrl: string;
43
+ /** Beszel account the manager enrolls nodes with. */
44
+ managerAccount: string;
45
+ /** Read-only Beszel account visitors are signed in as. */
46
+ viewerAccount: string;
47
+ };
48
+ };
49
+ worker?: WorkerPolicy;
50
+ ubuntu: { release: '26.04'; architectures: ['amd64', 'arm64'] };
51
+ }
52
+ const ID = /^[a-z][a-z0-9-]{0,62}$/;
53
+ const HOST = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/;
54
+ function fail(): never {
55
+ throw new FleetUsageError(
56
+ 'Invalid Fleet definition. Check version 3, one macOS manager, unique Linux worker IDs, singleton services and network metadata in fleet.json.'
57
+ );
58
+ }
59
+ function keys(value: Record<string, unknown>, allowed: string[]): boolean {
60
+ return Object.keys(value).every((key) => allowed.includes(key));
61
+ }
62
+ function network(value: unknown): NonNullable<MachineDefinition['network']> {
63
+ if (
64
+ !record(value) ||
65
+ !keys(value, ['tailscaleName', 'user', 'sshPort', 't3Port'])
66
+ )
67
+ return fail();
68
+ if (
69
+ typeof value['tailscaleName'] !== 'string' ||
70
+ !HOST.test(value['tailscaleName'])
71
+ )
72
+ return fail();
73
+ if (
74
+ typeof value['user'] !== 'string' ||
75
+ !/^[a-z_][a-z0-9_-]{0,31}$/.test(value['user'])
76
+ )
77
+ return fail();
78
+ for (const port of ['sshPort', 't3Port'])
79
+ if (!validPort(value[port])) return fail();
80
+ return value as NonNullable<MachineDefinition['network']>;
81
+ }
82
+ function validPort(value: unknown): boolean {
83
+ return (
84
+ Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535
85
+ );
86
+ }
87
+ function worker(value: unknown): MachineDefinition {
88
+ if (!record(value) || !keys(value, ['id', 'ssh', 'os', 'network']))
89
+ return fail();
90
+ if (typeof value['id'] !== 'string' || !ID.test(value['id'])) return fail();
91
+ if (typeof value['ssh'] !== 'string' || !HOST.test(value['ssh']))
92
+ return fail();
93
+ // Workers are the Linux boxes; any other platform is not a worker.
94
+ if (value['os'] !== 'linux') return fail();
95
+ return {
96
+ id: value['id'],
97
+ ssh: value['ssh'],
98
+ os: 'linux',
99
+ ...(value['network'] ? { network: network(value['network']) } : {}),
100
+ };
101
+ }
102
+ function manager(value: unknown): ManagerDefinition {
103
+ if (
104
+ !record(value) ||
105
+ !keys(value, ['id', 'os', 'network', 'neverSleepOnPower']) ||
106
+ typeof value['id'] !== 'string' ||
107
+ !ID.test(value['id']) ||
108
+ value['os'] !== 'macOS' ||
109
+ value['neverSleepOnPower'] !== true
110
+ )
111
+ return fail();
112
+ return {
113
+ id: value['id'],
114
+ os: 'macOS',
115
+ network: managerNetwork(value['network']),
116
+ neverSleepOnPower: true,
117
+ };
118
+ }
119
+ function managerNetwork(value: unknown): ManagerDefinition['network'] {
120
+ if (
121
+ !record(value) ||
122
+ !keys(value, ['tailscaleName', 'user']) ||
123
+ typeof value['tailscaleName'] !== 'string' ||
124
+ !HOST.test(value['tailscaleName']) ||
125
+ typeof value['user'] !== 'string' ||
126
+ !/^[a-z_][a-z0-9_.-]{0,31}$/.test(value['user'])
127
+ )
128
+ return fail();
129
+ return { tailscaleName: value['tailscaleName'], user: value['user'] };
130
+ }
131
+ export function parseDefinition(value: unknown): FleetDefinition {
132
+ if (
133
+ !record(value) ||
134
+ value['version'] !== 3 ||
135
+ !keys(value, [
136
+ '$schema',
137
+ 'version',
138
+ 'source',
139
+ 'secrets',
140
+ 'manager',
141
+ 'workers',
142
+ 'services',
143
+ 'ubuntu',
144
+ 'worker',
145
+ ])
146
+ )
147
+ return fail();
148
+ if (!Array.isArray(value['workers']) || value['workers'].length > 64)
149
+ return fail();
150
+ const lead = manager(value['manager']);
151
+ const workers = value['workers'].map(worker);
152
+ const ids = [lead.id, ...workers.map((entry) => entry.id)];
153
+ if (new Set(ids).size !== ids.length) return fail();
154
+ return {
155
+ version: 3,
156
+ source: source(value['source']),
157
+ secrets: secrets(value['secrets']),
158
+ manager: lead,
159
+ workers,
160
+ services: services(value['services']),
161
+ ubuntu: baseline(value['ubuntu']),
162
+ ...(value['worker'] === undefined
163
+ ? {}
164
+ : { worker: parseWorkerPolicy(value['worker']) }),
165
+ };
166
+ }
167
+ /** Singleton services name no host: they always run on the manager. */
168
+ function services(value: unknown): FleetDefinition['services'] {
169
+ if (!record(value) || !keys(value, ['proxy', 'monitoring'])) return fail();
170
+ const { proxy, monitoring } = value;
171
+ if (
172
+ !record(proxy) ||
173
+ !keys(proxy, ['endpoint']) ||
174
+ !origin(proxy['endpoint'])
175
+ )
176
+ return fail();
177
+ return {
178
+ proxy: { endpoint: proxy['endpoint'] },
179
+ monitoring: monitoringService(monitoring),
180
+ };
181
+ }
182
+ function monitoringService(
183
+ value: unknown
184
+ ): FleetDefinition['services']['monitoring'] {
185
+ const fields = ['hubPort', 'publicUrl', 'managerAccount', 'viewerAccount'];
186
+ if (!record(value) || !keys(value, fields) || value['hubPort'] !== 8443)
187
+ return fail();
188
+ const { publicUrl, managerAccount, viewerAccount } = value;
189
+ if (!origin(publicUrl) || !account(managerAccount) || !account(viewerAccount))
190
+ return fail();
191
+ return { hubPort: 8443, publicUrl, managerAccount, viewerAccount };
192
+ }
193
+ const origin = (value: unknown): value is string =>
194
+ typeof value === 'string' && /^https:\/\/[A-Za-z0-9.-]{1,253}$/.test(value);
195
+ const account = (value: unknown): value is string =>
196
+ typeof value === 'string' &&
197
+ /^[^\s@]{1,64}@[A-Za-z0-9.-]{1,253}$/.test(value);
198
+ function secrets(value: unknown): SecretsConfig {
199
+ return parseSecretsConfig(value) ?? fail();
200
+ }
201
+ function source(value: unknown): FleetDefinition['source'] {
202
+ if (
203
+ !record(value) ||
204
+ !keys(value, ['repository']) ||
205
+ typeof value['repository'] !== 'string' ||
206
+ !/^https:\/\/[A-Za-z0-9.-]+\/[A-Za-z0-9._/-]+\.git$/.test(
207
+ value['repository']
208
+ )
209
+ )
210
+ return fail();
211
+ return { repository: value['repository'] };
212
+ }
213
+ function baseline(ubuntu: unknown): FleetDefinition['ubuntu'] {
214
+ if (
215
+ !record(ubuntu) ||
216
+ !keys(ubuntu, ['release', 'architectures']) ||
217
+ ubuntu['release'] !== '26.04' ||
218
+ JSON.stringify(ubuntu['architectures']) !== '["amd64","arm64"]'
219
+ )
220
+ return fail();
221
+ return ubuntu as FleetDefinition['ubuntu'];
222
+ }
223
+
224
+ export function loadDefinition(path: string): FleetDefinition {
225
+ try {
226
+ return parseDefinition(JSON.parse(readFileSync(path, 'utf8')));
227
+ } catch {
228
+ return fail();
229
+ }
230
+ }
231
+ /** Local v1 roster supplies paths/requirements only for explicitly enrolled workers. */
232
+ export function enrolledRoster(
233
+ definition: FleetDefinition,
234
+ overrides: unknown
235
+ ): FleetConfig {
236
+ const roster = parseConfig(overrides);
237
+ for (const node of roster.nodes) {
238
+ const declared = definition.workers.find((entry) => entry.id === node.id);
239
+ if (!declared)
240
+ throw new FleetUsageError(
241
+ 'Roster contains a node that is not a declared worker. Add the worker to fleet.json in Git before rollout; the manager is never a roster node.'
242
+ );
243
+ if (declared.ssh !== node.ssh)
244
+ throw new FleetUsageError(
245
+ 'Fleet SSH assignment conflicts with the local roster. Review the migration; discovery cannot overwrite explicit assignments.'
246
+ );
247
+ }
248
+ return {
249
+ ...roster,
250
+ nodes: roster.nodes.map((node) => ({
251
+ ...node,
252
+ required: [
253
+ ...new Set([
254
+ ...node.required,
255
+ ...(definition.worker?.requiredCapabilities ?? []),
256
+ ]),
257
+ ],
258
+ })),
259
+ };
260
+ }