@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
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
+ }